JWT๋
JWT(JSON Web Token)๋? | JWT ๊ตฌ์ฑ์์
ํ ํฐ์ด ํ์ํ ์ด์ ์ JWT ๋์ ๋ฐฉ์
์ธ์ฆ, ์ธ๊ฐ ํ๋ฆ(+ํด๋์ค ๋ค์ด์ด๊ทธ๋จ)
JwtProvider ๊ตฌํ
JwtProvider๋ Jwt๋ฅผ ์์ฑ, ํ์ฑ, ๊ฒ์ฆํ๋ ํด๋์ค๋ค.
๊ตฌํ์ jjwt ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ค.
jwt ๊ด๋ จ ์ ๋ณด์ธ secret, expire-length๋ฅผ application.yml
์ ๋ค์๊ณผ ๊ฐ์ด ์ค์ ํ๋ค.
jwt:
header: Authorization
secret: (ํน์ ๋ฌธ์์ด์ Base64๋ก ์ธ์ฝ๋ฉํ ๊ฐ)
access-token:
expire-length: 3600
refresh-token:
expire-length: 86400
private final Key key;
@Value("${jwt.access-token.expire-length}")
private long accessTokenExpireLengthInSeconds;
@Value("${jwt.refresh-token.expire-length}")
private long refreshTokenExpireLengthInSeconds;
public JwtProvider(
@Value("${jwt.secret}") String secret) {
byte[] keyBytes = Decoders.BASE64.decode(secret);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
Jwts.builder๋ก ํ ํฐ์ ์์ฑํ๊ณ Jwts.parseBuilder์ parseClaimJws๋ก claims๋ฅผ ํ์ฑํ ์ ์๋ค.
public Jwt createJwt(Map<String, Object> claims) {
String accessToken = createToken(claims, getExpireDateAccessToken());
String refreshToken = createToken(new HashMap<>(), getExpireDateRefreshToken());
return Jwt.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
public String createToken(Map<String, Object> claims, Date expireDate) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(expireDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public Claims getClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
Jwt ํด๋์ค๋ access token๊ณผ refresh token ํ๋๋ก ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
//Jwt.java
@Getter
public class Jwt {
private String accessToken;
private String refreshToken;
@Builder
public Jwt(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
๋ก๊ทธ์ธ ๊ตฌํ
๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๋ฉด ํ ํฐ์ ์๋ต์ผ๋ก ๋ฐํํฉ๋๋ค.
//UserController.java
@PostMapping("/users/sign-in")
public ResponseEntity<ApiResponse<Jwt>> signIn(@Valid @RequestBody UserLoginDto loginDto)
throws JsonProcessingException {
Jwt jwt = usersService.signIn(loginDto);
return ResponseEntity.ok(success(SuccessCode.GET_SUCCESS, jwt));
}
//UserService.java
@Transactional
public Jwt signIn(UserLoginDto loginDto) throws JsonProcessingException {
// db์ ์๋(ํ์๊ฐ์
ํ) ์ ์ ์ธ์ง ๊ฒ์ฆ
Users user = usersRepository.findByTel(loginDto.getTel())
.orElseThrow(() -> new IllegalArgumentException("ํ์๊ฐ์
ํ์ง ์์ ์ ์ ์
๋๋ค."));
// ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
if (!loginDto.getPassword().equals(user.getPassword())) {
throw new IllegalArgumentException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.");
}
// ํ ํฐ ์์ฑ
Jwt jwt = authService.createJwt(user);
// refreshToken ์ ์ฅ
user.updateRefreshToken(jwt.getRefreshToken());
return jwt;
}
//AuthService.java
public Jwt createJwt(Users user) throws JsonProcessingException {
// claims ์์ฑ
Map<String, Object> claims = new HashMap<>();
AuthenticateUser authenticateUser = new AuthenticateUser(user);
String authenticateUserJson = objectMapper.writeValueAsString(authenticateUser);
claims.put(AUTHENTICATE_USER, authenticateUserJson);
// ํ ํฐ ์์ฑ
return jwtProvider.createJwt(claims);
}
ํํฐ๋ฅผ ํตํ ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ ๊ธฐ๋ฅ ๊ตฌํ
ํ์ด์ง ์ ๊ทผ ์์ฒญ ์, ํ ํฐ์ด ์ ํจํ ๊ฒฝ์ฐ๋ง ์ ๊ทผ์ ํ์ฉํ๋ ํํฐ๋ฅผ ๊ตฌํํ๊ฒ ์ต๋๋ค.
์ธ๊ฐ ์ฒ๋ฆฌ๊ฐ ํ์ํ์ง ์์ ์์ฒญ์ ๋ํด์ ํ ํฐ ์ ํจ์ฑ์ ๊ฒ์ฆํ์ง ์๋๋ก whiteListUris๋ฅผ ์์ฑํ์ต๋๋ค.
resolveToken์ผ๋ก request header์ Authorization์ ๋ด๊ธด accessToken์ ๊ฐ์ ธ์ต๋๋ค.
validateToken์ผ๋ก accessToken์ ์ ํจ์ฑ์ ๊ฒ์ฆํฉ๋๋ค.
์ ํจํ ํ ํฐ์ด๋ฉด userId, role ์ ๋ณด๋ฅผ request์ attribute๋ก ์ถ๊ฐํฉ๋๋ค.
request header์ ํ ํฐ์ด ์๊ฑฐ๋, ์ ํจํ์ง ์์ ํ ํฐ์ด๋ฉด 401 UNAUTHORIZED ์๋ต์ ๋ณด๋
๋๋ค.
@RequiredArgsConstructor
@Component
public class JwtValidationFilter implements Filter {
private final JwtProvider jwtProvider;
private final ObjectMapper objectMapper;
private final String[] whiteListUris
= new String[] {"*/users*", "/auth/refresh/token", "*/h2-console*"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// ํ ํฐ ์ ํจ์ฑ์ ๊ฒ์ฆํ์ง ์์๋ ๋๋ ๊ฒฝ์ฐ
if (whiteListCheck(httpServletRequest.getRequestURI())) {
chain.doFilter(request, response);
return;
}
// ์์ฒญ ํค๋์ ํ ํฐ์ด ์๋ ๊ฒฝ์ฐ
if (!isContainToken(httpServletRequest)) {
httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), "์ธ์ฆ ์ค๋ฅ");
return;
}
String accessToken = jwtProvider.resolveToken(httpServletRequest);
JwtExceptionType jwtException = jwtProvider.validateToken(accessToken);
if (jwtException == JwtExceptionType.VALID_JWT_TOKEN) {
request.setAttribute(AUTHENTICATE_USER, getAuthenticateUser(accessToken));
} else if (jwtException == JwtExceptionType.EXPIRED_JWT_TOKEN) {
httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), "ํ ํฐ ๋ง๋ฃ");
return;
}
chain.doFilter(request, response);
}
private boolean whiteListCheck(String uri) {
return PatternMatchUtils.simpleMatch(whiteListUris, uri);
}
private boolean isContainToken(HttpServletRequest request) {
String authorization = request.getHeader(AUTHORIZATION_HEADER);
return authorization != null && authorization.startsWith(BEARER_TOKEN_PREFIX);
}
private AuthenticateUser getAuthenticateUser(String token) throws JsonProcessingException {
Claims claims = jwtProvider.getClaims(token);
String authenticateUserJson = claims.get(AUTHENTICATE_USER, String.class);
return objectMapper.readValue(authenticateUserJson, AuthenticateUser.class);
}
}
// JwtProvider.java
// Request Header์์ token ๊ฐ ๊ฐ์ ธ์ด
public String resolveToken(HttpServletRequest request) {
String header = request.getHeader(AUTHORIZATION_HEADER);
return header.split(" ")[1];
}
// jwt ์ ํจ์ฑ ๊ฒ์ฌ
public JwtExceptionType validateToken(String token) {
try {
getClaims(token);
return JwtExceptionType.VALID_JWT_TOKEN;
} catch (SignatureException exception) {
log.info("JWT ์๋ช
์ด ์ ํจํ์ง ์์ต๋๋ค");
return JwtExceptionType.INVALID_JWT_SIGNATURE;
} catch (MalformedJwtException exception) {
log.info("JWT๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ง ์์์ต๋๋ค.");
return JwtExceptionType.INVALID_JWT_TOKEN;
} catch (ExpiredJwtException exception) {
log.info("๋ง๋ฃ๋ ํ ํฐ์
๋๋ค.");
return JwtExceptionType.EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException exception) {
log.info("์ง์๋์ง ์๋ ํ์์ ํ ํฐ์
๋๋ค.");
return JwtExceptionType.UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException exception) {
log.info("JWT Claim์ด ๋น์ด์์ต๋๋ค.");
return JwtExceptionType.EMPTY_JWT;
}
}
์ด๋ ๊ฒ ์์ฑํ ํํฐ๋ FilterRegistrationBean์ ํตํด ๋ฑ๋กํด์ผ ๋์ํฉ๋๋ค.
FilterRegisterationBean๋ฅผ ๋ฑ๋กํ๊ธฐ ์ํ WebConfig๋ผ๋ Configuration ํด๋์ค๋ฅผ ์์ฑํ์ต๋๋ค.
setFilter๋ก filter๋ฅผ ๋ฑ๋กํ๊ณ setOrder๋ก ์์๋ฅผ ์ง์ ํ์ต๋๋ค.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<Filter> jwtValidationFilter(JwtProvider jwtProvider, ObjectMapper objectMapper) {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new JwtValidationFilter(jwtProvider, objectMapper));
filterFilterRegistrationBean.setOrder(1);
return filterFilterRegistrationBean;
}
}
์ธํฐ์ ํฐ๋ฅผ ํตํ ์ธ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ
ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ์ ํน์ ์์ฒญ, ์ปจํธ๋กค๋ฌ์ ๊ด๊ณ์์ด ์ ์ญ์ ์ผ๋ก ์ฒ๋ฆฌํด์ผ ํ๋ ์์ ์ด๋ฏ๋ก ํํฐ๋ก ๊ตฌํํ์ต๋๋ค.
๋ฐ๋ฉด ํน์ url์ ๋ํ ์ธ๊ฐ ๊ธฐ๋ฅ์ ํด๋ผ์ด์ธํธ์ ์์ฒญ๊ณผ ๊ด๋ จ๋์ด ์ ์ญ์ ์ผ๋ก ์ฒ๋ฆฌํด์ผ ํ๋ ์์ ๋ค์ด๋ฏ๋ก ์ธํฐ์ ํฐ๋ก ๊ตฌํํ์ต๋๋ค.
์ ์ ๋ ADMIN, CUSTOMER, DRIVER ์ค ํ๋์ ๊ถํ์ ๊ฐ์ง๋๋ค.
์ด๋ ๊ณ ๊ฐ ๊ถํ์ ๊ฐ์ง ์ ์ ๋ ๊ธฐ์ฌ ํ์ด์ง์ ์ ๊ทผ ํ ์ ์์ผ๋ฉฐ, ๊ธฐ์ฌ ๊ถํ์ ๊ฐ์ง ์ ์ ๋ ๊ณ ๊ฐ ํ์ด์ง์ ์ ๊ทผํ ์ ์์ต๋๋ค.
์ด์ ๊ฐ์ ์ธ๊ฐ๋ฅผ ์ฒ๋ฆฌํ๋ UserAuthorizationInterceptor๋ฅผ ์์ฑํ์ต๋๋ค.
ํ ํฐ ์ ํจ์ฑ์ด ๊ฒ์ฆ๋ ์ ์ ์ ํํด์ ์ธ๊ฐ ์ฒ๋ฆฌ๋ฅผ ์งํํฉ๋๋ค.
์์ฒญํ url์ด ์๋ ๊ฒฝ์ฐ 404 NOT FOUND๋ฅผ ์๋ตํฉ๋๋ค.
hasAuthority๋ก ์ ์ role์ ๋ฐ๋ฅธ ์ปจํธ๋กค๋ฌ ์ ๊ทผ ๊ถํ์ ํ์ธํฉ๋๋ค.
์ ๊ทผ ๊ถํ์ด ์๋ค๋ฉด ๋ค์ ์ธํฐ์
ํฐ๋ ์ปจํธ๋กค๋ฌ๋ฅผ ์คํํฉ๋๋ค.
์ ๊ทผ ๊ถํ์ด ์๋ค๋ฉด 401 UNAUTHORIZED ์๋ต์ ๋ฐํํฉ๋๋ค.
public class UserAuthorizationInterceptor implements HandlerInterceptor {
private static final String CUSTOMER_URL = "/reservations";
private static final String DRIVER_URL = "/orders";
@Override
public boolean preHandle(HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull Object handler)
throws IOException {
if (request.getAttribute(AUTHENTICATE_USER) != null) {
AuthenticateUser authenticateUser = (AuthenticateUser)request.getAttribute(AUTHENTICATE_USER);
Role role = authenticateUser.getRole();
String baseUrl = getBaseUrl(handler);
if (baseUrl == null) {
response.sendError(HttpStatus.NOT_FOUND.value(), "์์ฒญํ ํ์ด์ง๋ฅผ ์ฐพ์ ์ ์์");
return false;
}
// url์ ๋ํ ์ ๊ทผ ๊ถํ ํ์ธ
if (hasAuthority(baseUrl, role)) {
return true;
}
}
response.sendError(HttpStatus.UNAUTHORIZED.value(), "์์ธ์ค ๊ถํ ์์");
return false;
}
private static String getBaseUrl(Object handler) {
if (handler instanceof HandlerMethod handlerMethod) { //ํธ์ถํ ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ ๋ชจ๋ ์ ๋ณด๊ฐ ํฌํจ๋์ด ์์
return handlerMethod.getMethod().getDeclaringClass().getAnnotation(RequestMapping.class).value()[0];
}
return null;
}
private boolean hasAuthority(String url, Role role) {
if ((role.equals(Role.CUSTOMER) && url.endsWith(DRIVER_URL))
|| (role.equals(Role.DRIVER) && url.endsWith(CUSTOMER_URL))) {
return false;
}
return true;
}
}
์ด๋ ๊ฒ ์์ฑํ ์ธํฐ์
ํฐ๋ InterceptorRegistry๋ฅผ ํตํด ๋ฑ๋กํด์ผ ๋์ํฉ๋๋ค.
WebConfig์ InterceptorRegistry.addInterceptor๋ก ์ธํฐ์
ํฐ๋ฅผ ๋ฑ๋กํ์ต๋๋ค.
ํ์๊ฐ์
/๋ก๊ทธ์ธ์ jwt ๋ฐ๊ธ ์ ์ ์ํ๋๋ ์์
์ด๋ฏ๋ก excludePathPatterns๋ก ์ธ๊ฐ ์ฒ๋ฆฌ์์ ์ ์ธํ์ต๋๋ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserAuthorizationInterceptor())
.order(1)
.excludePathPatterns("/api/v1/users/**", "*/h2-console*", "/css/**", "/*.ico", "/error");
}
}
- (ํ์) ์์ธ์ฒ๋ฆฌ ํด์ผํจ
- (์ ํ) ๋ฆฌํ๋ ์ ํ ํฐ ๋ฐ๊ธ / ๋ก๊ทธ์์ ๊ตฌํํ ์ง ์๊ธฐํด๋ด์ผํจ
- ๊ตฌํํ๊ณ ๋๋๊น ๋นํ์์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํด์ผํ๋ ์ถ์๋ฐ ๋นํ์์ด ์ ๊ทผํ๋ url๋ง ์ธ์ฆ, ์ธ๊ฐ ์ ํ๋ฉด ๋ ๊ฒ ๊ฐ๊ธฐ๋ ํ๊ณ