이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › 보안
인증과 인가는 어떻게 다른가
인증과 인가는 서버가 요청을 받았을 때 하는 두 가지 확인입니다. 인증은 요청을 보낸 사람이 누구인지 확인하는 것(아이디와 비밀번호가 맞는지)이고, 인가는 그 사람이 이 일을 해도 되는지 확인하는 것(권한이 있는지)입니다. 순서는 인증이 먼저입니다. 이 확인은 컨트롤러의 코드에 없습니다. 요청이 컨트롤러에 닿기 전에 스프링 시큐리티가 하고, 그 규칙이 화면의 위쪽 설정 부분입니다. .requestMatchers("/admin").hasRole("ADMIN") 줄은 /admin을 ADMIN 권한이 있는 사람에게만 열어 주는 인가 규칙이고, .anyRequest().authenticated() 줄은 그 밖의 주소를 누구인지 확인된 사람에게만 열어 주는 규칙이며, .httpBasic(...) 줄은 요청에 실린 아이디와 비밀번호로 누구인지 확인하는 방법입니다. 이 실습은 인가 규칙을 지우거나 열어 두면서, 관리자 admin, 일반 회원 kim, 틀린 비밀번호, 아이디 없음의 요청을 /admin에 보내 어느 확인에서 막히고 어떤 응답이 나가는지 봅니다.
실행으로 대조 · 스프링 부트 4.1.1과 스프링 시큐리티 7.1.1에서 열두 가지 조합을 실제 서버에 HTTP Basic 요청을 보내 대조
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
// 인가: /admin은 ADMIN 권한이 있는 사람만
.requestMatchers("/admin").hasRole("ADMIN")
// 인가 규칙: 그 밖의 주소는 누구인지 확인된(인증된) 사람만 허용
.anyRequest().authenticated())
// 요청에 실린 아이디와 비밀번호로 누구인지 확인한다
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
@RestController
public class AdminController {
@GetMapping("/admin")
public String admin() {
return "관리자 화면";
}
}
스프링 부트 4.1.1(스프링 프레임워크 7.0.9, 내장 톰캣 11)과 스프링 시큐리티 7.1.1을 JDK 21에서 실행한 결과입니다. 열두 가지 조합을 모두, 화면의 코드와 같은 소스(보안 설정 SecurityConfig와 컨트롤러 AdminController, 컨트롤러에는 실행 횟수를 세는 줄만 더함)를 패키지 app으로 컴파일하고 자식 자바 프로세스로 실제 서버를 띄운 뒤, 실제 HTTP 요청(GET /admin)을 보내 확인했습니다. 아이디와 비밀번호는 Authorization 헤더에 실어 보냈고, 요청마다 서버를 새로 띄웠습니다. 화면에서 줄인 것은 package와 import 줄, 시작 클래스, 그리고 사용자 등록입니다. 사용자는 메모리 안의 사용자 저장소(InMemoryUserDetailsManager)에 일반 회원 kim(역할 USER)과 관리자 admin(역할 ADMIN)을 둔 것이고, 비밀번호는 {noop} 접두어를 붙여 암호화하지 않은 시험용 값입니다(실제 서비스에서는 비밀번호를 암호화해 저장합니다). 보안 설정은 화면의 줄 말고는 아무것도 더하지 않았습니다. CSRF와 세션 설정은 기본값이고, GET 요청이라 결과에 영향을 주지 않았습니다. "서버가 확인한 사람"은 요청이 처리되는 동안 SecurityContextHolder에 어떤 Authentication이 기록되었는지로 얻었습니다. SecurityContextHolder의 저장 방식을 기본 방식을 감싼 기록용 방식으로 바꿔 읽고 쓰는 때마다 이름과 권한을 적었으므로, 컨트롤러에 가지 못한 요청의 인증 정보도 볼 수 있고 보안 필터 체인에는 아무것도 더하지 않았습니다. 한계는 필터 안에서 일어난 일을 읽고 쓰는 시점으로만 본다는 것입니다(필터의 내부 판단 자체를 본 것은 아닙니다). 대조한 것은 이 기록에서 얻은 확인된 사람(없으면 익명이거나 아무것도 설정되지 않음), 상태 코드와 본문, WWW-Authenticate 헤더 유무, admin() 메서드의 실행 횟수, 인증이 인가보다 먼저 일어나는 순서, 오류 응답이 /error로 한 번 더 넘어가는지입니다. 문서가 정한 것은 RFC 9110의 11절 HTTP Authentication(11.1, 11.6.1)과 15.5.2절(401은 유효한 인증 정보가 없다는 뜻이고 WWW-Authenticate를 보내야 한다), 15.5.4절(403은 서버가 요청을 이해했지만 처리를 거부한다는 뜻), 스프링 시큐리티 문서 Authorize HttpServletRequests(AuthorizationFilter가 규칙을 위에서부터 차례로 보고 첫 번째로 맞는 것만 적용한다는 것, hasRole이 ROLE_ 접두어를 붙인다는 것, 거부되면 AccessDeniedException이 던져진다는 것)와 Authentication의 Architecture, Basic 절(인증되지 않은 요청이 거부되면 AuthenticationEntryPoint가 WWW-Authenticate 헤더를 보낸다는 것, 인증이 실패하면 SecurityContextHolder를 비우고 같은 처리기를 부른다는 것)입니다. 문서 내용은 WebFetch로 열어 요약된 결과로 확인했고 RFC 전문을 직접 읽지는 못했습니다. 아래는 문서가 정한 것이 아니라 이 버전에서 관찰한 동작입니다. 첫째, 이 화면의 코드를 그대로 실행하면 일반 회원 kim의 요청에 403이 그대로 나갑니다. 오류 응답이 /error로 한 번 더 넘어가는데, 그 두 번째 요청에서도 kim이 확인된 사람으로 기록되어 막히지 않았기 때문입니다. 세션을 쓰지 않고 직접 만든 필터로 인증한 "시큐리티 필터 체인" 실습에서는 같은 단계에서 403이 401로 바뀌었으니, 구성에 따라 다를 수 있습니다. 둘째, 틀린 비밀번호는 /admin이 permitAll()이어도 401입니다. 아이디와 비밀번호가 실린 요청은 인가 규칙을 보기 전에 인증에서 거절되었고, 인가 단계의 흔적은 기록에 없었습니다. 셋째, 아이디가 없는 요청은 인증에서 막히지 않고 익명 사용자로 인가 단계까지 가며, 거기서 규칙이 막으면 401, 열려 있으면 컨트롤러까지 갑니다. 넷째, 401 응답에는 WWW-Authenticate 헤더(값은 Basic realm="Realm")가 붙고 403과 200 응답에는 없었습니다. 다섯째, 응답 본문은 401이 비어 있고 403은 스프링 부트의 오류 페이지였는데, 요청의 Accept 헤더에 따라 달라질 수 있어 화면에서는 상태 코드만 다뤘습니다. 여섯째, 스프링 시큐리티 7.1에서는 로그인한 사용자의 권한에 ROLE_ 권한 말고 인증 방식을 나타내는 권한(FACTOR_PASSWORD)이 함께 기록되는데, 이 실습에서는 다루지 않았습니다. 로그인 방식(로그인 화면, 세션, 토큰)은 다루지 않았습니다. 이 실습의 요청은 요청마다 아이디와 비밀번호를 실어 보내는 HTTP Basic 방식이고, 로그인 상태를 이어 가는 세션과 토큰은 기초개념의 "세션", "토큰" 실습과 공부 쪽 "시큐리티 필터 체인" 실습에서 다룹니다. 실제 필터 체인에는 여기 적지 않은 필터가 더 있습니다.
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.