[JWT] (수정중) Spring Security에서의 JWT 인증 코드 정리
📁 디렉토리
* 🌳 = 클래스, 🌱 = 해당 클래스 내 메서드, 내부 클래스
JWT 적용을 위한 사전 작업
: JWT 적용 코드를 작성하기 전에 기초적인 Spring Security 설정 작업을 한다.
- build.gradle 에 의존 라이브러리 추가 (jjwt)
- UserDto의 Post 내부클래스에 password 필드 추가
- Member 엔티티 클래스에 password 필드 추가
- MemberService 클래스에 사용자 등록시 패스워드+권한정보 함께 DB에 저장되도록 관련 코드 추가
(PasswordEncoder, CustomAuthorityUtils DI)
*🌳CustomAuthorityUtils : 메모리, DB에 저장된 Role 기반으로 권한 정보 생성해주는 Util 이다.
- 🌳 SecurityConfiguration : Spring Security 를 이용한 보안 강화를 위한 최소한의 보안 설정 구성 클래스
🌱 SecurityFilterChain : 보안을 위한 특정 작업 처리 (여러개의 Filter)
: crsf 공격 설정 비활성화, CORS 설정, 폼 로그인 방식 설정, HTTP request 요청 접근 설정 등
🌱 PasswordEncoder : PasswordEncoder Bean 객체 생성
: 비밀번호를 암호화해준다.
🌱 CorsConfigurationSource : 구체적인 CORS 정책 설정
- setAllowedOrigins() - 모든 출처에 대해 스크립트 기반의 HTTP 통신 허용
- setAllowedMethods() - 파라미터로 지정한 HTTP Method HTTP 통신 허용
- registerCorsConfiguration() - 모든 URL 에 CORS 정책 적용
JWT 자격 증명을 위한 로그인 인증 구현
사용자의 로그인 인증 성공 후, JWT가 클라이언트에게 전달되는 과정
- 클라이언트가 서버 측에 로그인 인증 요청(Username/Password를 서버 측에 전송)
- 로그인 인증을 담당하는 Security Filter(JwtAuthenticationFilter)가 클라이언트의 로그인 인증 정보 수신
- Security Filter가 수신한 로그인 인증 정보를 AuthenticationManager에게 전달해 인증 처리를 위임
- AuthenticationManager가 Custom UserDetailsService(MemberDetailsService)에게 사용자의 UserDetails 조회를 위임
- Custom UserDetailsService(MemberDetailsService)가 사용자의 크리덴셜을 DB에서 조회한 후, AuthenticationManager에게 사용자의 UserDetails를 전달
- AuthenticationManager가 로그인 인증 정보와 UserDetails의 정보를 비교해 인증 처리
- JWT 생성 후, 클라이언트의 응답으로 전달
4번, 6번 ➡ Spring Security 의 AuthenticationManager 가 대신 처리해줌
2번, 3번, 7번 : 🌳 JwtAuthenticationFilter
5번 : 🌳 MemberDetailsService 를 구현하면 됨!
🌳 JwtAuthenticationFilter : Custom Security Filter
(extends UsernamePasswordAuthenticationFilter - 폼로그인 Default Security Filter)
: 클라이언트의 로그인 인증 정보 직접적 수신 ➡ 인증 처리의 EntryPoint 역할
- 로그인 인증을 담당하는 Security Filter(JwtAuthenticationFilter)가 클라이언트의 로그인 인증 정보 수신
- Security Filter가 수신한 로그인 인증 정보를 AuthenticationManager에게 전달해 인증 처리를 위임
- JWT 생성 후, 클라이언트의 응답으로 전달
🌱 attemptAuthentication() : 메서드 내부에서 인증을 시도하는 로직
- ObjectMapper - DTO로 역직렬화 위함
- objectMapper.readValue(request.getInputStream(), LoginDto.class)
- UsernamePasswordAuthenticationToken - Username, Password 정보 포함
- authenticationManager.authenticate(authenticationToken) - AuthenticationManage에게 전달, 인증처리 위임
🌱 successfulAuthentication() : 인증에 성공할 경우 호출
- authResult.getPrincipal() - 엔티티 클래스 객체 획득 (principal 필드에 Member 객체 할당)
- delegateAccessToken & delegateRefreshToken - 토큰 생성
- setHeader()
🌱 delegateAccessToken() : 토큰 생성
jwtTokenizer.generateAccessToken(member)
🌱 delegateRefreshToken() : 토큰 생성
jwtTokenizer.refreshAccessToken(member)
➡ 작성 완료 후 🌳 SecurityConfiguration 에 적용시켜줘야 한다!
🌱SecurityFilterChain ➡ .apply(new CustomFilterConfigurer())
🌱 public class CustomFilterConfigurer (extends AbstractHttpConfigurer)
: 구현한 JwtAuthenticationFilter 등록 역할
🌳 MemberDetailsService : Custom UserDetailsService
(implements UserDetailsService)
: Custom UserDetailsService(MemberDetailsService)가 사용자의 크리덴셜을 DB에서 조회한 후,
AuthenticationManager에게 사용자의 UserDetails를 전달
🌱@Override 메서드들
(getAuthorities(), getUsername(), isAccountNonExpired(), isCrendentialsNonExpired(), isEnabled())
🌳 LoginDTO : 클라이언트가 전송한 Username / Password 정보를 역직렬화
(Security Filter 에서 사용할 수 있도록)
🌳 JwtTokenizer : 로그인 인증에 성공한 클라이언트에게 JWT 생성 및 발급,
클라이언트의 요청 들어올 때마다 전달된 JWT 검증
@Getter
@Value("${jwt.key}")
private String secretKey;
@Getter
@Value("${jwt.access-token-expiration-minutes}")
private int accessTokenExpirationMinutes;
@Getter
@Value("${jwt.refresh-token-expiration-minutes}")
private int refreshTokenExpirationMinutes;
: @Value 어노테이션 - JWT 생성 시 필요한 정보 (application.yml 에서 로드)
🌱 encodeBase64SecretKey() : plain Password 를 Base64 문자열로 인코딩
🌱 generateAccessToken(): 인증된 사용자에게 JWT 최초 생성 메서드
- setClaims() - 인증된 사용자와 관련된 정보 추가
- setSubject() - JWT 제목
- setIssuedAt() - JWT 발행 일자
- setExpiration() - JWT 만료 일시
- signWith() - 서명 키 객체 설정
- compact() - JWT 생성, 직렬화
🌱 generateRefreshToken(): AccessToken 만료시 AccessToken 생성
🌱 getClaims(): Signature 내부 검증 후, JWT 파싱해서 Claims를 얻음
* Jws? = JWT + Signature
🌱 verifySignature(): Signature를 검증하는 용도
🌱 getTokenExpiration(): JWT의 만료 일시 지정 메서드
🌳 application.yml :
jwt:
key: ${JWT_SECRET_KEY} # 민감한 정보는 시스템 환경 변수에서 로드한다.
access-token-expiration-minutes: 30
refresh-token-expiration-minutes: 420
* SecretKey : 민감한 정보이므로 Windows 환경 변수 - 시스템 변수에 등록한다.
application.yml 프로퍼티 명 ≠ 시스템 환경 변수 문자열 (의도하지 않은 값으로 채워질 수도!)
로그인 인증 성공 및 실패에 따른 추가 처리
: 로그인 인증 성공시 로그 기록, 사용자 정보를 response 로 전송
로그인 인증 실패시에도 인증 실패에 추가 처리 가능
🌳 AuthenticationSuccessHandler
(implements AuthenticationSuccessHandler)
🌱 onAuthenticationSuccess (Override)
🌳 AuthenticationFailureHandler :
((implements AuthenticationFailureHandler)
🌱 onAuthenticationFailure (Override)
🌱 sendErrorResponse
⬇
🌳 SecurityConfiguration 에 추가
🌱 CustomFilterConfigurer 에
jwtAuthenticationFilter.setAuthenticationSuccessHandler, jwtAuthenticationFilter.setAuthenticationFailureHandler
🌳 JwtAuthenticationFilter 에 호출 코드 추가
🌱 successfulAuthentication() 에 this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult)
로그인 인증을 성공적으로 수행하면
response header(Authorization, Refresh) 를 통해 JWT를 전달 받을 수 있다~
JWT를 이용한 자격 증명 및 검증 구현
: 클라이언트에서 JWT를 이용해 자격 증명이 필요한 리소스에 대한 request 전송 시,
request header 를 통해 전달받은 JWT를 서버 측에서 검증하는 기능
🌳 JwtVerificationFilter : request header에 포함된 JWT에 대해 검증 작업을 수행
(extends OncePerRequestFilter)
🌱 doFilterInternal()
🌱 shouldNotFilter()
🌱 verifyJws()
🌱 setAuthenticationToContext()
⬇
🌳 SecurityConfiguration 에 업데이트
- 세션 정책 설정 추가
- JwtVerificationFilter 추가
서버 측 리소스에 역할 기반 권한 적용
.authorizeHttpRequests(authorize -> authorize
.antMatchers(HttpMethod.POST, "/*/members").permitAll()
.antMatchers(HttpMethod.PATCH, "/*/members/**").hasRole("USER")
.antMatchers(HttpMethod.GET, "/*/members").hasRole("ADMIN")
.antMatchers(HttpMethod.GET, "/*/members/**").hasAnyRole("USER", "ADMIN")
.antMatchers(HttpMethod.DELETE, "/*/members/**").hasRole("USER")
.anyRequest().permitAll()
);
PostMan 으로 JWT 검증 테스트
예외 처리 로직 추가
🌳 JwtVerificationFilter
🌳 AuthenticationEntryPoint
🌳 ErrorResponder
🌳 AccessDeniedHandler
⬇
🌳 SecurityConfiguration 에 추가