JWT란?
두 당사자 간에 정보를 안전하게 전송하기 위해 널리 사용되는 JSON 기반의 토큰 표준
주로 인증과 권한 부여 목적으로 사용된다.
JWT와 세션의 차이
특징 | JWT | 세션 |
저장 방식 | 클라이언트가 토큰을 저장 | 서버가 세션을 관리 |
서버 부하 | 상태 비저장 (Stateless) | 상태 저장 (Stateful) |
확장성 | 마이크로서비스 환경에서 유리 | 분산 환경에서는 부적합 |
보안 | 탈취된 토큰은 만료 전까지 사용 가능 | 세션은 서버 측에서 제어 가능 |
Stateless? Stateful?
Stateless(상태 없음)과 Stateful(상태 유지)는 상태를 관리하는 방식에 따라 구분되는 개념이다.
Stateless (상태 없음)
요청 간에 서버가 클라이언트의 상태 정보를 저장하지 않는 방식
각 요청은 독립적으로 처리되며, 이전 요청의 정보를 참조하지 않는다.
확장성과 분산 처리가 중요한 경우 사용
Stateful (상태 유지)
요청 간에 서버가 클라이언트의 상태 정보를 저장하고 참조하는 방식
클라이언트가 서버와의 연결을 유지하는 동안 상태가 지속된다.
사용자와 지속적인 상호작용이 필요한 시스템에서 사용 (채팅, 게임, 장바구니 등)
Maven에 JWT 라이브러리 추가
mvn repository 에 jjwt 검색
아래 보이는 세 개를 다운받으면 된다.
JJWT :: API
JJWT :: impl
JJWT :: Jackson
pom.xml에 넣어주기
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
JWT 확인
아래 사이트에서 토큰에 어떤 데이터가 들어가있는지 확인할 수 있다.
JWT 구조
Header
어떤 암호화 알고리즘으로 토큰을 만들 것인지, 토큰 타입
Payload
토큰 안에 여러 정보를 담아두는 곳 (사용자 id, 사용자 이름 등)
Signature
토큰의 무결성을 보장하기 위해 사용된다.
JWT 생성
Claims 객체 생성
Claims는 JWT의 Payload에 포함될 데이터를 나타낸다.
key와 value 형식으로 데이터를 넣는다.
Claims claims = Jwts.claims().setSubject(subject);
claims.put("userName", "test10");
claims.put("userIdx", 10);
// Payload 구조
{
"sub": "내 토큰",
"userName": "test10",
"userIdx": 10
}
토큰 생성
setClaims
JWT의 Payload에 작성한 claims 객체를 넣는다.
setIssuedAt
토큰의 발행 시간을 현재 시점으로 설정한다.
setExpiration
토큰의 만료 시간을 설정한다.
signWith(SignatureAlgorithm.HS256, "secret key")
암호화할 때 사용하는 알고리즘을 적어주고, 알고리즘을 사용할 때 시크릿 키를 사용한다.
=> 이 시크릿 키로 인증 생성 및 검증을 하기 때문에 안전하게 관리되어야 한다.
=> 32글자 이상이어야 한다.
compact
JWT를 문자열 형태로 반환한다.
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + exp))
.signWith(SignatureAlgorithm.HS256, "내 secret 키")
.compact();
JwtUtil
package token;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtUtil {
public static String generateToken(String subject, int exp) {
Claims claims = Jwts.claims().setSubject(subject);
claims.put("userName", "test10");
claims.put("userIdx", 10);
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + exp))
.signWith(SignatureAlgorithm.HS256, "qwerasdfzxcvqwerasdfzxcvqwerasdfzxcv1234567890")
.compact();
return token;
}
}
Main
시간은 밀리초 단위이기 때문에 10000 = 10초가 된다.
1000 = 1초
1000 * 60 = 1분
1000 * 60 * 60 = 1시간
package token;
public class Main {
public static void main(String[] args) {
// 밀리초 단위
String token = JwtUtil.generateToken("내 토큰", 10000); // 10초 뒤 만료
System.out.println(token);
}
}
JWT 검증
토큰을 생성했을 때 사용한 시크릿 키를 등록해주고, 매개변수로 받은 토큰을 넣어준다.
에러 처리가 되면 토큰이 만료된 것이므로 토큰 만료됨을 출력하고 false를 반환한다.
public static boolean validate(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(SECRET)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
System.out.println("토큰 만료됨");
return false;
}
return true;
}
else if ("/login".equals(action)) {
User user = new User(
req.getParameter("email"),
req.getParameter("password")
);
int userIdx = userService.login(user);
if (userIdx > 0 ) {
// jwt 토큰 생성
String token = JwtUtil.generateToken(userIdx, user.getEmail());
UserSignupRes resDto = new UserSignupRes(true, token);
Cookie cookie = new Cookie("ATOKEN", token); // ATOKEN으로 토큰 저장
cookie.setHttpOnly(true);
cookie.setSecure(false); // 원래는 true로 해야됨
cookie.setMaxAge(30 * 60); // 초 단위
cookie.setPath("/");
resp.addCookie(cookie);
JsonParser.parse(resp, resDto);
return;
}
resp.sendRedirect("/user/login");
}
JWT에서 값 가져오기
토큰 생성했을 때 사용한 시크릿 키를 사용한다.
Claims 객체에 토큰을 풀어서 저장해주고, 가져오고 싶은 key와 타입 클래스를 사용해서 가져와준다.
public static int getUserIdx(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(SECRET)
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("userIdx", Integer.class);
} catch (ExpiredJwtException e) {
System.out.println("토큰이 만료되었습니다!");
return 0;
}
}
전체 코드
package utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtUtil {
private static final String SECRET = "내 시크릿 키";
private static final int EXP = 30 * 60 * 1000; // 30분
public static int getUserIdx(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(SECRET)
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("userIdx", Integer.class);
} catch (ExpiredJwtException e) {
System.out.println("토큰이 만료되었습니다!");
return 0;
}
}
public static String generateToken(int userIdx, String userEmail) {
Claims claims = Jwts.claims();
claims.put("userEmail", userEmail);
claims.put("userIdx", userIdx);
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXP))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
return token;
}
public static boolean validate(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(SECRET)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
System.out.println("토큰이 만료되었습니다!");
return false;
}
return true;
}
}
'BE > Java' 카테고리의 다른 글
[Java] PortOne 카카오페이 결제 연동 (JSON 데이터 처리, 장바구니 기능) (0) | 2025.01.15 |
---|---|
[Java] 카카오 로그인 API 연동 (1) | 2025.01.10 |
[Java] DTO 설계 시 고려사항 (0) | 2025.01.10 |
[Java] multipart/form-data란? / form 태그로 서버에 파일 전달하기 (0) | 2025.01.09 |
[Java] SQL Injection, PreparedStatement (2) | 2025.01.09 |