LUMI_dev

JWT 다루기 본문

라이브러리를 추가해야 함

build.gradle에 JWT 부분 추가 + 재빌드(코끼리)

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.2'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.sparta'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JWT
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'


    // Security
    implementation 'org.springframework.boot:spring-boot-starter-security'

    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

application.properties에 SecretKey 추가 

spring.application.name=spring-auth
jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==
  • 이유) 코드에 있는 것보다 더 안전하기 때문 + 코드 단에서 이것을 가져다가 사용하는 방법 확인 위함
  • 더 안정성을 높이기 위해 Base64로 인코딩 

 

JWTUtil 생성

 

Util이란?

특정한 매개변수 혹은 파라메터에 대한 어떤 작업을 수행하는 메서드들이 존재하는 클래스 

= 다른 객체에 의존하지 않고 하나의 모듈로서 동작하는 클래스

 

JWTUtil이란?

JWT와 관련된 기능들을 가진 Class

 

다섯가지 기능을 만들어 볼 것임

<JWT 관련 기능>
1. JWT 생성
2. 생성된 JWT를 Cookie에 저장
3. Cookie에 들어있던 JWT 토큰을 Substring
4. JWT 검증
5. JWT에서 사용자 정보 가져오기

 

첫번째 방법) 서버에서 쿠키를 만들 때 장점 (Response 객체 Header에 넣어서 반환) 

- 쿠키 자체에 어떤 만료 기한을 줄수도 있고, 다른 옵션을 추가할 수 있음

- Header에 Set-Cookie라는 이름으로 넘어가면서 자동으로 쿠키가 저장됨

- Header에 key값을 줘야하고, Value에 Token 넣음 

 

두번째 방법) Header에 넣어서 보내는 방법 (토큰에 담아서 넣는 방법) 

- 코드 수가 줄어듦 : 쿠키를 따로 만들 필요 없이 그냥 토큰 자체를 Header에다 넣으면 되므로 

 

이런 처리 방법은 협업하는 프론트엔드 개발자와 얘기해서 결정해야 함

 


실습) JwtUtil 만들기

 

1)  토큰 생성에 필요한 데이터 추가하기

JwtUtil.java

package com.sparta.springauth.jwt;

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Base64;

@Component
public class JwtUtil {
    // Header KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";

    // 사용자 권한 값의 KEY (예.어드민/회원/비회원 등 권한)  - 비번처럼 위험한 값은 아니라 JWT에 종종 넣어서 보냄
    // 권한 값도 구분하기 위해서 앞에 Key 값을 줄 수 있음 ("auth")
    public static final String AUTHORIZATION_KEY = "auth";


    // Token 식별자
    //Bearer = 토큰 앞에 붙이는 용어(규칙), 이게 앞에 있으면 이 값은 토큰입니다 
    public static final String BEARER_PREFIX = "Bearer "; //한 칸 띄우고 붙여야 함 

    // 토큰 만료시간
    private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분



    //application.properties에 설정한 Secret key 값 가져오는 법 = @Value("${jwt.secret.key}")
    @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
    private String secretKey; //Secret key가 여기 담김 
    private Key key;
    
    //SignatureAlgorithm : HS256 알고리즘
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 로그 설정
    public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

    @PostConstruct //딱 한번만 받아오면 되는 값을 사용할 때마다 요청을 새로 호출하는 실수 방지 위해 사용 
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey); //사용하기 위해 decoding하는 코드 
        key = Keys.hmacShaKeyFor(bytes); //decode한 키 값을 우리가 사용할 key에 담아주는 메서드 
    }

}


//JWT 생성

//생성된 JWT를 쿠키에 저장

//쿠키에 들어있던 JWT 토큰을 Substring

//JWT 검증

//JWT에서 사용자 정보 가져오기

 

 

application.properties에 설정한 Secret key 값 가져오는 법 = @Value("${jwt.secret.key}")

 

jwt 만드는 법 구글링 할 때 jwt 0.11.5버전으로 찾기

 

 

2) 사용자의 권한을 관리하는 inner class (UserRoleEnum) 만들기

 

 

UserRoleEnum.java

package com.sparta.springauth.entity;

public enum UserRoleEnum {


    USER(Authority.USER),  // 사용자 권한 //Authority.USER 여기에 값을 넣어줄 수 있음 - 여기 넣는 값은 생성자가 됨 
    ADMIN(Authority.ADMIN);  // 관리자 권한

    private final String authority;

    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    public String getAuthority() { //getAuthority하면 ROLE_USER,ROLE_ADMIN가져올 수 있음
        return this.authority;
    }

    public static class Authority {
        public static final String USER = "ROLE_USER";//USER은 ROLE_USER라는 이름을 갖게 됨
        public static final String ADMIN = "ROLE_ADMIN"; //ADMIN은 ROLE_ADMIN이라는 이름을 갖게 됨

    }
}

 

 

3) JWT 기능 5가지 구현 

 

 

기능 1. JWT 토큰 생성 

JwtUtil.java

//JWT 토큰 생성
        public String createToken(String username, UserRoleEnum role) { // UserRoleEnum role - UserRoleEnum의 권한 값
            Date date = new Date();

            //아래 넣을 데이터는 필요한 데이터 지정해서 넣으면 됨
            return BEARER_PREFIX + //토큰 앞에 Bearer
                    Jwts.builder()
                            .setSubject(username) // 사용자 식별자값(ID) - PK (username)
                            .claim(AUTHORIZATION_KEY, role) // 사용자 권한 .claim(key,value)
                            .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간 //date.getTime() = 현재 시간
                            .setIssuedAt(date) // 발급일
                            .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                                // ★★★ 제일 중요!! signWith(Secretkey값, signatureAlgorithm (HS256)) = 암호화 알고리즘과 key를 넣어주는 것
                                 //이걸로 인해 안에 들어간 데이터가 암호화 > Token > +Bearer 붙어서 반환
                            .compact(); //.builder ~ 쭉 데이터 넣고 ~ .compact
        }

 

