본 프로젝트는 최주호 님의 「스프링부트 시큐리티 & JWT」 강의를 참고하여 진행하였습니다.
최주호 님의 「스프링부트 시큐리티 & JWT」 강의 git 주소
https://github.com/codingspecialist/-Springboot-Security-OAuth2.0-V3
버전 업데이트 이후 수정본으로 진행하는 git 주소
https://github.com/Solkot/Security_Oauth
-> branch마다 저장하면서 진행하고 있습니다. OAuth 전의 과정의 경우 Profile branch에 저장되어 있습니다.
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/**").authenticated()
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER") //ROLE_ADMIN에서 이제 ROLE_ 같은 접두사는 자동으로 붙기 때문에 사용 X
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/loginForm")
.loginProcessingUrl("/login")// /login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해줍니다.
.defaultSuccessUrl("/") //로그인 성공 후 이동할 기본 URL 지정
.permitAll() // 로그인 페이지와 로그인 요청 URL은 누구나 접근 가능하도록 허용
));
return http.build();}
주석에 달아놓긴 하였으나 실제로 어떻게 차이나는지 저도 궁금해서 찾아보았습니다.
사용자가 로그인이 필요한 페이지에 접근했을 때, 즉 권한이 필요하다고 생각하는 페이지에 접근했을때 스프링 시큐리가 로그인 페이지로 리다이렉트시켜 이동시키는 경로입니다.
즉, "로그인 폼이 있는 페이지" 입니다.
사용자가 로그인 폼에서 아이디, 비밀번호를 입력하고 전송할 때 (POST)
이 경로로 요청이 들어오면 Spring Security가 자동으로 가로채서 로그인 인증을 수행합니다.
즉, "로그인 검증을 실행하는 백엔드 처리 URL" 이라고 생각하면 됩니다.
<form action="/login" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">로그인</button>
</form>
action="/login"이면 사용자가 로그인 버튼을 눌렀을 때, /login으로 POST 요청 발생, UsernamePasswordAuthenticationFilter가 이 요청을 가로채고 인증 처리를 합니다.
따라서 따로 컨트롤러에 /login 매핑을 직접 만들 필요가 없습니다.
| 구분 | 내용 |
|---|---|
| 로그인 요청 | /login으로 POST |
| 필터 가로채기 | UsernamePasswordAuthenticationFilter |
| 사용자 조회 | UserDetailsService → loadUserByUsername() |
| 사용자 정보 변환 | User(DB 객체) → UserDetails(PrincipalDetails) |
| 비밀번호 검증 | PasswordEncoder |
| 인증 성공 | Authentication 객체 생성 |
| 세션 저장 | SecurityContextHolder |
| 보안 어노테이션 | @EnableMethodSecurity |
로그인 과정은 위와 같다.
간단하게 보자면 User -> UserDetails -> Authetication -> SecruitySession이라고 보면 된다.
추가적으로 설명을 덧붙이자면 아래에 나와있다.
사용자가 로그인 폼에서 아이디·비밀번호를 입력하고 /login 요청을 전송
스프링 시큐리티가 자동으로 등록한 UsernamePasswordAuthenticationFilter가 이미 존재하고,
그 필터가 /loginProcessingUrl()로 지정된 주소(/login) 요청을 자동으로 가로채서 로그인 처리를 합니다.
UsernamePasswordAuthenticationToken, 필터가 username/password를 꺼내서 인증용 토큰 객체를 만든다. 임시용 토큰이라서 아직 인증은 되지 않은 상태
AuthenticationManager, 토큰을 AuthenticationManager로 넘기면, 내부에서 UserDetailsService가 실행되어 loadUserByUsername을 통해 DB에서 사용자 정보를 조회
DB의 사용자 엔티티(User)를 시큐리티에서 사용할 수 있는 형태(UserDetails)로 변환
물론 Authentication 객체에 넘기려면 UserDetails 객체로 넘겨야 하는 이유도 있지만, 우리가 받는 정보에는 날 것의 정보가 들어가 있어, 필요한 정보만 가져가야만 보안에도 유리하다.
그렇기에 User에서 로그인에 필요한 최소 정보만 갖는 UserDetails를 사용한다.
PasswordEncoder, 요청한 비밀번호(평문)와 DB의 암호화된 비밀번호(BCryptPasswordEncoder)를 비교한다.
굳이 loadUserByUsername()에서 비밀번호를 확인하지 않고, 나중에 검증하는 이유는 "단일 책임 원칙"을 지키기 위해서라고 생각한다.

인증에 성공하면 Authentication 객체(인증된 사용자 정보, 권한 포함)를 생성한다.
SecurityContext에 Authentication을 저장해야 이후에도 로그인 유지가 가능하다.
이후 추가적으로 설명하지만 어노테이션을 통해 쉽게 웹페이지마다의 권한을 설정한다고 보면 된다.
@GetMapping("/test/login")
public @ResponseBody String testLogin(Authentication authentication, //DI -> downcasting -> userObject //DI(의존성 주입)
@AuthenticationPrincipal PrincipalDetails userDetails) { //DI -> getUser, 원랴는 UserDetails를 받기에 PrincipalDetails로도 받을 수 있음
System.out.println("/test/login==");
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); //원래는 UserDetail로 DownCasting해서 받아야 하지만, PrincipalDetails이 UserDetail을 implementation하기에 가능
System.out.println("authentication : "+ principalDetails.getUser());
System.out.println("userDetails: "+ userDetails.getUser());
return "세션 정보 확인하기";
}
Authentication은 UserDetails를 받는만큼 클라이언트의 정보를 확인할 수 있다.
Authentication은 스프링 시큐리티의 핵심 인증 객체입니다.
로그인에 성공하면 SecurityContextHolder에 저장되고 로그인이 계속 유지된다.
Authentication은 다운캐스팅을 통해 정보를 확인할 수 있지만, @AuthenticationPrincipal은 그럴 필요가 없다.
왜 그런지는 추가적으로 조사해본 결과
| 구분 | Authentication | @AuthenticationPrincipal |
|---|---|---|
| 의미 | 스프링 시큐리티의 전체 인증 객체 | 인증 객체 안의 principal(UserDetails)만 주입 |
| 포함 정보 | principal, credentials, authorities, details 등 | principal (즉, 로그인한 사용자 정보) |
| 사용 위치 | 컨트롤러, 서비스, 시큐리티 내부 | 주로 컨트롤러 파라미터 |
| 형태 | 인터페이스(Authentication) | 어노테이션(@AuthenticationPrincipal) |
| 사용 목적 | 전체 인증 상태 접근 | 로그인한 사용자 정보만 간단히 사용 |
즉, Authentication이 더 여러 정보를 가지고 있지만, 로그인한 사용자 정보만 간단히 조회하려면 @AuthenticationPrincipal이 사용하기 편리하다.
Spring Security 6.x부터는
@EnableGlobalMethodSecurity가 Deprecated(사용 중단) 되었고,
대신 @EnableMethodSecurity를 사용한다.

기본적으로 스프링 필터에 @EnableMethodSecurity를 사용한다.

@Secured 에노테이션을 사용할 수 있다.
@Secured의 경우 조건식은 불가능해 권한을 한 가지 걸 수 있다.

@PreAuthorize, @PostAuthorize 애노테이션을 사용할 수 있다.
@PreAuthorize, @PostAuthorize의 경우 조건식을 통해 다양한 조건을 사용할 수 있으며, 조건식이 존재한다.
단순한 Role 체크만 필요할 때는 @Secured가 더 직관적이기에 사용한다.
| 구분 | @Secured | @PreAuthorize |
|---|---|---|
| 권한 표현 | 단일 Role만 가능 | SpEL 사용 가능 → 복수 권한, 조건식 가능 |
| 장점 | 간단하고 직관적 | 복잡한 권한 로직 가능, 파라미터 검증 가능 |
| 단점 | 조건식 불가 | 단순 체크만 해도 다소 장황 |
| 사용 이유 | 단순 Role 체크 시 코드 간결 | 복잡한 조건 필요 시 |
참고로 메소드가 아닌 global하게 걸고 싶으면 SpringSecurity 체인에 가서 걸어버리면 된다.
Spring Security 6.x부터는
@EnableGlobalMethodSecurity가 Deprecated(사용 중단) 되었고,
대신 @EnableMethodSecurity를 사용한다.
이로써 Spring Boot Security + JWT 프로젝트의 기본 로그인 과정이 완료되었습니다.
다음 단계에서는 OAuth-goole 과정을 진행할 예정입니다.