Skip to content

Commit

Permalink
Merge pull request #18 from KUIT-Couphone/feature/jwtFilter
Browse files Browse the repository at this point in the history
feat: JWT Authentication 로직 개선 및 예외처리 보완
  • Loading branch information
akfrdma0125 authored Aug 5, 2023
2 parents b77eb46 + caa046c commit 08e30e0
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.couphoneserver.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response); // go to JwtAuthenticationFilter
} catch (Exception e) {
setErrorResponse(request, response, e);
}
}

public void setErrorResponse(HttpServletRequest request, HttpServletResponse response, Exception e) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
final Map<String, Object> body = new HashMap<>();

body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", e.getMessage());
body.put("path", request.getServletPath());

final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
response.setStatus(HttpServletResponse.SC_OK);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.couphoneserver.config;

import com.example.couphoneserver.service.MemberDetailService;
import com.example.couphoneserver.service.RefreshTokenService;
import com.example.couphoneserver.utils.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
Expand All @@ -21,6 +22,7 @@
public class SecurityConfig {
private final MemberDetailService memberDetailService;
private final JwtTokenProvider jwtProvider;
private final RefreshTokenService refreshTokenService;

@Bean
public WebSecurityCustomizer configure() {
Expand All @@ -33,12 +35,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(FormLoginConfigurer::disable)

.authorizeHttpRequests((auth) -> auth.requestMatchers("/auth/login").permitAll())
.authorizeHttpRequests((auth) -> auth.requestMatchers("/admin/**").hasRole("ADMIN"))
.authorizeHttpRequests((auth) -> auth.anyRequest().permitAll())
.sessionManagement(httpSecuritySessionManagementConfigurer ->
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(new TokenAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(new TokenAuthenticationFilter(jwtProvider, refreshTokenService), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtExceptionFilter(), TokenAuthenticationFilter.class);
return http.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.example.couphoneserver.config;

import com.example.couphoneserver.common.exception.jwt.bad_request.JwtNoTokenException;
import com.example.couphoneserver.common.exception.jwt.unauthorized.JwtExpiredTokenException;
import com.example.couphoneserver.service.RefreshTokenService;
import com.example.couphoneserver.utils.jwt.JwtCode;
import com.example.couphoneserver.utils.jwt.JwtTokenProvider;
import jakarta.servlet.FilterChain;
Expand All @@ -16,45 +19,61 @@

import java.io.IOException;

import static com.example.couphoneserver.common.response.status.BaseExceptionResponseStatus.EXPIRED_TOKEN;
import static com.example.couphoneserver.common.response.status.BaseExceptionResponseStatus.TOKEN_NOT_FOUND;

@Slf4j
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtProvider;
private final RefreshTokenService tokenService;
private final static String HEADER_AUTHORIZATION = "Authorization";
private final static String TOKEN_PREFIX = "Bearer ";

/**
* 1. request header Authorization 내 Token 유효성 검사
* 2. 유효하면 인증 정보를 security context 에 저장
* 3. Token 기간 만료 시 userId 를 header 에서 가져옴
* 4. userId 로 refresh token 을 조회한 뒤 유효성 검사이후 유효기간 전이라면 access 재발급
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String accessToken = resolveToken(request, HEADER_AUTHORIZATION);
if (permitAllUrl.of(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
String accessToken = resolveToken(request, HEADER_AUTHORIZATION); // 1. 사용자가 보낸 토큰을 확인

if (StringUtils.hasText(accessToken) && jwtProvider.validateToke(accessToken) == JwtCode.ACCESS) {
// Access token 이 유효하면
Authentication authentication = jwtProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // security context 에 인증 정보 저장
log.info("Access Token 은 아직 유효합니다.");
} else if (StringUtils.hasText(accessToken) && jwtProvider.validateToke(accessToken) == JwtCode.EXPIRED) {
// 재발급해야함
// 2-1. Access token 이 만료되었다면
log.info("Authorization 필드에 담겨진 Access Token 이 Expired 되었습니다!");
String refreshToken = null;

if (StringUtils.hasText(request.getHeader("Auth"))) { // Auth 에는 userId가 담겨서 와야함!
Long userId = Long.parseLong(request.getHeader("Auth"));
refreshToken = jwtProvider.getRefreshToken(userId); // userId로 refreshToken 조회
}
// refresh token 검증
// 2-2. jwt token 으로부터 userId 를 찾고, 해당 userId 에 대한 refreshToken 을 탐색
Long userId = jwtProvider.getUserIdFromExpiredToken(accessToken);
log.warn(userId.toString());
refreshToken = jwtProvider.getRefreshToken(userId);

// refresh token 이 존재하고 유효하다면
if (StringUtils.hasText(refreshToken) && jwtProvider.validateToke(refreshToken) == JwtCode.ACCESS) {
// access token 재발급
log.info("해당 회원 ID 에 대한 Refresh token 이 존재하고 유효합니다.");
log.info("Access token 을 재발급하여 반환합니다.");

Authentication authentication = jwtProvider.getAuthentication(refreshToken);

String newAccessToken = jwtProvider.generateAccessToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);

response.setHeader(HttpHeaders.AUTHORIZATION, newAccessToken);
log.info("Access token 을 재발급합니다.");
} else if (StringUtils.hasText(refreshToken) && jwtProvider.validateToke(refreshToken) == JwtCode.EXPIRED) {
// refresh token 이 존재하지만 만료되었다면
log.warn("해당 회원 ID 에 대한 Refresh token 이 존재하지만, 만료되었습니다.");
throw new JwtExpiredTokenException(EXPIRED_TOKEN);
}
if (refreshToken == null) {
// refresh token 이 존재하지 않으면
log.warn("해당 회원 ID 에 대한 Refresh token 이 존재하지 않습니다.");
throw new JwtNoTokenException(TOKEN_NOT_FOUND);
}
}
filterChain.doFilter(request, response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtAuthenticationInterceptor)
.order(1)
.addPathPatterns("/auth", "/brands");
.addPathPatterns("/auth", "/brands", "/users")
.excludePathPatterns("/auth/login");
}

@Override
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/example/couphoneserver/config/permitAllUrl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.couphoneserver.config;

import java.util.HashSet;
import java.util.Set;

public class permitAllUrl {
private static final Set<String> urlSet;

static {
urlSet = new HashSet<>();
urlSet.add("/auth/login");
}

public static boolean of(String url) {
return urlSet.contains(url);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.example.couphoneserver.utils.jwt;

import com.example.couphoneserver.common.exception.MemberException;
import com.example.couphoneserver.common.exception.jwt.bad_request.JwtBadRequestException;
import com.example.couphoneserver.common.exception.jwt.bad_request.JwtNoTokenException;
import com.example.couphoneserver.common.exception.jwt.bad_request.JwtUnsupportedTokenException;
import com.example.couphoneserver.common.exception.jwt.unauthorized.JwtExpiredTokenException;
import com.example.couphoneserver.common.exception.jwt.unauthorized.JwtInvalidTokenException;
import com.example.couphoneserver.common.exception.jwt.unauthorized.JwtMalformedTokenException;
import com.example.couphoneserver.domain.entity.RefreshToken;
Expand Down Expand Up @@ -160,10 +162,39 @@ public Long getUserId(String token) {
}

private Claims getClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
} catch (SecurityException e) {
throw new JwtInvalidTokenException(INVALID_TOKEN);
} catch (MalformedJwtException e) {
throw new JwtMalformedTokenException(INVALID_TOKEN);
} catch (ExpiredJwtException e) {
throw new JwtExpiredTokenException(EXPIRED_TOKEN);
} catch (UnsupportedJwtException e) {
throw new JwtUnsupportedTokenException(UNSUPPORTED_TOKEN_TYPE);
} catch (IllegalArgumentException e) {
throw new JwtBadRequestException(JWT_ERROR);
}
}
public Long getUserIdFromExpiredToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.get("userId", String.class));
} catch (ExpiredJwtException e) {
// 만료된 토큰에서 사용자 ID를 추출
// access token 이 만료되었지만 refresh token 이 존재하는 경우
// JwtExceptionFilter 에 의해 ExpiredException 이 처리되기 전에 userId 를 반환해야 할 때에만 사용
Claims expiredClaims = e.getClaims();
return Long.parseLong(expiredClaims.get("userId", String.class));
}
}

}

0 comments on commit 08e30e0

Please sign in to comment.