[SPRING:이론] 11 : JWT

김승수·2024년 5월 31일
0

SPRING

목록 보기
11/27

⏰ 2024. 05. 31 금

✔ 스프링 이론 강의를 듣고 정리하면서 작성했습니다.

💡 목차

  1. JWT 란?
  2. JWT 특징과 장단점
  3. JWT 사용 흐름
  4. JWT 구조
  5. JWT 다루기

JWT 란?

JWT 란?

  • JWT(Json Web Token)은 JSON 형식을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이고, 일반적으로 Web Token을 쿠키 저장소를 사용하여 JWT를 저장한다.

  • 일반적으로 JWT를 쿠키에 저장해 클라이언트에 반환하지만, JWT를 HTTP ResponseHeader에 담아 클라이언트에 반환하는 방법도 있다.

  • 로그인 정보를 Server에 저장하지 않고, Client에 로그인 정보를 JWT로 암호화하여 저장하고, 모든 서버에 동일한 Secret Key를 소유해 암호화와 위조를 검증한다.

JWT 특징과 장단점

JWT 특징

  1. 자가 수용적(Self-contained): 필요한 모든 정보를 토큰 자체에 포함하여 외부 저장소에 대한 의존성을 줄인다.
  2. 유연성(Flexibility): 클레임 정보를 자유롭게 정의할 수 있으며, 필요한 만큼의 정보를 토큰에 포함시킬 수 있다.
  3. 암호화된(Secure): 페이로드의 내용은 서명되어 있어 조작을 방지하고, 필요에 따라 암호화하여 추가 보안을 제공할 수 있다.
  4. HTTP와 웹 서비스에 적합(HTTP and Web-friendly): URL 파라미터, 요청 헤더, HTTP 요청 본문 등 다양한 방식으로 사용할 수 있다.

JWT 장점

  • 동시 접속자가 많을 때 서버 측 부하를 낮춘다.
  • Client, Server가 다른 도메인을 사용하는 경우에도 사용 가능하다.

JWT 단점

  • 구현 복잡도가 증가한다.
  • JWT에 담는 내용이 커질 수록 네트워크 비용 증가한다.
  • 기 생성된 JWT를 일부만 만료시킬 방법은 없다.(생성 시에 만료 기한은 줄 수 있다.)
  • Secret key 유출 시 JWT 조작 가능

JWT 사용 흐름

JWT 사용 흐름

클라이언트의 로그인 시, JWT를 만들어 반환하는 과정

  1. Client가 username, password를 입력해 로그인 시도

  2. DB에서 로그인 시 입력된 정보를 DB에서 확인해 로그인 성공 시

  3. 서버에서 "로그인 정보"를 가져와 Secret Key 사용해서 암호화한 JWT 발급

  4. 서버에서 직접 쿠키를 생성해 JWT를 쿠키에 담아 Client 응답에 JWT 전달
    또는, 쿠키를 생성하지 않고 ResponseHeader에 담아 Client 응답에 JWT 전달

클라이언트에서 JWT를 통해 인증 방법

  1. 클라이언트에서 데이터와 함께 API 요청 시, 쿠키ResponseHeader에 포함된 JWT를 가져오기

  2. 클라이언트에서 전달된 Secret Key를 사용해 JWT의 위조 여부와 유효기간을 검증

  3. 검증 성공 시, JWT에서 사용자 정보를 가져와 확인하고 요청에 맞는 응답을 클라이언트에 전달

JWT 구조

  • 암호화된 JWT는 jwt.io 홈페이지를 통해 누구나 평문으로 복호화 가능해 JWT의 정보를 확인할 수 있다.
  • 하지만 Secret Key가 없으면 JWT를 사용하고 수정하는 행위는 불가능하다.
  • 결국 JWT는 Read only 데이터이다.

JWT 구조

  • JWT는 크게 세 부분 Header, Payload, Signature로 구성됩니다.

  1. Header

    • 토큰의 유형(타입)해싱 알고리즘을 지정
  2. Payload

    • 실제 유저의 정보가 들어있는 클레임(claim)을 포함하며, 클라이언트와 서버 간에 공유되는 정보
  3. Signature

    • HeaderPayload를 인코딩한 후 비밀 키를 사용하여 서명된 값으로, 토큰의 유효성을 검사하는 데 사용

