JWT란?

2024. 7. 9. 19:05·프로그래밍 일기/Java & Spring
반응형

JWT 란

JSOM Web Token의 약자로 json 데이터가 담겨있는 토큰을 말한다.

JWT 구조

Header 헤더
alg : 암호화 알고리즘
typ : 토큰 유형

Payload 정보
sub : 제목
iat : 발급시간

Signature 서명

base64UrlEncode(
	HMACSHA256(
    	base64UrlEncode(header) +
        base64UrlEncode(payload) +
        secretKey
    )
)

 

세션 VS JWT

세션은 데이터의 일부를 서버가 저장하고 나머지 데이터를 클라이언트에게 보낸다. 이후 요청이 올 때 마다 세션을 검사하여 요청에 응답한다. 그러나 트래픽이 증가하면, 서버에 부담이 많이 가기 때문에 JWT와 같은 인증방식이 등장했다.

JWT는 인증에 성공한 후 전달받은 토큰을 클라이언트가 보관하고 있으며, API 요청시 토큰을 함께 보낸다.
서버 측에서는 secretKey를 이용하여 토큰을 디코딩하여 유효성을 검사한다.

JWT 장단점

장점
- 데이터 위변조가 어렵다.
- 별도의 저장소가 필요하지 않다.
- 확장성이 우수하다.

단점
- 정보에 중요한 정보를 담을 수 없다.
- 토큰 탈취 시 대처가 어렵다.

Refresh 토큰의 등장

하나의 토큰만 발급하는 것도 문제는 없으나 토큰이 만료되면 다시 한 번 로그인해야하는 번거로움이 생긴다.
이를 해결하기 위해 기존 토큰보다 만료시간이 좀 더 긴 토큰인 Refresh토큰이 생겨났다.
기존 토큰(보통 AccessToken이라 불린다.)이 만료되면 Refresh토큰을 통해 다시 Access토큰과 Refresh토큰을 요청받는다.
이를 통해 사용자 경험 저하를 방지할 수 있다.

Refresh 토큰 + Redis

Redis에 Refresh 토큰을 저장하여 탈취를 막을 뿐만 아니라 다중 로그인 제어와 같은 기능을 구현할 수 있다.
해당 방법은 세션과 유사하나 세션에 비해 Redis가 서버에 거는 부담이 줄어들어 이와 같은 방법을 사용한다.
따라서 자신의 프로젝트에 맞는 인증 방식을 채택해야 한다. 세션은 구시대적 방식이 아니다.

토큰 위변조 파악 로직

package com.synergy.synergyback.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    public String generateToken(String email, String userType, String nickname, long expirationTime) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("email", email);
        claims.put("userType", userType);
        claims.put("nickname", nickname);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(email)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expirationTime))
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }

    public Boolean validateToken(String token, String email, String userType) {
        Map tokenData = getTokenData(token);
        final String tokenEmail = (String) tokenData.get("email");
        return (tokenEmail.equals(email) && !isTokenExpired(token));
    }

    public Map getTokenData(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();

        String userType = claims.get("userType", String.class);
        String email = claims.get("email", String.class);
        String nickname = claims.get("nickname", String.class);

        Map<String, String> tokenData = new HashMap<>();
        tokenData.put("code", "200");
        tokenData.put("userType", userType);
        tokenData.put("email", email);
        tokenData.put("nickname", nickname);

        return tokenData;
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private Date getExpirationDateFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        return claims.getExpiration();
    }
}

토큰 유효성 검사 로직 적용 위치

# java

package com.synergy.synergyback.config.interceptor;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class TokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // 요청 경로 확인
        String requestURI = request.getRequestURI();

        // /manage/** 경로에 대해서만 처리
        if (requestURI.startsWith("/manage/")) {
            // 쿠키에서 reroll_AT 값 확인
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if ("reroll_AT".equals(cookie.getName())) {
                        // reroll_AT 쿠키 값 확인
                        String rerollAtValue = cookie.getValue();
                        // userType이 admin인지 확인
                        if ("admin".equals(getUserTypeFromToken(rerollAtValue))) {
                            // admin 권한이면 요청 허용
                            return true;
                        }
                    }
                }
            }

            // userType이 admin이 아니면 요청 거부
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Admin access only");
            return false;
        }

        // /manage/** 경로가 아닌 경우에는 인터셉터 적용하지 않음
        return true;
    }

    // reroll_AT 쿠키에서 userType 추출하는 메서드 (실제 구현은 프로젝트의 인증/인가 로직에 따라 다름)
    private String getUserTypeFromToken(String rerollAtValue) {
        // 예시 구현
        // 실제로는 JWT 디코딩 또는 다른 인증 방식을 통해 userType 추출 필요
        return "admin"; // 예시로 admin을 리턴하도록 설정
    }
}
# nuxt

