본문 바로가기

Back-end/Spring Security

[Spring Security] Security + JWT 설정 (JwtUtil편)

✅ Security Config + Jwt Filter추가

  @Bean
    public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
        http
                // 새로운 방식으로 CORS 설정 적용
                .cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
                //csrf disable
                .csrf(AbstractHttpConfigurer::disable)

                //form 로그인 방식 disalbe
                .formLogin(AbstractHttpConfigurer::disable)


                //http basic 인증 방식 disable
                .httpBasic(AbstractHttpConfigurer::disable)
                //JwtFilter 추가
                .addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
                //JwtAuthentication Exception Hadling
                .exceptionHandling(authenticationManager -> authenticationManager
                        .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                        .accessDeniedHandler(new CustomAccessDeniedHandler()))

                .authorizeHttpRequests(authorize -> authorize
                                // 공개 접근 허용 URL 설정
                                .requestMatchers("/" , "/api/member/login").permitAll()
                                .requestMatchers("/css/**", "/js/**", "/img/**", "/favicon.ico").permitAll()
                                .requestMatchers("/admin/**").hasRole("ADMIN")
//                        .requestMatchers("/seller/**").hasRole("SELLER")
                                .requestMatchers("/mypage/**").hasRole("USER")
                                .anyRequest().authenticated()
                )

                //JWT 사용하기 때문에 세션 설정 : STATELESS
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/")
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID")
                        .permitAll()
                )
                //GET 요청을 통해 로그아웃을 처리하도록 허용
                .logout(logout -> logout.logoutRequestMatcher(
                        new OrRequestMatcher(
                                new AntPathRequestMatcher("/logout", "GET"),
                                new AntPathRequestMatcher("/logout", "POST")
                        )
                ))
//                .userDetailsService(customUserDetailsService)
                // OAuth2 로그인 설정
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/login")
                        .userInfoEndpoint(userInfo -> userInfo
                                .userService(customOAuth2UserService)
                        )
                        .successHandler(customOAuth2SuccessHandler)
                        .failureHandler((request, response, exception) -> {
                            // OAuth2 로그인 실패 시 처리
                            response.sendRedirect("/login?error=oauth2");
                        })
                );

        return http.build();
    }

 

 

🖥️ JwtUtil 코드

@Slf4j
@Component
public class JwtUtil {

    private final SecretKey secretKey ;
    private final Long expirationTime;
    private final Long refreshExpirationTime;

    private final CustomUserDetailsService customUserDetailsService;

    private final MemberRepository memberRepository;

    private final RefreshTokenRepository refreshTokenRepository;


    public JwtUtil(
            @Value("${spring.jwt.secret}") String secret,
            @Value("${spring.jwt.expiration-time}") Long expirationTime,
            @Value("${spring.jwt.refresh-expiration-time}") Long refreshExpirationTime,
            CustomUserDetailsService customUserDetailsService, MemberRepository memberRepository, RefreshTokenRepository refreshTokenRepository) {
        this.customUserDetailsService = customUserDetailsService;
        this.memberRepository = memberRepository;
        this.refreshTokenRepository = refreshTokenRepository;
        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),
                Jwts.SIG.HS256.key().build().getAlgorithm());
        this.expirationTime = expirationTime;
        this.refreshExpirationTime = refreshExpirationTime;
    }

    // 토큰에서 회원 이름 추출
    public String getUsername(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
    }

    // 토큰에서 이메일 추출 PK
    public String getEmail(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("email", String.class);
    }

    //JWT 토큰에서 인증정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = customUserDetailsService.loadUserByUsername(this.getEmail(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    public Member getMember(String token) {
        return memberRepository.findByEmail(this.getEmail(token))
                .orElseThrow(() -> new UsernameNotFoundException("Not Found USER"));
    }

    public Boolean isExpired(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }
    public JwtDto createTokens(Member member) throws ParseException {
        String accessToken = createAccessToken(member);
        String refreshToken = createRefreshToken(member);

        log.info("success to create tokens");
        return JwtDto.builder()
                .grantType("Bearer")
                .access_token(accessToken)
                .refresh_token(refreshToken)
                .build();
    }

    public String createAccessToken(Member member) throws ParseException {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expirationTime);
        // JWT access token 생성
        return Jwts.builder()
                .claim("email", member.getEmail())
                .claim("name", member.getMemberName())
                .claim("roles", member.getRoles())
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(expiryDate)
                .signWith(secretKey)
                .compact();// builder에서 최종적으로 JWT string 생성
    }

    public String createRefreshToken(Member member) throws ParseException {

        // 토큰의 subject가 user의 Email로 저장되므로 loadUserByUsername()의 인자로 넘기기
//        Member member = (Member) customUserDetailsService.loadUserByUsername(accessToken);
        // 해당 유저에 대해 refresh token 조회
        Optional<RefreshToken> refreshTokenByUser = refreshTokenRepository.findByMemberEmail(member.getEmail());

        // refresh token이 이미 존재하는 경우
        if(refreshTokenByUser.isPresent())
            return refreshTokenByUser.get().getRefreshToken(); // 해당 refresh token 반환

        // refresh token이 존재하지 않으므로 refresh token 생성
        Date refreshValidity = new Date(new Date().getTime() + refreshExpirationTime); // refreshToken 만료 기간 설정
        String refreshToken = Jwts.builder()
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(refreshValidity)
                .signWith(secretKey)
                .compact(); // 최종 JWT string 생성

        // RefreshTokenRepository에 user와 매핑해 refresh token 저장
        refreshTokenRepository.save(new RefreshToken(member, refreshToken, refreshValidity));

        return refreshToken;
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token.");
        } catch (UnsupportedJwtException e) {
            log.info("JWT token not supported.");
        } catch (IllegalArgumentException e) {
            log.info("JWT token is invalid");
        }
        return false;
    }

    public String validateRefreshToken(String accessToken, String token) throws ParseException {
        RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token)
                .orElseThrow(() -> new BusinessException(ErrorCode.REFRESH_TOKEN_INVALD));

        // 만료된 accessToken의 주인과 입력한 refresh token의 유저가 같은지 판단
        if(!this.getEmail(accessToken).equals(refreshToken.getMember().getEmail()))
            throw new BusinessException(ErrorCode.NOT_MATCH_USER_ACCESSTOKEN_REFRESHTOKEN);

        // refresh token이 유효하지 않은 경우 (만료기간이 지난 경우) => 로그인 풀도록 요청
        if(refreshToken.getValidityDate().before(new Date())) {
            // RefreshToken DB에서 해당 엔티티 삭제 후 만료 response 반환
            refreshTokenRepository.delete(refreshToken);
            return HttpStatus.UNAUTHORIZED.toString();
        }
        // refresh token이 유효한 경우 => access token 발급
        else{
            Member member = (Member) customUserDetailsService.loadUserByUsername(refreshToken.getMember().getEmail());
            return createAccessToken(member);
        }
    }
}

 

