JWT 방식 사용자 인증

JWT 란?

🍪 JWT(Json Web Token)이란 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이다. 즉 토큰의 한 종류라고 생각하시면 됩니다. 보통 쿠키 저장소에 담기기에 ‘저장된 쿠키’라고 생각하면 쉽다.

 

왜 JWT를 사용하는가 ? 

 

다음과 같이 서버가 1대로 운영될 경우 1개의 저장소를 통하여 모든 Client  로그인 정보 소유


트래픽 증가로 인하여 서버를 확장할 경우 각각의 서버마다 저장공간이 필요하게된다. 

이때, 회원이 접속한 서버로 다시 접속하지 않을 경우 로그인 상태를 보장할 수 없게 된다.

위와 같은 문제를 해결하기 위하여

1. Sticky Session : Client마다 요청 Server 고정

2. 세션 저장소 생성

 


 

다음과 같이 Session storage를 통하여 모든 Client 의 로그인 정보 소유

 


JWT 방식

 

로그인 정보를 Server 에 저장하지 않고, Client 에 로그인정보를 JWT 로 암호화하여 저장

→ JWT 통해 인증/인가

 

모든 서버에서 동일한 Secret Key 소유

Secret Key 통한 암호화 / 위조 검증 (복호화 시)

 

 

JWT 장/단점 ? 

장점1. 동시 접속자가 많을 때 Client 쪽에 저장을 하기에 서버 측 부하를 줄일 수 있다.2.Client, Sever 가 다른 도메인을 사용할 때ex)카카오 OAuth2 로그인 시 JWT Token 사용

 

단점1. 구현의 복잡도 증가2. JWT에 담는 내용이 커질수록 네트워크 비용 증가(클라이언트 -> 서버)3. 생성된 JWT를 일부만 만료시킬 방법이 없음4. Secret 키를 통하여 암호화/위조검증이 이루어지기에 유출 시 JWT 조작 위험

 

JWT  사용 흐름 ? 

1. Client가 로그인 성공 시 로그인 정보로 암호화(Secret key를 사용)한 JWT 발행

 

발급된 JWT 예시

2. JWT Client에게 응답(방식은  개발자가 정하면 된다.)

3. Client 에서 JWT 저장 (쿠키, Local storage 등)

4. JWT 인증이 필요한 요청에 대해서 정보를 클라이언트에서 요청 메세지에 넣어 전송

5. 받은 토큰을 위조 여부 검증을 통하여 사용자 인증

 

JWT  구성 ? 

Header : 
{
   "alg": "HS256",
   "typ": "JWT"
}

payload :  
{
    사용자 인증 관련 정보들 -> JWT같은 경우 Secret 키 유출로 인하여 언제든지 복호화가 가능하기에 중요한 정보는 넣지 말자
}

Signature
HMACSHA256(
        base64UrlEncode(header) + "." +
        base64UrlEncode(payload),
        secret)

 

JWT  구현? 

 

 

[JWT dependency 추가]

compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

 

[application.properties] : secret key 지정 -> secret key는 정하면 된다.


jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZz
rgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=

 

Jwt를 위한 별도의 클래스 생성

package com.sparta.myselectshop.jwt;


import com.sparta.myselectshop.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String AUTHORIZATION_KEY = "auth";
    private static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L;

    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // header 토큰을 가져오기
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null;
    }

    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username)
                        .claim(AUTHORIZATION_KEY, role)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date)
                        .signWith(key, signatureAlgorithm)
                        .compact();
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

}