
해당 포스트는 https://www.youtube.com/watch?v=36lpDzQzVXs 를 보고 정리한 내용입니다 생활코딩

header : JWT 의 여러 정보가 있는 공간, Signature 를 만드는 방법에는 여러 방법(알고리즘) 이 존재하는데, 그 알고리즘의 정보를 저장한다.Payload : 실제 JWT 의 내용(본문) 이 포함된 공간.Payload 의 각 부분을 Claim 이라고 하는데, Claim(주장) 이라고 부르는 이유는 Payload 를 순순히 정보로 받는 것이 아닌(주장) 서버가 발행해준 JWT 가 맞는지 검증 을 거처야하기 때문이다.Signature : 서버가 발행 해준 JWT 가 맞는지, 위조되지 않았는지 검증을 하기위한 서명(Signature) 을 포함한다.
이 3요소(header, Payload, Signature)를 base64 로 encoding 을 한후 .(마침표) 로 연결시켜주면 우리가 아는 JWT 가 된다
먼저, 서명이 오면 서명을 받는 쪽에서 확인할수있는 무언가(key)가 필요하다.
서버에서는 이것을 secret key라는 이름으로 가지고 있는다.
클라이언트에게서 login (Authentication) 요청이 들어오면, 애플리케이션 서버는
서명을 만드는 함수(HMAC) 에다가 Payload , secret Key (서버가 가지고있는) 그에맞는 서명(signature) 을 리턴한다.

header 에 있는 HMAC 알고리즘 정보로 HMAC 함수를 만들어서 서버가 가지고 있는 secret Key 값과, Payload 에 적용시켜서 리턴된 signature 와, JWT 의 토큰안에 들어있는 signature 를 비교해서
클라이언트(Browser) 가 login 시도를 한다, 서버는 DB 에 Id, password 와 일치하는 회원이 있는지 확인한다.
있으면, 서버의 Secret Key, Payload ,header 의 알고리즘 (HS256)을 통해 signature 를 만들고, Base64 로 인코딩 한후 header.payload.signature
로 JWT 를 만든다

서버는 발급한 JWT를 응답으로 클라이언트에 전송한다.
(전송 방법은 1) 쿠키를 통한 전송, 또는 2) HTTP 메시지 헤더의 Authorization 필드를 사용하는 방식이 있다.)
클라이언트는 받은 JWT를 브라우저의 쿠키 저장소나 sessionStorage에 저장하고,
이후 요청마다 이 토큰을 함께 전송함으로써 로그인 상태(Stateful) 를 유지한다.

login 후, 브라우저가 요청을 보내면, 요청에 JWT 를 담아서 보내게 된다.

서버가 요청을 받으면 JWT 를 꺼낸후, JWT 의 signature 와 Payload 와 secret key 를 통해 만든 signature 를 통해 검증한다. 검증에 통과가 되면 서버가 클라이언트가 보낸 요청에 대한 처리를 해줘서 응답으로 보내게 된다.

JWT 를 발급받은 후(인가)에선JWT 를 쿠키안에 넣어서 인증(Authentication) 처리를 해보자
AUTHORIZATION_HEADER : 쿠키의 name 값 (name 을 key 형식으로 사용해서 value 를 꺼내온다)
AUTHORIZATION_KEY: 사용자 권한 값을 가지고 오기 위한 keyBEARER_PREFIX : 토큰 식별자. 토큰 앞에는 BEARER(공백) 을 붙이는것이 관례이다.
TOKEN_TIME : 토큰 만료 시간secretKey : 서버가 가지고 있는 secretKeysignatureAlgorithm : 암호화 알고리즘. 요즘은 HS256 를 대부분 사용한다.createToken : username, role(권한) 과 세팅해준 상수값과 jwts.bulider() 를 통해 JWT 토큰을 만든다.addJwtToCookie : 생성해준 JWT 를 응답 메세지의 Cookie 에 담아주는 메서드validateToken : io.jsonwebtoken.Jwts 에서 제공해주는 메서드들로 토큰이 위조된건지, 만료되었는지 알수있다package com.example.springauth.jwt;
import io.jsonwebtoken.*;
import com.example.springauth.Entity.UserRoleEnum;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
/**
* Util 는 특정 파라메터에 대한 작업을 수행하는 메서드들을 모아둔 클래스.
* 다른 객체에 의존하지 않는다.
* ex) String $ 로 반환
*/
@Component
public class JwtUtil {
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";//쿠키의 name 값
// 사용자 권한 값의 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분
/**
* application.properties 에서 설정한 key 값으로 SecretKey 의 value 를 가지고 온다
*/
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private Key key; //@PostConstruct 에서 초기화
//암호화 알고리즘
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); //Base64 로 인코딩 되어 있던 문자열을 다시 원래의 바이트 배열로 복원하는 과정(이게 진짜 secretKey 의 원본) ex 우리가 원래 byte 배열을 etf-8 로 encoding 해서 한국어로 만드는것처럼
}
//여기까지 데이터 준비코드
/**
* 이 코드를 실행하면
* JWT 의
* header, payload, signature 를 알잘딱 만들어주고, JWT 를 만들어준다.
* signWith() 로 signature 로 만드려면 byte 코드로 디코딩 되어 있어야 한다
*/
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) //signature 자동 생성
.compact(); //리턴값은 String (최종 JWT 문자열 완성 코드)
}
// 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 토큰의(String) 에서 Bearer_ 만큼을 제외한 SubString 이 필요하다
*/
// JWT 토큰 substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { //null 인지 아닌지, BEARER_PREFIX로 시작하는지 안하는지
return tokenValue.substring(7);
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
// 토큰 검증
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;
}
// 토큰에서 사용자 정보 가져오기 Payload 가지고 오기
public Claims getUserInfoFromToken(String token) {//정보를 찾아오려면 시큐리티 키값이 필요함
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
// HttpServletRequest 에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
Cookie[] cookies = req.getCookies(); //하면 Cookie[] 를 반환한다.
if(cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}
return null;
}
}
/**
* 시크릿 키는 서버에 존재해야 한다
* 시그니쳐는 header+payload+secretKey 의 3개를 디코딩(바이트 코드로) 한후, 특정 시그니처 알고리즘으로 조합해서 사용한다
* 이것으로 토큰의 위조 유무를 파악할수 있다.
*/