Skip to content

Commit

Permalink
refactor: JWT 에러 응답 리팩토링 및 인가 인증 예최 처리 코드 수정 (#103)
Browse files Browse the repository at this point in the history
* refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101)

* chore: JWT 관련 의존성 변경 (#101)

* refactor: JWT 및 인증 관련 로직 리팩토링 (#101)

* test: 테스트 코드 및 설정 관련 변경(#101)

* refactor: ObjectMapper Autowired 로 주입 (#101)

* refactor: AuthService 반환 타입 Optional<User> -> User 변경 (#101)
  • Loading branch information
choidongkuen authored Jan 17, 2024
1 parent 9e59ac8 commit 7470f70
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 75 deletions.
4 changes: 1 addition & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,4 @@ sentryVersion=4.1.1
### AWS-CLOUD ###
springCloudAwsVersion=3.1.0
### JWT ###
jwtVersion=0.9.1
jwtBindingVersion=4.0.1
jwtJaxbApiVersion=2.3.1
jwtVersion=0.11.5
10 changes: 3 additions & 7 deletions gradle/devtool.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ allprojects {
compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok"

implementation "io.jsonwebtoken:jjwt:${jwtVersion}"

// com.sun.xml.bind
implementation "com.sun.xml.bind:jaxb-impl:${jwtBindingVersion}"
implementation "com.sun.xml.bind:jaxb-core:${jwtBindingVersion}"
// javax.xml.bind
implementation "javax.xml.bind:jaxb-api:${jwtJaxbApiVersion}"
implementation "io.jsonwebtoken:jjwt-api:${jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}"

testCompileOnly "org.projectlombok:lombok:${lombokVersion}"
testAnnotationProcessor "org.projectlombok:lombok"
Expand Down
10 changes: 4 additions & 6 deletions src/main/java/net/teumteum/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package net.teumteum.auth.service;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.auth.domain.response.TokenResponse;
Expand All @@ -25,16 +24,15 @@ public TokenResponse reissue(HttpServletRequest request) {
String accessToken = jwtService.extractAccessToken(request);

checkRefreshTokenValidation(refreshToken);

User user = findUserByAccessToken(accessToken).orElseThrow(
() -> new IllegalArgumentException("access token 에 해당하는 user를 찾을 수 없습니다."));
User user = findUserByAccessToken(accessToken);

checkRefreshTokenMatch(user, refreshToken);
return issueNewToken(user);
}

public Optional<User> findUserByAccessToken(String accessToken) {
return userConnector.findUserById(Long.parseLong(jwtService.getUserIdFromToken(accessToken)));
public User findUserByAccessToken(String accessToken) {
return userConnector.findUserById(jwtService.getUserIdFromToken(accessToken))
.orElseThrow(() -> new IllegalArgumentException("access token 에 해당하는 user를 찾을 수 없습니다."));
}

private void checkRefreshTokenValidation(String refreshToken) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/net/teumteum/auth/service/OAuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public TokenResponse oAuthLogin(String registrationId, String code) {
registrationId);
Authenticated authenticated = getAuthenticated(clientRegistration.getRegistrationId());
OAuthUserInfo oAuthUserInfo = getOAuthUserInfo(clientRegistration, authenticated, code);
return checkUserAndMakeResponse(oAuthUserInfo, authenticated);
return makeResponse(oAuthUserInfo, authenticated);
}

private Authenticated getAuthenticated(String registrationId) {
Expand All @@ -65,7 +65,7 @@ private OAuthUserInfo getOAuthUserInfo(ClientRegistration clientRegistration, Au
return new KakaoOAuthUserInfo(oAuthAttribute);
}

private TokenResponse checkUserAndMakeResponse(OAuthUserInfo oAuthUserInfo, Authenticated authenticated) {
private TokenResponse makeResponse(OAuthUserInfo oAuthUserInfo, Authenticated authenticated) {
String oauthId = oAuthUserInfo.getOAuthId();

return getUser(oauthId, authenticated)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
package net.teumteum.core.security.filter;

import jakarta.servlet.ServletException;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.core.error.ErrorResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

private final ObjectMapper objectMapper;

@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException, ServletException {
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException {
this.sendUnAuthorizedError(response, accessDeniedException);
}

private void sendUnAuthorizedError(HttpServletResponse response,
Exception exception) throws IOException {
Exception exception) throws IOException {
response.setStatus(SC_FORBIDDEN);
OutputStream os = response.getOutputStream();
log.error("Responding with unauthorized error. Message - {}", exception.getMessage());
response.sendError(SC_FORBIDDEN, exception.getMessage());
objectMapper.writeValue(os, ErrorResponse.of("인가 과정에서 오류가 발생했습니다."));
os.flush();
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package net.teumteum.core.security.filter;

import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.core.error.ErrorResponse;
import org.springframework.security.core.AuthenticationException;
Expand All @@ -13,22 +16,25 @@

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private static final String ATTRIBUTE_NAME = "exception";

private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException
) throws IOException {
this.sendUnAuthenticatedError(response, authenticationException);
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
this.sendUnAuthenticatedError(request, response, authenticationException);
}

private void sendUnAuthenticatedError(HttpServletResponse response,
Exception exception) throws IOException {
private void sendUnAuthenticatedError(HttpServletRequest request, HttpServletResponse response, Exception exception)
throws IOException {
response.setStatus(SC_UNAUTHORIZED);
OutputStream os = response.getOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
log.error("Responding with unauthenticated error. Message - {}", exception.getMessage());
objectMapper.writeValue(os, ErrorResponse.of("인증 과정에서 오류가 발생했습니다."));
objectMapper.writeValue(os, ErrorResponse.of((String) request.getAttribute(ATTRIBUTE_NAME)));
os.flush();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import net.teumteum.user.domain.User;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
Expand All @@ -25,6 +24,8 @@
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final String ATTRIBUTE_NAME = "exception";

private final JwtService jwtService;
private final AuthService authService;
private final JwtProperty jwtProperty;
Expand All @@ -39,7 +40,7 @@ protected void doFilterInternal(HttpServletRequest request,

try {
String token = this.resolveTokenFromRequest(request);
if (checkTokenExistenceAndValidation(token)) {
if (checkTokenExistenceAndValidation(request, token)) {
User user = getUser(token);
saveUserAuthentication(user);
}
Expand All @@ -50,12 +51,19 @@ protected void doFilterInternal(HttpServletRequest request,
}

private User getUser(String token) {
return this.authService.findUserByAccessToken(token)
.orElseThrow(() -> new UsernameNotFoundException("일치하는 회원 정보가 존재하지 않습니다."));
return this.authService.findUserByAccessToken(token);
}

private boolean checkTokenExistenceAndValidation(String token) {
return StringUtils.hasText(token) && this.jwtService.validateToken(token);
private boolean checkTokenExistenceAndValidation(HttpServletRequest request, String token) {
if (!StringUtils.hasText(token)) {
setRequestAttribute(request, "요청에 대한 JWT 정보가 존재하지 않습니다.");
return false;
}
if (!jwtService.validateToken(token)) {
setRequestAttribute(request, "요청에 대한 JWT 가 유효하지 않습니다.");
return false;
}
return true;
}

private void saveUserAuthentication(User user) {
Expand All @@ -66,8 +74,13 @@ private void saveUserAuthentication(User user) {
private String resolveTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(jwtProperty.getAccess().getHeader());
if (!ObjectUtils.isEmpty(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) {
return token.substring(jwtProperty.getBearer().length()).trim();
return token.substring(7);
}
setRequestAttribute(request, "요청에 대한 JWT 파싱 과정에서 문제가 발생했습니다.");
return null;
}

private void setRequestAttribute(HttpServletRequest request, String name) {
request.setAttribute(ATTRIBUTE_NAME, name);
}
}
67 changes: 43 additions & 24 deletions src/main/java/net/teumteum/core/security/service/JwtService.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package net.teumteum.core.security.service;

import static io.jsonwebtoken.SignatureAlgorithm.HS512;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Date;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.auth.domain.response.TokenResponse;
import net.teumteum.core.property.JwtProperty;
import net.teumteum.user.domain.User;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
Expand All @@ -22,10 +26,20 @@
@Slf4j
@Service
@RequiredArgsConstructor
public class JwtService {
public class JwtService implements InitializingBean {

private static final String TOKEN_SUBJECT = "ACCESSTOKEN";

private final JwtProperty jwtProperty;
private final RedisService redisService;
private Key key;

@Override
public void afterPropertiesSet() {
byte[] secretKey = Decoders.BASE64.decode(jwtProperty.getSecret());
key = Keys.hmacShaKeyFor(secretKey);
}


public String extractAccessToken(HttpServletRequest request) {
String accessToken = request.getHeader(jwtProperty.getAccess().getHeader());
Expand All @@ -44,17 +58,16 @@ public String extractRefreshToken(HttpServletRequest request) {
return null;
}

public String getUserIdFromToken(String token) {
public Long getUserIdFromToken(String token) {
try {
return Jwts.parser().setSigningKey(jwtProperty.getSecret().getBytes())
.parseClaimsJws(token).getBody().getSubject();
return Long.valueOf(getClaims(token).get("id", String.class));
} catch (Exception exception) {
throw new JwtException("Access Token is not valid");
}
}

public TokenResponse createServiceToken(User users) {
String accessToken = createAccessToken(String.valueOf(users.getId()));
String accessToken = createAccessToken(users.getId().toString());
String refreshToken = createRefreshToken();

this.redisService.setDataWithExpiration(String.valueOf(users.getId()), refreshToken,
Expand All @@ -63,38 +76,44 @@ public TokenResponse createServiceToken(User users) {
return new TokenResponse(jwtProperty.getBearer() + " " + accessToken, refreshToken);
}

public String createAccessToken(String payload) {
return this.createToken(payload, jwtProperty.getAccess().getExpiration());
public String createAccessToken(String userId) {
return this.createToken(userId, jwtProperty.getAccess().getExpiration());
}

public String createRefreshToken() {
return this.createToken(UUID.randomUUID().toString(), jwtProperty.getRefresh().getExpiration());
}

private String createToken(String payload, Long tokenExpiration) {
Claims claims = Jwts.claims().setSubject(payload);
Date tokenExpiresIn = new Date(new Date().getTime() + tokenExpiration);
private String createToken(String payLoad, Long tokenExpiration) {

Date tokenExpiresIn = new Date(new Date().getTime() + tokenExpiration);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setSubject(TOKEN_SUBJECT)
.claim("id", payLoad)
.signWith(key, HS512)
.setExpiration(tokenExpiresIn)
.signWith(SignatureAlgorithm.HS512, jwtProperty.getSecret().getBytes())
.compact();
}

public boolean validateToken(String token) {
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(jwtProperty.getSecret().getBytes())
.parseClaimsJws(token);
return !claimsJws.getBody().getExpiration().before(new Date());
} catch (ExpiredJwtException exception) {
log.warn("만료된 jwt 입니다.");
} catch (UnsupportedJwtException exception) {
log.warn("지원되지 않는 jwt 입니다.");
} catch (IllegalArgumentException exception) {
log.warn("jwt 에 오류가 존재합니다.");
Claims claims = getClaims(token);
return !claims.getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
log.error("JWT 가 만료되었습니다.");
} catch (UnsupportedJwtException e) {
log.error("지원되지 않는 JWT 입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT 가 잘못되었습니다.");
}
return false;
}

private Claims getClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
3 changes: 1 addition & 2 deletions src/test/java/net/teumteum/core/property/PropertyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,10 @@ class Read_jwt_value_from_application_yml {
void Make_jwt_property_from_application_yml() {
// given
String expectedBearer = "Bearer";
String expectedSecret = "secret";

// when & then
Assertions.assertEquals(expectedBearer, jwtProperty.getBearer());
Assertions.assertEquals(expectedSecret, jwtProperty.getSecret());
Assertions.assertNotNull(jwtProperty.getSecret());
}
}
}
Loading

0 comments on commit 7470f70

Please sign in to comment.