개발 환경
spring boot 3.0.2
spring security : 6.0.1
Gradle
Spring Security를 통한 JWT 적용
JWT를 사용하기 위해 구현해야 할 것은 기본적으로 TokenProvider, TokenAuthenticationFilter이다.
- JWT 토큰 제공을 위한 TokenProvider
- HTTP Request에서 토큰을 읽어 들여 정상 토큰이면 Security Context에 저장하는 TokenAuthenticationFilter
그리고 Spring Security에 적용하기 위해 구현해야 할 것은 기본적으로 아래와 같다.
- 기본적으로 Spring Security 설정을 위한 SecurityConfig
TokenProvider
@RequiredArgsConstructor
@Service
public class JwtTokenProvider {
private final JwtProperties jwtProperties;
private final UserDetailService userDetailService;
private final RedisTemplate redisTemplate;
private final static String TOKEN_PREFIX = "Bearer ";
public String generateToken(User user, Duration expiredAt){
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()),user);
}
private String makeToken(Date expiry, User user){
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiry)
.setSubject(user.getEmail())
.claim("id",user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
public boolean validToken(String token){
try{
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token);
return true;
}
catch (Exception e){
return false;
}
}
//토큰 기반으로 인증 정보를 가져오는 메서드
public Authentication getAuthentication(String token) {
//비밀값으로 토큰을 복호화한 뒤 클레임에 있는 사용자 이메일인 token subject를 가져온다
Claims claims = getClaims(token);
UserDetails userDetails = userDetailService.loadUserByUsername(claims.getSubject());
return new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities());
}
public Date getExpiration(String token){
Claims claims = getClaims(token);
return claims.getExpiration();
}
private Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
public String getToken(String authorizationHeader){
//JWT가 Bearer 시작한다면 Bearer 빼고 return
if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)){
return authorizationHeader.substring(TOKEN_PREFIX.length());
}
return null;
}
public void removeTokenFromRedis(String token){
if (redisTemplate.opsForValue().get(token) != null){
// Refresh Token을 삭제
redisTemplate.delete(token);
}else{
throw new IllegalArgumentException("refresh key에 대한 값이 없습니다.");
}
}
public Long getUserIdFromRedis(String token){
Optional<Long> optionalValue = Optional.ofNullable(Long.valueOf((String) redisTemplate.opsForValue().get(token)));
return optionalValue.orElseThrow(() -> new IllegalArgumentException("refresh key에 대한 값이 없습니다."));
}
TokenAuthenticationFilter
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate redisTemplate;
//요청이 오면 헤더값을 비교해서 토큰유무 및 유효성 체크
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//요청 헤더의 Authorization 키의 값 조회
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
String token = jwtTokenProvider.getToken(authorizationHeader);
//토큰 유효성 검사
if(StringUtils.hasText(token) && jwtTokenProvider.validToken(token)){
//Redis에 해당 accessToken logout 여부 확인(블랙 리스트)
String isLogout = (String) redisTemplate.opsForValue().get(token);
//로그아웃 하지 않은 경우
if(ObjectUtils.isEmpty(isLogout)){
//토큰으로부터 유저 정보 가져오기
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// SecurityContext 에 Authentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
요청이 들어오면 SecurityConfig 클래스의 filterChain에 설정한
http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)으로 인해
UsernamePasswordAuthenticationFilter 전에 TokenAuthenticationFilter를 거치게 된다.
이때 토큰 유효성 검사(정상 토큰)를 통해 Security Context에 저장한다.
SecurityConfig
- 스프링 시큐리티 5.7.0 이상일 때 고려할 점
- 기존에는 하나의 SecurityFilterChain을 구성하기 위해서 WebSecurityConfigurerAdapter를 상속한 클래스에서 config 메소드를 오버라이딩한다. 하지만 스프링 시큐리티 5.7.0 버전부터 WebSecurityConfigurerAdapter를 사용하지 않는다고 되어있다.
참고 링크 : https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
그럼 어떻게 시큐리티 설정을 할까?
이전 : WebSecurityConfigurerAdapter를 상속받아 설정을 오버라이딩 하는 방식
이후 : 모두 Bean으로 등록하는 방식
중요 코드
- TokenAuthenticationFilter 빈 등록
- formLogin과 logout 비활성화해주기
- UsernamePasswordAuthenticationFilter 이전에 TokenAuthenticationFilter 추가
- AccessToken 만료되었을 때 /token 엔드포인트를 통해 재발급해야하기 때문에 permitAll() 설정
@EnableWebSecurity
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {
private final TokenProvider tokenProvider;
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers("/img/**", "/css/**", "/js/**");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//formLogin과 logout 비활성화해주기
http.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable();
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//UsernamePasswordAuthenticationFilter 이전에 TokenAuthenticationFilter 추가
http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests()
//AccessToken 만료되었을 때 /token 엔드포인트를 통해 재발급 필요
.requestMatchers("/token","/login","/signup").permitAll()
.requestMatchers("/**").authenticated()
.anyRequest().permitAll();
http.exceptionHandling()
.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new AntPathRequestMatcher("/**"));
return http.build();
}
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(jwtTokenProvider,redisTemplate);
}
}
다음 포스팅
- 로그인 적용
'Backend > JWT' 카테고리의 다른 글
[JWT] JWT 인증/인가 적용기(4) - AccessToken이 만료된 경우 (0) | 2024.02.28 |
---|---|
[JWT] JWT는 어디에 담아 클라이언트와 요청/응답할까 (0) | 2024.02.27 |
[JWT] JWT 인증/인가 적용기(3) - 로그인 (2) | 2024.02.27 |
[JWT] JWT 인증/인가 구현기(1) - Spring Security 인증 구조 (0) | 2024.01.27 |
[JWT] Access Token과 Refresh Token이란? (0) | 2024.01.27 |