Skip to content

Commit

Permalink
feat: JWT filter
Browse files Browse the repository at this point in the history
  • Loading branch information
devmizz committed May 31, 2024
1 parent 340a5c2 commit 96a7da7
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.example.security.dto;

import java.util.UUID;
import lombok.Builder;
import org.example.vo.UserRoleApiType;

@Builder
public record AuthenticatedUser(
UUID userId,
UserRoleApiType role
Expand Down
77 changes: 77 additions & 0 deletions app/api/src/main/java/org/example/security/token/JWTFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.example.security.token;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import exception.BusinessException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.example.security.dto.AuthenticatedUser;
import org.example.security.dto.TokenParam;
import org.example.security.vo.TokenError;
import org.example.vo.UserRoleApiType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@RequiredArgsConstructor
@Component
public class JWTFilter extends OncePerRequestFilter {

private final JWTProcessor jwtProcessor;
private final RefreshTokenProcessor refreshTokenProcessor;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if (request.getHeader("Refresh") != null) {
TokenParam token = refreshTokenProcessor.process(request, response);
response.getWriter().write(new ObjectMapper().writeValueAsString(token));
return;
}

if (request.getHeader("Authorization") != null) {
handleAccessToken(request);
}

filterChain.doFilter(request, response);
}

private void handleAccessToken(HttpServletRequest request) {
String accessToken = jwtProcessor.extractAccessToken(request);
DecodedJWT decodedJWT = jwtProcessor.decodeToken(accessToken);
saveOnSecurityContextHolder(decodedJWT);
}

private void saveOnSecurityContextHolder(DecodedJWT decodedJWT) {
Map<String, Object> claims = decodedJWT.getClaim("claim").asMap();

if (!claims.containsKey("userId") || !claims.containsKey("role")) {
throw new BusinessException(TokenError.INVALID_CLAIM);
}

AuthenticatedUser authenticatedUser = AuthenticatedUser.builder()
.userId(UUID.fromString(claims.get("userId").toString()))
.role(UserRoleApiType.valueOf(claims.get("role").toString()))
.build();

SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
authenticatedUser,
null,
List.of(new SimpleGrantedAuthority(authenticatedUser.role().getAuthority()))
)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.example.security;
package org.example.security.token;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
Expand Down
51 changes: 51 additions & 0 deletions app/api/src/main/java/org/example/security/token/JWTProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.example.security.token;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import exception.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.example.property.TokenProperty;
import org.example.security.vo.TokenError;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JWTProcessor {

private final TokenProperty tokenProperty;

public String extractAccessToken(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");

if (!authorization.startsWith("Bearer ")) {
throw new BusinessException(TokenError.WRONG_HEADER);
}

return authorization.replace("Bearer ", "");
}

public String extractRefreshToken(HttpServletRequest request) {
String refresh = request.getHeader("Refresh");

if (refresh == null) {
throw new BusinessException(TokenError.WRONG_HEADER);
}

return refresh;
}

public DecodedJWT decodeToken(String token) {
try {
return JWT.require(Algorithm.HMAC512(tokenProperty.secretKey()))
.build()
.verify(token);
} catch (TokenExpiredException e) {
throw new BusinessException(TokenError.EXPIRED_TOKEN);
} catch (Exception e) {
throw new BusinessException(TokenError.INVALID_TOKEN);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.example.security.token;

import com.auth0.jwt.interfaces.DecodedJWT;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.example.security.dto.TokenParam;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class RefreshTokenProcessor {

private final JWTProcessor jwtProcessor;
private final JWTGenerator jwtGenerator;

public TokenParam process(HttpServletRequest request, HttpServletResponse response) {
String accessToken = jwtProcessor.extractAccessToken(request);
String refreshToken = jwtProcessor.extractRefreshToken(request);

return new TokenParam(accessToken, refreshToken);
}
}
94 changes: 94 additions & 0 deletions app/api/src/main/java/org/example/security/vo/TokenError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.example.security.vo;

import exception.BusinessError;

public enum TokenError implements BusinessError {

WRONG_HEADER {
@Override
public int getHttpStatus() {
return 401;
}

@Override
public String getErrorCode() {
return "TKN-001";
}

@Override
public String getClientMessage() {
return "유효하지 않은 토큰입니다.";
}

@Override
public String getLogMessage() {
return "요청 헤더가 잘못 처리되었습니다.";
}
},

EXPIRED_TOKEN {
@Override
public int getHttpStatus() {
return 401;
}

@Override
public String getErrorCode() {
return "TKN-002";
}

@Override
public String getClientMessage() {
return "토큰이 만료되었습니다.";
}

@Override
public String getLogMessage() {
return "토큰이 만료되었습니다.";
}
},

INVALID_TOKEN {
@Override
public int getHttpStatus() {
return 401;
}

@Override
public String getErrorCode() {
return "TKN-003";
}

@Override
public String getClientMessage() {
return "유효하지 않은 토큰입니다.";
}

@Override
public String getLogMessage() {
return "만료 이외의 토큰 오류가 발생했습니다.";
}
},

INVALID_CLAIM {
@Override
public int getHttpStatus() {
return 401;
}

@Override
public String getErrorCode() {
return "TKN-003";
}

@Override
public String getClientMessage() {
return "유효하지 않은 토큰입니다.";
}

@Override
public String getLogMessage() {
return "토큰 claim 구성에 오류가 있습니다.";
}
}
}
13 changes: 12 additions & 1 deletion app/api/src/main/java/org/example/vo/UserRoleApiType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
package org.example.vo;

import lombok.Getter;

@Getter
public enum UserRoleApiType {
GUEST, USER, ADMIN
GUEST("ROLE_GUEST"),
USER("ROLE_USER"),
ADMIN("ROLE_ADMIN");

final String authority;

UserRoleApiType(String authority) {
this.authority = authority;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.example.property.TokenProperty;
import org.example.security.dto.TokenParam;
import org.example.security.dto.UserParam;
import org.example.security.token.JWTGenerator;
import org.example.vo.UserRoleApiType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
Expand Down

0 comments on commit 96a7da7

Please sign in to comment.