기능 2. 생성된 JWT를 쿠키에 저장 

 

//생성된 JWT를 쿠키에 저장
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
                                    //쿠키의 name부분에 Header 넣기, value에는 token

        cookie.setPath("/");

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

 

기능 3.쿠키에 들어있던 JWT 토큰을 Substring

  //쿠키에 들어있던 JWT 토큰을 Substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { //hasText(tokenValue) = 공백인지, null인지 확인할 수 있음
            return tokenValue.substring(7); //Bearer 하고 공백까지 잘라낸 순수 토큰 값 반환
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

 

기능 4. JWT 검증 

//잘 substring됐는지 JWT 검증
    public boolean validateToken(String token) { //위의 substring으로 자른 순수 token을 받음
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); //토큰이 위변조가 되지 않았는지, 만료되진 않았는지 확인
                                                //암호화할 때 사용했던 key
            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; //여기까지 왔으면 문제 있다는 거니까 false 반환
    }

 

기능 5. JWT 토큰에서 사용자 정보 가져오기

//JWT 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
    return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();//.getBody(); : body 부분에 있는 Claims를 가져올 수 있음
}

 

전체 코드

더보기

JwtUtil.java

package com.sparta.springauth.jwt;

import com.sparta.springauth.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.Cookie;
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.URLEncoder;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Component
public class JwtUtil {
    // 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);
    }

    //JWT 토큰 생성
        public String createToken(String username, UserRoleEnum role) { // UserRoleEnum role - UserRoleEnum의 권한 값
            Date date = new Date();

            //아래 넣을 데이터는 필요한 데이터 지정해서 넣으면 됨
            return BEARER_PREFIX + //토큰 앞에 Bearer
                    Jwts.builder()
                            .setSubject(username) // 사용자 식별자값(ID) - PK (username)
                            .claim(AUTHORIZATION_KEY, role) // 사용자 권한 .claim(key,value)
                            .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간 //date.getTime() = 현재 시간
                            .setIssuedAt(date) // 발급일
                            .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                                // ★★★ 제일 중요!! signWith(Secretkey값, signatureAlgorithm (HS256)) = 암호화 알고리즘과 key를 넣어주는 것
                                 //이걸로 인해 안에 들어간 데이터가 암호화 > Token > +Bearer 붙어서 반환
                            .compact(); //.builder ~ 쭉 데이터 넣고 ~ .compact
        }

         //생성된 JWT를 쿠키에 저장
        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
                                            //쿠키의 name부분에 Header 넣기, value에는 token

                cookie.setPath("/");

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


    //쿠키에 들어있던 JWT 토큰을 Substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { //hasText(tokenValue) = 공백인지, null인지 확인할 수 있음
            return tokenValue.substring(7); //Bearer 하고 공백까지 잘라낸 순수 토큰 값 반환
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

    //잘 substring됐는지 JWT 검증
        public boolean validateToken(String token) { //위의 substring으로 자른 순수 token을 받음
            try {
                Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); //토큰이 위변조가 되지 않았는지, 만료되진 않았는지 확인
                                                    //암호화할 때 사용했던 key
                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; //여기까지 왔으면 문제 있다는 거니까 false 반환
        }

        //JWT 토큰에서 사용자 정보 가져오기
        public Claims getUserInfoFromToken(String token) {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();//.getBody(); : body 부분에 있는 Claims를 가져올 수 있음
        }


}

JWT 테스트 

AutoController

package com.sparta.springauth.auth;

import com.sparta.springauth.entity.UserRoleEnum;
import com.sparta.springauth.jwt.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/api")
public class AuthController {
    public static String AUTHORIZATION_HEADER = "Authorization";

    private final JwtUtil jwtUtil;

    public AuthController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    // jwt를 받아오지 못해서 jwtUtil에 빨간 줄
    //JwtUtil.java 보면 jwt를 컴포넌트(@Component) 로 지정해놓음 - Bean이니까 가져오기

    //JWT를 만드는 코드
    @GetMapping("/create-jwt")
    public String createJwt(HttpServletResponse res) {

        // Jwt 생성
        String token = jwtUtil.createToken("Robbie", UserRoleEnum.USER);//UserRoleEnum.USER 권한 = 일반 유저 

        // Jwt 쿠키 저장
        jwtUtil.addJwtToCookie(token, res); //JWT에서 만든 것 사용


        return "createJwt : " + token;
    }


    @GetMapping("/get-jwt")
    public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
                                        //토큰의 name부분이 Authorization

        // 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); //JwtUtil의  .claim(AUTHORIZATION_KEY, role) // 사용자 권한 .claim(key,value) 참고
        
        /**/
        
        System.out.println("authority = " + authority);

        return "getJwt : " + username + ", " + authority;
    }
}

 

 

실행) 

 

 

 

개발자 도구)

 

 

쿠키 value 복사해서

더보기

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJSb2JiaWUiLCJhdXRoIjoiVVNFUiIsImV4cCI6MTczODY2NjcyMSwiaWF0IjoxNzM4NjYzMTIxfQ.e2-lwubOcyMbjE80uldGNnyp2GQQgCfOAj-8OIzVGX0

 

https://jwt.io/에 넣기

 

위험한 정보 넣으면 안됨! 

 

 

get-jwt 확인