모든 http 요청에 대해 해당 설정 적용
- 예외처리 부분
- accessDeniedHandler : 403 오류 핸들러
- authenticationEntryPoint : 401 오류 핸들러
- JWT 인증필터 적용
- JwtProvider 주입
UsernamePasswordAuthenticationFilter 전에 해당 필터 적용
- 인증 절차
- AUTH_WHITE_LIST의 url들은 permitAll
: https://velog.io/@choidongkuen/Spring-Security-SecurityConfig-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-permitAll-%EC%9D%B4-%EC%A0%81%EC%9A%A9%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%98%EB%8D%98-%EC%9D%B4%EC%9C%A0#2-permitall-%EC%97%90-%EB%8C%80%ED%95%9C-%EB%82%98%EC%9D%98-%EC%9E%98%EB%AA%BB%EB%90%9C-%EC%9D%B4%ED%95%B4
: permitAll을 해도 filterChain을 거침!!
: 따라서 JwtFilter에서 오류가 발생해도 다음 필터로 넘길 수 있도록 try - catch를 사용해야 함- 그 외 요청들은 authenticated
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private static final String SUCCESS = "success";
private static final String EXPIRED = "expired";
private static final String DENIED = "denied";
private final JwtProvider jwtProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response
, FilterChain filterChain) throws ServletException, IOException {
try {
String accessToken = jwtProvider.resolveToken(request, HttpHeaders.AUTHORIZATION);
Authentication authentication = jwtProvider.getAuthentication(accessToken);
//String loginId = authentication.getName();
//String refreshToken = jwtProvider.getRefreshTokenById(loginId);
// access token 검증
if (StringUtils.hasText(accessToken) && jwtProvider.validateToken(accessToken) == SUCCESS) {
SecurityContextHolder.getContext().setAuthentication(authentication); // security context에 인증 정보 저장
}
/*else if (StringUtils.hasText(accessToken) && jwtProvider.validateToken(accessToken) == EXPIRED) {
System.out.println("Access token has expired");
// refresh token 검증
if (StringUtils.hasText(refreshToken) && jwtProvider.validateToken(refreshToken) == SUCCESS) {
System.out.println("getting new access token");
// access token 재발급
String newAccessToken = jwtProvider.generateAccessToken(authentication);
System.out.println("Reissue access token success");
response.setHeader(HttpHeaders.AUTHORIZATION, newAccessToken);
} else { //refresh token 만료
System.out.println("Reissue refresh token");
jwtProvider.deleteRefreshToken(loginId);
String newAccessToken = jwtProvider.generateAccessToken(authentication);
String newRefreshToken = jwtProvider.regenerateRefreshToken(authentication);
response.setHeader(HttpHeaders.AUTHORIZATION, newAccessToken);
}
}
*/
} catch (ExpiredJwtException e) {
request.setAttribute("exception",EXPIRED);
} catch (IllegalArgumentException e) {
request.setAttribute("exception",DENIED);
}
filterChain.doFilter(request, response);
}
}
길지만 결국 주석부분 빼면 간단함
- resolveToken : request의 헤더에서 accessToken 빼냄
- getAuthentication : accessToken으로부터 Authentication 얻어냄
- validateToken : accessToken 유효성 검사
- 유효한 경우, securityContext에 인증정보(Authentication) 저장
- 토큰오류를 분리해서 처리하기 위해 catch구문으로 분리
- setAttribute로 어떤 오류인지 기록 -> AuthenticationEntryPoint에서 처리
주석부분
: accessToken이 만료된 경우, refreshToken의 유효성 검사를 거쳐 accessToken을 재발급 / refreshToken + accessToken 재발급
-> header에 직접 넣어주기
과정을 백의 filter에서 직접 해주려 시도했으나, 실패
사실 그럴 필요도 없는 것 같음 ^-^
@PostConstruct
protected void init() {
secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(key));
//secretKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
now= new Date().getTime();
}
public String generateAccessToken(Authentication authentication) {
return generateToken(authentication, EXPIRE_TIME);
}
public String generateRefreshToken(Authentication authentication) {
String refreshToken = generateToken(authentication, REFRESH_EXPIRE_TIME);
return refreshToken;
}
public String generateToken(Authentication authentication, long expireTime) {
now= new Date().getTime();
Date expiration = new Date(now+expireTime);
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.setIssuedAt(new Date(now))
.setExpiration(expiration)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
//token으로부터 authentication을 얻는 것
public Authentication getAuthentication(String token) {
//name으로부터 userDetails 얻음
UserDetails userDetails = customUserDetailsService.loadUserByUsername(this.getAccount(token));
//Authentication의 구현체(Username..) 얻음
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
//token으로부터 name 알아냄
public String getAccount(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey).build()
.parseClaimsJws(token).getBody().getSubject();
}
//request에 헤더설정 해줘야 함
public String resolveToken(HttpServletRequest request, String header) {
String bearerToken = request.getHeader(header);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
public String validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return SUCCESS;
} catch (ExpiredJwtException e) { // 기한 만료
return EXPIRED;
} catch (Exception e) {
return DENIED;
}
}
public String getRefreshToken(Long userId) {
Member member = memberRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("user doesn't exist"));
return refreshTokenRepository.findByMember(member).getRefreshToken();
}
generateAccessToken, generateRefreshToken -> generateToken : 인증정보(authentication)기반으로 토큰 생성, 유효시간 차이두기 위해 분리
getAuthentication : token으로부터 authentication (UsernamePasswordAuthenticationToken : authentication의 구현체) 얻어냄
1. getAccount -> name 얻어냄 2. name으로부터 userDetails 얻음 3. User -> Authentication
getAccount : token으로부터 name 얻어냄 (getSubject)
resolveToken : request 헤더에서 토큰 뽑아냄
validateToken : token 유효성 검증 : parseClaim 과정에서 유효성검사 거침
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final String SUCCESS = "success";
private static final String EXPIRED = "expired";
private static final String DENIED = "denied";
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String exception = (String)request.getAttribute("exception");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json; charset=UTF-8");
//토큰 만료
if(exception.equals(EXPIRED)) {
setResponse(response,EXPIRED);
}
if(exception.equals(DENIED)) {
setResponse(response,DENIED);
}
}
public void setResponse(HttpServletResponse response,String msg) throws IOException{
ObjectNode json = new ObjectMapper().createObjectNode();
json.put("message", msg);
json.put("code", HttpStatus.UNAUTHORIZED.value());
String newResponse = new ObjectMapper().writeValueAsString(json);
response.getWriter().write(newResponse);
}
}
401 오류가 발생한 경우 어떤 오류인지 분류해서 Response로 보내기 위함
시큐리티 설정부분이 아닌 서비스 로직
public TokenResponseDTO signIn(SignUpDTO dto) {
Member member = memberRepository.findByLoginId(dto.getLoginId());
if (!(passwordEncoder.matches(dto.getPassword(), member.getPassword()))) {
throw new LogInFailure();
}
// user 검증
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(dto.getLoginId(), dto.getPassword());
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
// token 생성
String accessToken = jwtProvider.generateAccessToken(authentication);
String refreshToken = jwtProvider.generateRefreshToken(authentication);
User user = (User) authentication.getPrincipal(); // user 정보
String userName = user.getUsername();
RefreshToken generatedRefreshToken = RefreshToken.builder()
.refreshToken(refreshToken)
.member(memberRepository.findByLoginId(userName))
.build();
// refresh token 저장
if (!refreshTokenRepository.existsByMember(member)) {
refreshTokenRepository.save(generatedRefreshToken);
} else {
refreshTokenRepository.findByMember(member).updateRefreshToken(generatedRefreshToken.getRefreshToken());
}
return TokenResponseDTO.builder()
.loginId(dto.getLoginId())
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer ")
.build();
- AccessToken 재발급
1. filterChain 이용
- JwtExceptionFilter
- JwtFilter
JwtFilter 앞에 예외처리 전용 JwtExceptionFilter를 사용하려고 함
JwtFilter에서 catch로 분류에서 Exception을 던지면 -> JwtExceptionFilter에서 setResponse하는 방식
filte에서 직접 401Error에 Exception을 발생시켜버리면 위에서 언급했던 WHITE_LIST를 사용할 수가 없음
: permitAll을 적용해서 filter를 지나는건 똑같고, 여기서 Exception을 발생시켜버리기 때문
- filterChain에서 catch로 만료, Illegal Exception을 분류 -> request에 exception이라는 이름의 Attribute로 분류해 저장
- filterChain -> AuthenticationEntryPoint.commence 호출 (401 발생)
여기서 code와 msg 설정해서 Response 보냄