// middleware/auth.global.js

export default defineNuxtRouteMiddleware(async (to, from) => {
    const config = useRuntimeConfig();
    const { $auth } = useNuxtApp();
    const rerollAT = useCookie('reroll_AT');
    const rerollRT = useCookie('reroll_RT');

    if (!rerollAT?.value) {
        if (rerollRT?.value) {
            const response = await $fetch('/token/refresh', {
                baseURL: config.public.baseURL,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                }
            });

            if (response.code === '200') {
                document.cookie = `reroll_AT=${response.accessToken}; SameSite=None; Secure; Max-Age=10800; Path=/`;
                document.cookie = `reroll_RT=${response.refreshToken}; SameSite=None; Secure; Max-Age=36000; Path=/`;
            } else {
                $auth.dataRefresh();
            }
        } else {
            $auth.dataRefresh();
        }
    }

    if (to.path.startsWith('/manage')) {
        if (!$auth.isAdmin.value) {
            return navigateTo('/login');
        }
    }
});
반응형
저작자표시 (새창열림)

'프로그래밍 일기 > Java & Spring' 카테고리의 다른 글

Spring Boot Nuxt3 카카오 로그인 및 JWT 토큰 발급  (0) 2024.07.22
자바 스프링 메일 안에 이미지 추가하는 방법  (0) 2023.07.18
[에러해결] javax.mail.MessagingException: Could not connect to SMTP host: smtp.worksmobile.com, port: 465, response: -1 (isSSL true 설정)  (0) 2023.07.18
[Spring Boot] 파일 다운로드 로직 구현 feat.Nuxt3  (0) 2023.07.12
[Spring Boot] 단일, 다중 파일 업로드 로직 구현 feat.Nuxt3  (0) 2023.07.12
'프로그래밍 일기/Java & Spring' 카테고리의 다른 글
  • Spring Boot Nuxt3 카카오 로그인 및 JWT 토큰 발급
  • 자바 스프링 메일 안에 이미지 추가하는 방법
  • [에러해결] javax.mail.MessagingException: Could not connect to SMTP host: smtp.worksmobile.com, port: 465, response: -1 (isSSL true 설정)
  • [Spring Boot] 파일 다운로드 로직 구현 feat.Nuxt3
MakeMe
MakeMe
제가 포스팅한 글 중 잘못된 부분이 있으면 알려주세요!
  • MakeMe
    Developer blog
    MakeMe
    • 모든 글 (71)
      • 프로그래밍 일기 (57)
        • Java & Spring (21)
        • Python & Flask (3)
        • Linux (12)
        • Front-End (10)
        • DB & SQL (6)
        • Git (3)
        • IDE (2)
      • 자격증 (7)
        • 정보처리기능사 (2)
        • SQLD (1)
        • SW개발_L5 (1)
        • AWS (3)
      • 독립일기 (7)
        • 중소기업청년대출 (7)
  • 인기 글

  • 태그

    중소기업청년대출
    MYSQL
    Spring
    젠킨스 우분투 설치
    Vue
    자동배포설정
    nuxt3
    고용보험내역서
    넉스트
    flask 세팅
    DBeaver
    스프링
    flask
    중기청서류
    젠킨스 자동 배포
    springboot
    중기청후기
    psql
    자바
    AWS
    중기청필요서류
    건강보험자격득실확인서
    IntelliJ
    java
    nuxt
    중기청필수서류
    자바환경변수
    스프링부트
    인텔리제이
    DB
  • hELLO· Designed By정상우.v4.10.1
MakeMe
JWT란?
상단으로

티스토리툴바