Spring/Spring Security

[JWT] (수정중) Spring Security에서의 JWT 인증 코드 정리

jungha_k 2022. 11. 25. 14:48

📁 디렉토리

 

 

 

* 🌳 = 클래스, 🌱 = 해당 클래스 내 메서드, 내부 클래스

 

 

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가 클라이언트에게 전달되는 과정

  1. 클라이언트가 서버 측에 로그인 인증 요청(Username/Password를 서버 측에 전송)
  2. 로그인 인증을 담당하는 Security Filter(JwtAuthenticationFilter)가 클라이언트의 로그인 인증 정보 수신
  3. Security Filter가 수신한 로그인 인증 정보를 AuthenticationManager에게 전달해 인증 처리를 위임
  4. AuthenticationManager가 Custom UserDetailsService(MemberDetailsService)에게 사용자의 UserDetails 조회를 위임
  5. Custom UserDetailsService(MemberDetailsService)가 사용자의 크리덴셜을 DB에서 조회한 후, AuthenticationManager에게 사용자의 UserDetails를 전달
  6. AuthenticationManager가 로그인 인증 정보와 UserDetails의 정보를 비교해 인증 처리
  7. 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 에 추가