이번 포스팅에서는 SpringSecurity에 filter을 커스텀한 JWT 필터 적용한 과정을 정리해보자!
JWT 토큰으로 인증하고 SecurityContextHolder에 추가하는 필터를 설정하는 클래스이다.
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
log.info("인증필터 진입 doFilterInternal");
// 헤더에서 JWT 받기
String token = jwtTokenProvider.extractAccessToken(request).orElse(null);
// 유효한 토큰인지 확인
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효하면 토큰으로부터 유저 정보를 받기
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// SecurityContext 에 Authentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
기존 필터에서 가져올 수 없는 스프링의 설정 정보를 가져올 수 있게 확장된 추상 클래스다.
처음에는 GenreicFilterBean으로 상속받아 사용했었다. RequestDispatcher에 의해 다른 서블릿으로 디스패치되면서 필터가 두 번 실행되는 현상이 발생했다. 이 같은 문제를 해결하기 위해 OncePerRequestFilter가 있다
GenreicFilterBean을 상속받고 있지만 매 요청마다 한 번만 실행된다.
요청 당 한번의 실행을 보장하기 때문에 인증이나 인가를 한번만 거치고 다음 로직을 진행할 수 있다.
OncePerRequestFilter에서 구현하는 메서드이다
doFilter()는 다음 filterChain을 실행하는 것이며, 마지막 filter-chain인 경우 Dispatcher Servlet이 실행된다.
JWT 토큰과 관련된 메서드 모음 클래스
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
private final UserDetailsService userDetailsService;
@Value("${jwt.secretKey}")
private String secretKey;
@Value("${jwt.access.expiration}")
private Long accessTokenValidTime;
@Value("${jwt.refresh.expiration}")
private Long refreshTokenValidTime;
private final MemberRepository memberRepository;
private final RedisTemplate<String,String> redisTemplate;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String createAccessToken(String userPk) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위, 보통 여기서 user를 식별하는 값을 넣음
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + accessTokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
public String createRefreshToken(String id) {
Date now = new Date();
return Jwts.builder()
.setId(id) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + refreshTokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities());
}
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public String getUserId(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getId();
}
// Request의 Header에서 accesstoken 값을 가져옴. "Authorization" : "ACCESSTOKEN값'
public Optional<String> extractAccessToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader("Authorization"));
}
// Request의 Header에서 refreshtoken 값을 가져옴. "Authorization-Refresh" : "REFRESHTOKEN값'
public Optional<String> extractRefreshToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader("Authorization-Refresh"));
}
public void storeRefreshToken(String id, String refreshToken) {
Member member = memberRepository.findById(id).orElse(null);
if (member != null) {
redisTemplate.opsForValue().set(
id,
refreshToken,
refreshTokenValidTime,
TimeUnit.MILLISECONDS
);
}
}
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (SignatureException e) {
log.warn("JWT 서명이 유효하지 않습니다.");
throw new SignatureException("잘못된 JWT 시그니쳐");
} catch (MalformedJwtException e) {
log.warn("유효하지 않은 JWT 토큰입니다.");
throw new MalformedJwtException("유효하지 않은 JWT 토큰");
} catch (ExpiredJwtException e) {
log.warn("만료된 JWT 토큰입니다.");
throw new ExpiredJwtException(null,null,"토큰 기간 만료");
} catch (UnsupportedJwtException e) {
log.warn("지원되지 않는 JWT 토큰입니다.");
throw new UnsupportedJwtException("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.warn("JWT claims string is empty.");
} catch (NullPointerException e){
log.warn("JWT RefreshToken is empty");
} catch (Exception e) {
log.warn("잘못된 토큰입니다.");
}
return false;
}
엑세스 토큰을 만드는 메서드
public String createAccessToken(String userPk) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위, 보통 여기서 user를 식별하는 값을 넣음
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + accessTokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
리프레시 토큰을 만드는 메서드
public String createRefreshToken(String id) {
Date now = new Date();
return Jwts.builder()
.setId(id) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + refreshTokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
JWT 토큰에서 userpk를 가져오는 메서드
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
사용자 인증하는 메서드
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities());
}
엑세스 토큰을 헤더에서 꺼내오는 메서드
public Optional<String> extractAccessToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader("Authorization"));
}
토큰이 유용한지 검사하는 메서드
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (SignatureException e) {
log.warn("JWT 서명이 유효하지 않습니다.");
throw new SignatureException("잘못된 JWT 시그니쳐");
} catch (MalformedJwtException e) {
log.warn("유효하지 않은 JWT 토큰입니다.");
throw new MalformedJwtException("유효하지 않은 JWT 토큰");
} catch (ExpiredJwtException e) {
log.warn("만료된 JWT 토큰입니다.");
throw new ExpiredJwtException(null,null,"토큰 기간 만료");
} catch (UnsupportedJwtException e) {
log.warn("지원되지 않는 JWT 토큰입니다.");
throw new UnsupportedJwtException("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.warn("JWT claims string is empty.");
} catch (NullPointerException e){
log.warn("JWT RefreshToken is empty");
} catch (Exception e) {
log.warn("잘못된 토큰입니다.");
}
return false;
}
이제 doFilterInternal() 과정에 대해 정리해보자!