오늘의 TIL 2024-06-05 jwt(3)

이재성·2024년 6월 7일
post-thumbnail

스프링 시큐리티를 이용한 로그인 토큰 재발급 (1)

  • mySQL
  • 스프링 부트
  • 인텔리제이

진행과정

  • SecurityConfig
  • SecurityProvider
  • JwtFilter
  • Entity
  • Controller
  • Service
  • UserDetails
  • Repository

예외상황

  • 환경변수를 잘못 입력하면 에러가남
  • 클라이언트 요청 할때 필터를 제일 먼저 거쳐가기때문에 인가 설정을 잘해놓아야됨

SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@ComponentScan(basePackages = "com.sparta.areadevelopment.jwt")
public class SecurityConfig {
   private final TokenProvider tokenProvider;
    //암호화
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //csrf disable
        http
                .csrf(AbstractHttpConfigurer::disable);
        //폼을통한 로그인 방식 disable
        http
                .formLogin(AbstractHttpConfigurer::disable);
        //http basic 인증방식 disable
        http
                .httpBasic(AbstractHttpConfigurer::disable);
        //경로 별 인가
        http
                .authorizeHttpRequests((auth) ->auth
                                .requestMatchers("/auth/reissue","/**","/auth/login").permitAll()
                                .anyRequest().authenticated()
                        );
        http
                .addFilterBefore(new JwtAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);

        //세션 jwt를 통해 인증 인가를 위해 stateless 상태 설정
        http
                .sessionManagement((session) ->session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

SecurityProvider

@Slf4j
@Component
public class TokenProvider {
    private static final String AUTHORITIES_KEY = "auth";
    private static final String BEARER_TYPE = "Bearer";
    private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;            // 30분
    private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 14;  // 2주

    private final Key key;

    public TokenProvider(@Value("${JWT_SECRET_KEY}") String secretKey) {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    /**
     * 유저 정보를 통해 토큰 생성
     */
    public TokenDto generateToken(Authentication authentication) {
        log.info("generateToken start");

        long now = (new Date()).getTime();
        Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME); // 30분
        Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRE_TIME); // 14일

        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim("auth", "USER")
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        log.info(parseClaims(accessToken).toString());

        String refreshToken = Jwts.builder()
                .setExpiration(refreshTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        log.info("accessToken: {}", accessToken);
        return TokenDto.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    /**
     * 토큰에서 유저 정보 추출
     */
    public Authentication getAuthentication(String accessToken) {
        Claims claims = parseClaims(accessToken);

        if (claims.get("auth") == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }
        log.info("claims: {}", claims);
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get("auth").toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        log.info("authorities: {}", authorities);
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    /**
     * 토큰 정보 검증
     */
    public boolean validateToken(String token) {
        log.info("validateToken start");
        log.info("token: {}", token);
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
        } catch (ExpiredJwtException e) {
            // refresh token 활용해서 재발급
            log.info("Expired JWT Token", e);
            throw e;
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty.", e);
        }
        return false;
    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(accessToken)
                    .getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }

    public String getUsername(String refreshToken) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(refreshToken)
                .getBody();
        return claims.getSubject();
    }
}

JwtFilter

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
    private final TokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);


        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    // Request Header 에서 토큰 정보 추출
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

}

Entity

  • 테이블 이름은 User로 만들었더니
    나중에 UserDetails에서 같은 단어를 쓰게 되어 혼동도 오고 import가 동시에 안되서 불편했음.
@Getter
@NoArgsConstructor
@Entity
public class RefreshToken {
    @Id
    @Column(name = "rt_key")
    private String key;

    @Column(name = "rt_value")
    private String value;

    @Builder
    public RefreshToken(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public RefreshToken updateValue(String token) {
        this.value = token;
        return this;
    }

Controller

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
@PostMapping("/login")
public ResponseEntity login(@RequestBody UserLoginRequestDto userLoginRequestDto, HttpServletResponse response) {

    String username = userLoginRequestDto.getUsername();
    String password = userLoginRequestDto.getPassword();
    TokenDto token = authService.login(username,password);
    response.setHeader("refresh-token", token.getRefreshToken());
    response.setHeader("access-token", token.getAccessToken());
    return ResponseEntity.ok("로그인 완료!");
}
@PostMapping("/reissue")
public ResponseEntity<String> reissue(HttpServletRequest request,HttpServletResponse response) {
    String refreshToken = request.getHeader("refresh-token");
    String accessToken = request.getHeader("access-token");
    TokenDto token = authService.reissue(refreshToken,accessToken);
    response.setHeader("access-token", token.getAccessToken());
    response.setHeader("refresh-token", token.getRefreshToken());
    return ResponseEntity.ok("재발급완료");
}

이어서 다음에는 서비스쪽과 나머지 구현을 완료해 오도록 해보겠다.

profile
하이요

0개의 댓글