JWT 란
JSOM Web Token의 약자로 json 데이터가 담겨있는 토큰을 말한다.
JWT 구조
Header 헤더
alg : 암호화 알고리즘
typ : 토큰 유형
Payload 정보
sub : 제목
iat : 발급시간
Signature 서명
base64UrlEncode(
HMACSHA256(
base64UrlEncode(header) +
base64UrlEncode(payload) +
secretKey
)
)
세션 VS JWT
세션은 데이터의 일부를 서버가 저장하고 나머지 데이터를 클라이언트에게 보낸다. 이후 요청이 올 때 마다 세션을 검사하여 요청에 응답한다. 그러나 트래픽이 증가하면, 서버에 부담이 많이 가기 때문에 JWT와 같은 인증방식이 등장했다.
JWT는 인증에 성공한 후 전달받은 토큰을 클라이언트가 보관하고 있으며, API 요청시 토큰을 함께 보낸다.
서버 측에서는 secretKey를 이용하여 토큰을 디코딩하여 유효성을 검사한다.
JWT 장단점
장점
- 데이터 위변조가 어렵다.
- 별도의 저장소가 필요하지 않다.
- 확장성이 우수하다.
단점
- 정보에 중요한 정보를 담을 수 없다.
- 토큰 탈취 시 대처가 어렵다.
Refresh 토큰의 등장
하나의 토큰만 발급하는 것도 문제는 없으나 토큰이 만료되면 다시 한 번 로그인해야하는 번거로움이 생긴다.
이를 해결하기 위해 기존 토큰보다 만료시간이 좀 더 긴 토큰인 Refresh토큰이 생겨났다.
기존 토큰(보통 AccessToken이라 불린다.)이 만료되면 Refresh토큰을 통해 다시 Access토큰과 Refresh토큰을 요청받는다.
이를 통해 사용자 경험 저하를 방지할 수 있다.
Refresh 토큰 + Redis
Redis에 Refresh 토큰을 저장하여 탈취를 막을 뿐만 아니라 다중 로그인 제어와 같은 기능을 구현할 수 있다.
해당 방법은 세션과 유사하나 세션에 비해 Redis가 서버에 거는 부담이 줄어들어 이와 같은 방법을 사용한다.
따라서 자신의 프로젝트에 맞는 인증 방식을 채택해야 한다. 세션은 구시대적 방식이 아니다.
토큰 위변조 파악 로직
package com.synergy.synergyback.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
public String generateToken(String email, String userType, String nickname, long expirationTime) {
Map<String, Object> claims = new HashMap<>();
claims.put("email", email);
claims.put("userType", userType);
claims.put("nickname", nickname);
return Jwts.builder()
.setClaims(claims)
.setSubject(email)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Boolean validateToken(String token, String email, String userType) {
Map tokenData = getTokenData(token);
final String tokenEmail = (String) tokenData.get("email");
return (tokenEmail.equals(email) && !isTokenExpired(token));
}
public Map getTokenData(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
String userType = claims.get("userType", String.class);
String email = claims.get("email", String.class);
String nickname = claims.get("nickname", String.class);
Map<String, String> tokenData = new HashMap<>();
tokenData.put("code", "200");
tokenData.put("userType", userType);
tokenData.put("email", email);
tokenData.put("nickname", nickname);
return tokenData;
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private Date getExpirationDateFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims.getExpiration();
}
}
토큰 유효성 검사 로직 적용 위치
# java
package com.synergy.synergyback.config.interceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 요청 경로 확인
String requestURI = request.getRequestURI();
// /manage/** 경로에 대해서만 처리
if (requestURI.startsWith("/manage/")) {
// 쿠키에서 reroll_AT 값 확인
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("reroll_AT".equals(cookie.getName())) {
// reroll_AT 쿠키 값 확인
String rerollAtValue = cookie.getValue();
// userType이 admin인지 확인
if ("admin".equals(getUserTypeFromToken(rerollAtValue))) {
// admin 권한이면 요청 허용
return true;
}
}
}
}
// userType이 admin이 아니면 요청 거부
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Admin access only");
return false;
}
// /manage/** 경로가 아닌 경우에는 인터셉터 적용하지 않음
return true;
}
// reroll_AT 쿠키에서 userType 추출하는 메서드 (실제 구현은 프로젝트의 인증/인가 로직에 따라 다름)
private String getUserTypeFromToken(String rerollAtValue) {
// 예시 구현
// 실제로는 JWT 디코딩 또는 다른 인증 방식을 통해 userType 추출 필요
return "admin"; // 예시로 admin을 리턴하도록 설정
}
}
# nuxt
// middleware/auth.global.js
export default defineNuxtRouteMiddleware(async (to, from) => {
const config = useRuntimeConfig();
const { $auth } = useNuxtApp();
const rerollAT = useCookie('reroll_AT');
const rerollRT = useCookie('reroll_RT');
if (!rerollAT?.value) {
if (rerollRT?.value) {
const response = await $fetch('/token/refresh', {
baseURL: config.public.baseURL,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.code === '200') {
document.cookie = `reroll_AT=${response.accessToken}; SameSite=None; Secure; Max-Age=10800; Path=/`;
document.cookie = `reroll_RT=${response.refreshToken}; SameSite=None; Secure; Max-Age=36000; Path=/`;
} else {
$auth.dataRefresh();
}
} else {
$auth.dataRefresh();
}
}
if (to.path.startsWith('/manage')) {
if (!$auth.isAdmin.value) {
return navigateTo('/login');
}
}
});
'프로그래밍 일기 > Java & Spring' 카테고리의 다른 글
Spring Boot Nuxt3 카카오 로그인 및 JWT 토큰 발급 (0) | 2024.07.22 |
---|---|
자바 스프링 메일 안에 이미지 추가하는 방법 (0) | 2023.07.18 |
[에러해결] javax.mail.MessagingException: Could not connect to SMTP host: smtp.worksmobile.com, port: 465, response: -1 (isSSL true 설정) (0) | 2023.07.18 |
[Spring Boot] 파일 다운로드 로직 구현 feat.Nuxt3 (0) | 2023.07.12 |
[Spring Boot] 단일, 다중 파일 업로드 로직 구현 feat.Nuxt3 (0) | 2023.07.12 |