쿠키와 세션
쿠키와 세션 모두 HTTP에 상태 정보를 유지하기 위해 사용된다. 이를 통해 서버는 클라이언트 별로 인증 및 인가를 할 수 있다.
사용 예시 > 사이트 팝업의 "오늘 다시 보지 않기" 정보 저장
쿠키
클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일
구성요소
- Name 이름 : 쿠키를 구별하는데 사용되는 키 (중복 될 수 없음)
- Value 값 : 쿠키의 값
- Domain : 쿠키가 저장되는 도메인
- Path : 쿠키가 사용되는 경로
- Expires : 쿠키의 만료기한 (만료기한이 지나면 삭제 됨)
세션
서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용 됨.
서버에서 클라이언트 별로 유일무이한 '세션 ID'를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장 함.
사용 예시 > 로그인 정보 저장
JWT Json Web Token
Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token. 쿠키 저장소에 JWT 저장함.
JWT를 사용하는 이유 : 모든 서버에서 동일한 Secret Key를 소유하며 이 Key를 통해 로그인 정보를 암호화하여 클라이언트에 저장.
JWT의 구조 : Header - Payload - Signature. Payload에 실제 정보가 들어있고, Header와 Signature은 암호화 관련된 정보 양식
JWT 사용의 흐름 예시
Client가 username, password로 로그인 성공 시
- 서버에서 Secret Key를 사용해 로그인 정보를 JWT로 암호화
- 서버에서 직접 쿠키를 생성해 JWT를 담아 Client 응답에 전달
- 브라우저 쿠키 저장소에 자동으로 JWT 저장됨
Client 에서 JWT 통해 인증하는 과정
- 서버에서 API 요청 시마다 쿠키에 포함된 JWT 찾아서 사용 (쿠키가 여러 개일 수 있기 때문에 쿠키의 이름을 확인하여 가져옴)
- Client가 전달한 JWT을 Secret Key를 사용해 위조 여부 검증
- JWT 유효기간이 지나지 않았는지 검증
- 검증 성공 시, JWT에서 사용자 정보를 가져와 확인
JwtUtil 만들기
Util 클래스란 특정 매개 변수에 대한 작업을 수행하는 메소드들이 존재하는 클래스. 다른 객체에 의존하지 않고 하나의 모듈로서 동작하는 클래스. JWT 관련 기능들을 가진 JwtUtil 클래스 만들기
<JWT 관련 기능>
- JWT 생성
- 생성된 JWT를 Cookie에 저장
- Cookie에 들어있던 JWT 토큰을 Substring
- JWT 검증
- JWT에서 사용자 정보 가져오기
토큰 생성에 필요한 데이터
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 로그 설정
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
*@Value 로 가져오는 jwt.secret.key는 application properties에 있는, 강의에서 임의로 정해둔 Base64로 암호화된 Secret Key이다.
JWT를 생성할 때 가져온 Secret Key로 암호화 한다. 이 때 Secret Key는 암호화 되어 있기에 다시 복호화해서 사용해야 한다.
Key는 복호화 된 Secret Key를 담는 객체.
@PostConstructor는 딱 한 번만 받아오면 되는 값을 사용 할 때 마다 요청을 새로 호출하는 실수를 방지하기 위해 사용 된다.
암호화 알고리즘은 HS256 알고리즘 사용(option)
사용자의 권한 종류를 Enum으로 관리
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
JWT를 생성할 때 사용자의 정보로 해당 사용자의 권한을 넣어줄 때 사용
JWT 생성
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role) // 사용자 권한
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
JWT의 subject에 사용자의 식별값(ID) 넣기
사용자의 권한 정보 넣기 (key-value 형식)
토큰 만료시간 넣기 (ms 기준)
issuedAt에 발급일 넣기
signWith에 secretKey 값을 담고 있는 key와 암호화 알고리즘 값 넣기
JWT Cookie에 저장
// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
try {
token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
JWT 토큰 Substring
// JWT 토큰 substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
StringUtils.hasText를 사용하여 공백, Null을 확인하고 startsWith를 사용하여 토큰의 시작 값이 Bearer이 맞는지 확인.
맞다면 순수 JWT를 반환하기 위해 substring을 사용하여 Bearer 잘라내기
JWT 검증
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
Jwts.parserBuilder( )를 사용하여 JWT를 파싱할 수 있음. secret key 값을 이용해 JWT 검증.
JWT에서 사용자 정보 가져오기
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
JWT의 구조 중 Payload 부분에는 토큰에 담긴 정보가 들어있다. 여기에 담긴 정보의 한 '조각'을 클레임(claim)이라고 부르고, 이는 Key-value의 한 쌍으로 이루어져 있다. 토큰에는 여러 개의 클레임들을 넣을 수 있음.
Jwts.parserBuilder( )와 secret key를 사용하여 JWT의 Claims를 가져와 담겨 있는 사용자 정보 사용
JWT 테스트
@GetMapping("/create-jwt")
public String createJwt(HttpServletResponse res) {
// Jwt 생성
String token = jwtUtil.createToken("Terry", UserRoleEnum.USER);
// Jwt 쿠키 저장
jwtUtil.addJwtToCookie(token, res);
return "createJwt : " + token;
}
@GetMapping("/get-jwt")
public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
// JWT 토큰 substring
String token = jwtUtil.substringToken(tokenValue);
// 토큰 검증
if(!jwtUtil.validateToken(token)){
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 사용자 정보 가져오기
Claims info = jwtUtil.getUserInfoFromToken(token);
// 사용자 username
String username = info.getSubject();
System.out.println("username = " + username);
// 사용자 권한
String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
System.out.println("authority = " + authority);
return "getJwt : " + username + ", " + authority;
}
'Spring' 카테고리의 다른 글
Spring Security 프레임워크 이해하기 (0) | 2024.08.16 |
---|---|
Spring의 Filter 이해하기 및 구현하기 (0) | 2024.08.16 |
Spring의 JPA와 Entity 이해하기 (0) | 2024.08.07 |
Spring의 IoC(제어의 역전), DI(의존성 주입) 이해하기 (0) | 2024.08.06 |
Spring을 3 Layer Architecture로 역할 분리하기 (0) | 2024.08.06 |