🖥️ validation token & refresh token validation 코드

public boolean validateToken(String token) {
    try {
        Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
        return true;
    } catch (SecurityException | MalformedJwtException e) {
        log.info("Invalid JWT signature.");
    } catch (ExpiredJwtException e) {
        log.info("Expired JWT token.");
    } catch (UnsupportedJwtException e) {
        log.info("JWT token not supported.");
    } catch (IllegalArgumentException e) {
        log.info("JWT token is invalid");
    }
    return false;
}

public String validateRefreshToken(String accessToken, String token) throws ParseException {
    RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token)
            .orElseThrow(() -> new BusinessException(ErrorCode.REFRESH_TOKEN_INVALD));

    // 만료된 accessToken의 주인과 입력한 refresh token의 유저가 같은지 판단
    if(!this.getEmail(accessToken).equals(refreshToken.getMember().getEmail()))
        throw new BusinessException(ErrorCode.NOT_MATCH_USER_ACCESSTOKEN_REFRESHTOKEN);

    // refresh token이 유효하지 않은 경우 (만료기간이 지난 경우) => 로그인 풀도록 요청
    if(refreshToken.getValidityDate().before(new Date())) {
        // RefreshToken DB에서 해당 엔티티 삭제 후 만료 response 반환
        refreshTokenRepository.delete(refreshToken);
        return HttpStatus.UNAUTHORIZED.toString();
    }
    // refresh token이 유효한 경우 => access token 발급
    else{
        Member member = (Member) customUserDetailsService.loadUserByUsername(refreshToken.getMember().getEmail());
        return createAccessToken(member);
    }
}

 

 

✏️ 공부한점

① 스프링부트에서 @Value는 @RequiredArgsConstructor에 의해 주입받지 못한다.

 

필드 변수를 선언할때 @RequiredArgsConstructor를 이용하기 위해 항상 하던 것처럼

@Value 변수를 선언할 때에도 private final로 선언했지만 다음과 같은 에러가 발생

Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

Description:

Parameter 0 of constructor in ai.planit.superb.domain.mail.service.Test required a bean of type 'java.lang.String' that could not be found.


Action:

Consider defining a bean of type 'java.lang.String' in your configuration.


Lombok의 @RequiredArgsConstructor로 필드의 private final의 컴파일 에러는 해결된 것처럼 보이지만 

@Value가 주입되는 타이밍과 @RequiredArgsConstructor의 주입 타이밍이 달라 에러가 발생하는 것이 원인

그래서 생성자 주입방식으로 하나씩 값을 넣어주었다.

public JwtUtil(
            @Value("${spring.jwt.secret}") String secret,
            @Value("${spring.jwt.expiration-time}") Long expirationTime,
            @Value("${spring.jwt.refresh-expiration-time}") Long refreshExpirationTime,
            CustomUserDetailsService customUserDetailsService, MemberRepository memberRepository, RefreshTokenRepository refreshTokenRepository) {
        this.customUserDetailsService = customUserDetailsService;
        this.memberRepository = memberRepository;
        this.refreshTokenRepository = refreshTokenRepository;
        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),
                Jwts.SIG.HS256.key().build().getAlgorithm());
        this.expirationTime = expirationTime;
        this.refreshExpirationTime = refreshExpirationTime;
    }

 

Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt)

Jwts.parser()로 토큰을 열기위한 도구 세팅! 
(파서)verifyWith(key)로  서명을 검증할 키를 설정하고 (키)build()로 최종적으로 토큰을 열기위한 도구 세팅 완료 ( JwtParser 객체 생성)parseSignedClaims(jwt)로 jwt문자열을 열어서 서명이 잘 되었는지 확인!

 

 

③ Jwt생성

subject(토큰을 발급받는 주체가 누구인지),
issuer(토큰을 발행하는 주체가 누구인지) ,
issuedAt(토큰이 언제 발급되었는지),
expiration(토큰의 만료시간은 언제인지),
claims(어떤 정보가 있는지),
signWith(어떤 key로 토큰에 서명을 할건지),
compact() : 설정된 모든 정보를 기반으로 JWT토큰 생성하고 문자열로 반환!