
@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();
}
}
@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();
}
}
@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;
}
}
@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;
}
@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("재발급완료");
}