Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: JWT Authentication 로직 개선 및 예외처리 보완 #18

Merged
merged 3 commits into from
Aug 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
}

}