JWT(JSON Web Token) 란?
- JWT는 유저를 인증하고 식별하기 위한 토큰(Token) 기반 인증입니다.
- 토큰 자체에 사용자의 권한 정보나 서비스를 사용하기 위한 정보가 포함됩니다.
- RESTful과 같은 무상태(Stateless)인 환경에서 사용자 데이터를 주고받을 수 있게 됩니다.
- 세션(Session)을 사용하게 될 경우 쿠키 등을 통해 사용자를 식별하고 서버에 세션을 저장했지만, 토큰을 클라이언트에 저장하고 요청시 HTTP 헤더에 토큰을 첨부하는 것만으로도 단순하게 데이터를 요청하고 응답받을 수 있습니다.
JWT는 JSON 형식을 사용하여 정보를 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다. JWT는 서버와 클라이언트 간에 정보를 전송하는 데 널리 사용되며, 주로 인증과 권한 부여를 위해 사용됩니다.
JWT 구조
![](https://blog.kakaocdn.net/dn/cK6kG3/btsJsfzupLB/IWLIHpcos3GCCtICXacKjK/img.png)
- JWT는 세 파트로 나누어지며, 각 파트는 점으로 구분하여 표현됩니다.
(1) Header (헤더)
- 서명 시 사용하는 키(kid), 사용할 타입(typ), 서명 암호화 알고리즘(alg)의 정보가 담겨 있습니다.
![](https://blog.kakaocdn.net/dn/rpDyy/btsJrC23phV/kygg9BcMETdCTqBErHWFPk/img.png)
- kid : 서명 시 사용하는 키(Public/Private Key)를 식별하는 값
- typ : 토큰 유형
- alg : 서명 암호화 알고리즘 HS256(HMAC SHA-256), HS512, RS256(RSASSA SHA-256), ES256(ECDSA P-256 curve SHA-256)
(2) Payload (페이로드)
- 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있습니다.
- 클레임(Claim)은 Key/Value 형태로 된 값을 가집니다.
- 저장되는 정보에 따라 등록된 클레임(Registered Claims), 공개 클레임(Public Claims), 비공개 클레임(Private Cliams)로 구분됩니다.
![](https://blog.kakaocdn.net/dn/du6zS9/btsJrZczFSP/S6gnKMd8lQs0bOP7aB4tM0/img.png)
- iss : 토큰 발급자(issuer) – Public Claims
- sub : 토큰 제목(subject) – Public Claims
- iat : 토큰 발급 시간(issued at) – Public Claims
- exp : 토큰 만료 시간(expiration) – Public Claims
- roles : 권한 – Private Cliams
(3) Signature (서명)
- Header(헤더) 에서 정의한 알고리즘 방식(alg)을 활용합니다.
- Header(헤더)+ 페이로드(Payload)와 서버가 갖고 있는 유일한 key 값을 합친 것을 헤더에서 정의한 알고리즘으로 암호화합니다.
![](https://blog.kakaocdn.net/dn/UlTuS/btsJrTcnTko/62iKyxKZSBv56jJLvx6VF1/img.png)
- Header(헤더) 와 페이로드(Payload)는 단순히 인코딩된 값이기 때문에 제 3자가 복호화 및 조작할 수 있지만, Signature(서명)는 서버 측에서 관리하는 비밀키가 유출되지 않는 이상 복호화할 수 없습니다. 이는 토큰의 위변조 여부를 확인하는데 사용됩니다.
JWT의 장점
- 자체 포함 (Self-contained):
- JWT는 필요한 모든 정보를 페이로드에 담고 있어 서버가 추가적인 데이터 저장소 없이도 사용자 정보를 검증할 수 있습니다.
- 무상태 (Stateless):
- 서버는 클라이언트의 상태를 저장하지 않으므로, JWT는 상태 없는 인증에 적합합니다. 클라이언트가 토큰을 포함하여 요청을 보내면, 서버는 토큰만으로 요청을 인증할 수 있습니다.
- 보안:
- JWT는 서명 또는 암호화를 통해 데이터의 무결성을 보호하고, 암호화된 JWT는 데이터의 프라이버시를 보장합니다.
- 다양한 클레임:
- JWT는 다양한 클레임을 지원하여 유효 기간, 발급자, 주체 등의 정보를 포함할 수 있습니다.
- 호환성:
- JWT는 다양한 플랫폼과 언어에서 사용할 수 있으며, JSON 포맷을 사용하기 때문에 쉽게 읽고 쓸 수 있습니다.
JWT의 단점
- 크기:
- JWT는 Base64로 인코딩된 문자열로, 크기가 커질 수 있습니다. 이는 네트워크 요청 시 대역폭을 증가시킬 수 있습니다.
- 만료 관리:
- JWT는 기본적으로 만료 시간이 포함되지 않으며, 만료된 토큰에 대한 처리가 필요합니다. 클라이언트가 만료된 토큰을 가지고 있을 경우, 서버는 이를 적절히 처리해야 합니다.
- 서명 알고리즘의 안전성:
- JWT의 서명 알고리즘과 비밀 키의 안전성이 보안에 직접 영향을 미칩니다. 키가 유출되면 서명의 무결성도 무너질 수 있습니다.
MSA (Microservices Architecture)에서 JWT 사용의 장점
- 분산 인증:
- MSA에서는 여러 개의 마이크로서비스가 서로 독립적으로 동작합니다. JWT는 인증 정보를 각 서비스가 독립적으로 검증할 수 있게 해줍니다. 이로 인해 각 서비스가 독립적으로 사용자를 인증하고 권한을 부여할 수 있습니다.
- 무상태 인증:
- MSA에서는 서버 간의 상태를 공유하지 않고 독립적으로 처리하는 것이 일반적입니다. JWT는 무상태(stateless) 토큰으로, 각 서비스가 자체적으로 토큰을 검증할 수 있어 적합합니다.
- 스케일링:
- JWT는 클라이언트 측에서 정보를 저장하므로, 인증 관련 상태를 서버 측에서 관리할 필요가 없어 서버를 수평으로 확장하는 데 유리합니다.
- 통합 및 상호 운영성:
- 여러 마이크로서비스가 통합되어 동작할 때, JWT는 공통된 형식으로 인증 정보를 전달할 수 있어 서비스 간의 상호 운영성을 높입니다.
- 보안 관리:
- JWT는 클레임을 통해 인증 정보를 포함하고, 서명을 통해 데이터의 무결성을 보장합니다. 이를 통해 각 서비스가 인증 및 권한 부여를 효과적으로 관리할 수 있습니다.
JWT는 MSA 아키텍처에서 인증과 권한 부여를 효과적으로 관리할 수 있는 강력한 도구입니다. 각 서비스가 독립적으로 인증을 처리하고, 클라이언트와 서버 간의 보안이 유지되는 구조를 제공합니다.
JWT 설정
로그인이 성공하였을때 JWT를 생성하여 쿠키에 저장합니다. 클라이언트에서 JWT를 관리하고 서버에 요청할때 마다 쿠키의 JWT를 filter에서 인증 받도록 설계하였습니다. 쿠키로 설정시 해킹당하지 않게 보안에 주의하여야합니다. CORS, 자바스크립트에서 접근 불가, 애플리케이션 접근 등의 보안 설정이 중요합니다.
1) 의존성 주입
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
jjwt는 JWT 토큰 생성 및 파싱, 검증을 해주는 라이브러리 입니다.
jjwt-api, jjwt-impl, jjwt-jacson 를 모두 추가해 주어야합니다.
2) 토큰 생성
// 로그인 성공시에 jwt 설정 토큰을 만들어서 쿠키에 저장하여 가지고 다님
String token = JwtUtil.generateToken(info.get().getId(), SECRET_KEY);
Cookie cookie = new Cookie("token", token);
cookie.setHttpOnly(true); // 클라이언트 자바스크립트에서 접근 불가
cookie.setPath("/"); // 애플리케이션 전체에서 접근 가능
response.addCookie(cookie);
// 토큰 생성
public static String generateToken(String userId, String SECRET_KEY) {
// SECRET_KEY를 Base64로 디코딩하여 Key로 변환
byte[] keyBytes = Base64.getDecoder().decode(SECRET_KEY);
Key signingKey = Keys.hmacShaKeyFor(keyBytes);
Map<String, String> map = new HashMap<>(); // map 방식으로도 가능
map.put("userId", userId);
return Jwts.builder()
.setClaims(map)
.setSubject(userId)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10시간 유효
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}
SECRET_KEY : 토큰을 생성하기 위해 만든 암호화된 키값으로 아래의 방식으로 랜덤으로 생성하였습니다.
토큰 유효시간 : 10 시간
sign : HS256(HMAC SHA-256) 사용
secret key 생성
var key = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256);
System.out.println("secret key : " + key.getEncoded());
3) Spring Filter에서 JWT 복호화 및 인증
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtRequestFilter.class);
@Value("${jwt.key}")
private String SECRET_KEY;
// 제외할 URL 목록
private final List<String> excludeUrlPatterns = List.of("/api/login", "/api/create", "/api/logout");
private Key getSigningKey() {
byte[] keyBytes = Base64.getDecoder().decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes); // Key 객체 생성
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 현재 요청 URI
String requestUri = request.getRequestURI();
// 제외할 경로에 해당하는 경우 필터를 건너뜁니다.
if (excludeUrlPatterns.contains(requestUri)) {
chain.doFilter(request, response);
return;
}
logger.info("Request received at {}", request.getRemoteAddr());
// 쿠키에서 JWT 추출
String jwt = extractJwtFromCookies(request);
if (jwt != null) {
try {
String userId = extractUsername(jwt);
if (userId != null && request.getAttribute("userId") == null) {
// JWT가 유효하면 사용자 정보를 설정
request.setAttribute("userId", userId);
} else if (userId == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "토큰이 존재하지 않습니다.");
return;
}
} catch (Exception e) {
// JWT가 유효하지 않은 경우
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "토큰이 만료되었거나 존재하지 않습니다.");
return;
}
}
chain.doFilter(request, response);
logger.info("Response sent to {}", request.getRemoteAddr());
}
private String extractJwtFromCookies(HttpServletRequest request) {
jakarta.servlet.http.Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (jakarta.servlet.http.Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
private String extractUsername(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
//return (String) claims.get("userId"); // claims map 방식으로도 가능
}
}
쿠키에 저장된 토큰을 복호화하여 Claims 형태로 변환시킵니다. 위의 소스를 보면 sub 즉 토큰제목에 userId 를 집어 넣었고 토큰에서 토큰 제목이 존재하느냐 존재하지 않느냐에 따라 인증된 토큰을 확인되도록 하였습니다.
방식에 따라 claims에 오브젝트값을 세팅하여 key, value 처럼 key를 호출하여 값을 가져와서 세팅을 할 수도 있습니다.
JWT 실행 결과
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
위의 url에 접속하면 생성된 토큰을 입력하면 아래와 같이 토큰에 종속된 결과값을 볼 수 있습니다.
'WEB > Spring' 카테고리의 다른 글
Spring Cloud Config Server 설정하기2(Spring Cloud Bus) (0) | 2024.09.23 |
---|---|
Spring Cloud Config Server 설정하기1(Spring Cloud Config) (0) | 2024.09.12 |
Spring Boot AOP 적용 방법(Log, Transaction) (4) | 2024.09.03 |
Spring Boot로 MSA 개발하기 (5) | 2024.08.30 |
스프링 IOC컨테이너 - DI, DL (0) | 2024.03.23 |