JWT 다루기

JWT 다루기

  • JWT를 실제 서버에서 사용하기 위해서는 JWT을 만들고, 저장해 전달하고, 가져오고 하는 기능들을 하는 JwtUtil(= JwtProvider) 클래스를 만들어 사용한다.

⚡ Util 클래스

  • 특정 매개 변수(파라미터)에 대한 작업을 수행하는 메서드들이 존재하는 클래스로, 쉽게 말하면 다른 객체에 의존하지 않고 하나의 모듈로서 동작하는 클래스를 의미한다.

  • 만약 JWT 관련 특정 기능들만 모아놓은 Util 클래스를 JwtUtil 클래스라고 명명하고, 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; // JWT 암호화 알고리즘

	@PostConstruct
	public void init() {
   		byte[] bytes = Base64.getDecoder().decode(secretKey);
    	key = Keys.hmacShaKeyFor(bytes);
	}
    
  • @Value를 통해 properties에 작성해둔 Secret Key를 가져온다.
  • Secret Key는 인코딩 되어있기 때문에 디코딩해서 Key 객체로 담아야 한다.
  • Key 객체는 딱 한번만 받아와야 하는 값이기 때문에 사용할 때마다 여러번 호출되는 것을 방지하기 위한 방법으로@PostConstruct을 사용해 값을 주입한다.
  • Bearer은 JWT 또는 OAuth 토큰이라는 것을 표시하는데 사용한다.

JwtUtil 클래스에 구현해야 하는 기능들

  1. JWT를 생성하는 기능
  2. 생성된 JWT를 쿠키나 헤더에 저장하는 기능
  3. 쿠키나 헤더에 들어있던 JWT를 가져오기
  4. 가져온 JWT 토큰 Substring하는 기능
  5. JWT를 검증하는 기능
  6. JWT에서 사용자 정보를 가져오는 기능

1. 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에 사용자의 정보를 담기 위해 userID를 넣는다.
  • JWT에 사용자 권한 정보를 키, 벨류 형식으로 넣는다.
  • 토큰 만료 시간을 넣는다.
  • issuedAt에 발급일을 넣는다.
  • signWithSecret Key 값을 담고있는 key와 암호화 알고리즘을 값을 넣는다.
  • compact을 하면 JWT 토큰이 String 타입으로 생성된다.

2. 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("/");

        res.addCookie(cookie); // Response 객체에 Cookie 추가
    } catch (UnsupportedEncodingException e) {
        logger.error(e.getMessage());
    }
}

3. 쿠키(헤더)에 들어있던 JWT를 가져오기

// HttpServletRequest 에서 Cookie Value : JWT 가져오기
    public String getTokenFromRequest(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        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;
    }

4. 받아온 쿠키(헤더)의 Value인 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을 잘라낸다.

5. JWT를 검증하는 기능

// 토큰 검증
public boolean validateToken(String token) {
    try {
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        return true;
    } catch (SecurityException | MalformedJwtException | SignatureException e) {
        log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
    } catch (ExpiredJwtException e) {
        log.error("Expired JWT token, 만료된 JWT token 입니다.");
    } catch (UnsupportedJwtException e) {
        log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
    } catch (IllegalArgumentException e) {
        log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
    }
    return false;
}
  • Jwts.parserBuilder() 를 사용하여 JWT를 파싱해 검증에 사용한다.
  • JWT가 위변조되지 않았는지 setSigningKey(key)를 통해 확인한다.

6. JWT에서 사용자 정보를 가져오는 기능

// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
    return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
  • JWT의 구조 중 Payload 부분에는 토큰에 담긴 정보가 들어있다.
  • 담긴 정보는 클레임(Claims)타입으로 되어 있고, 이는 key-value 의 한 쌍으로 이뤄져있다.
  • 이 기능을 통해 받아온 claims 에서 getSubject() 메서드를 통해, JWT 생성 시 setSubject로 넣은 username을 가져와 JPA을 통해 유저 정보를 얻을 수 있다.
profile
개발하는 미어캣

0개의 댓글