본문 바로가기
Learning-log/Spring & JPA

[Spring Boot, 로그인] JWT란? JWT를 활용해서 Spring Boot에서 로그인 구현하기 ( JWT 회원정보 추출 커스텀 애노테이션 )

by why제곱 2023. 9. 14.

JWT

  • 인증에 필요한 정보들을 암호화시킨 JSON 토큰. 유저를 인증하고 식별하기 위한 Token 기반 인증
  • JSON 데이터를 Base64 URL-safe Encode를 통해 인코딩하여 직렬화
  • 토큰 내부에는 개인키를 통한 전자서명이 들어있음
  • 토큰 기반 인증
    • 토큰 자체에 사용자들의 정보들이 포함 ⇒ Stateless하게 설계 가능
    • Stateless(무상태성) 란?
      • 서버가 클라이언트의 상태를 보존하지 않음. 오직 클라이언트의 요청에 대한 응답만 주는 것.
      • HTTP통신은 무상태성을 지향. 그래야 서버를 무한 확장 가능하기 때문
      • 고객이나 요청이 갑자기 증가할 때 서버 추가가 쉬워짐
      • Stateful 하다면? 서버 하나 고장시 서버와 연결된 클라이언트가 기존 작업을 처음부터 다시 해야 하는 상황 발생(결제 튕기면 처음부터 다시!)
      • Stateless 하다면 ? 클라이언트가 고객의 작업정보를 보관하므로 한 서버가 고장나도 다른 서버에 같은 요청을 얼마든지 처리 가능
      • 단점 : 클라이언트가 서버에 요청할 때 전송할 정보가 늘어남 → 요청, 응답 시 오가는 payload가 커지므로 모든 경우를 무상태성으로 설계하지는 않음

JWT 토큰의 구조

  • Header : 토큰의 타입, 전자 서명시 사용한 알고리즘 저장
  • Payload : 보통 Claim이라는 토큰에서 사용할 정보들이 담김. 인증 시, 토큰에서 실제로 사용될 정보들. 개발자가 어떤 Claim을 넣을지 정한 후 마음대로 넣을 수 있음
    • 표준 스펙에 정의된 7가지 Claim
      • iss(Issuer) : 토큰 발급자
      • sub(Subjec) : 토큰 제목 - 토큰에서 사용자에 대한 식별값이 된다.
      • aud(Audience) : 토큰 대상자
      • exp(Expiration Time) : 토큰 만료 시간
      • nbf(Not Before) : 토큰 활성 날짜 (이 날짜 이전의 토큰은 활성화 되지 않음을 보장)
      • iat(Issued At) : 토큰 발급 시간
      • jti(JWT Id) : JWT 토큰 식별자 (issuer가 여러 명일 때 구분하기 위한 값)
    • 필요하다면 개발자가 추가로 작성해도 문제 없지만, payload는 암호화가 되지 않기 때문에 식별을 위한 정보만 저장해둘 것.
  • Signature :암호화 되어 있음.
    • Decoding을 해도 실제 서명부가 나오지 않고 암호화된 구조만 나타남
    • 서버가 가지고 있는 개인키를 통해 암호화되어 있기 때문에 외부에서 Signature를 복호화 불가

 

  • AccessToken & RefreshToken
    • 해커가 AccessToken 탈취시 모든 접근이 가능해지므로 유효기간을 짧게 하기.
    • RefreshToken을 이용해 AccessToken을 재발급 ⇒ 짧아진 유효기간 문제 해결
    • RefreshToken은 클라이언트가 아닌 서버DB에 저장되기 때문에 해커 탈취 위험이 적음
    • RefreshToken의 만료기간은 보통 2주로 많이 잡는다고 함.

 

  • JWT 설정 파일 : application-jwt.yml
    • 이 속성에서 정한 값은 아래와 같이 값을 넣어서 JwtService에서 사용
jwt:
  secretKey: //시크릿키 적으면됨
//base64로 인코딩된 암호 키, HS512를 사용할 것이기 때문에, 512비트(64바이트) 이상이 되어야 함.   
    
  access:
    expiration: 3600000 //1시간(60분) (1000L(ms -> s) * 60L(s -> m) * 60L(m -> h))
    header: Authorization

  refresh:
    expiration: 1209600000 //(1000L(ms -> s) * 60L(s -> m) * 60L(m -> h) * 24L(h -> 하루) * 14(2주))
    header: Authorization-refresh

 

  • jwt: secretKey: //base64로 인코딩된 암호 키, HS512를 사용할 것이기 때문에, // 512비트(64바이트) 이상이 되어야 합니다. 영숫자 조합으로 아무렇게나 길게 써주세요! access: expiration: 3600000 //1시간(60분) (1000L(ms -> s) * 60L(s -> m) * 60L(m -> h)) header: Authorization refresh: expiration: 1209600000 //(1000L(ms -> s) * 60L(s -> m) * 60L(m -> h) * 24L(h -> 하루) * 14(2주)) header: Authorization-refresh
  • JWT 주요 라이브러리 차이
    • okta의 jjwt 라이브러리
      • 별도의 decode 메서드 지원 x
      • JWT 생성 : Jwts.builder()
      • JWT 검증 : JWT.parserBuilder() 메서드 사용
      • JWT 클레임 추가 : addClaims() 사용 또는 set~~() 메서드 사용
      • 예시코드(jwt 생성)
      /**
         * JWT TokenDto 생성
         **/
    
        public TokenDto createTokenDto(Long userId) {
            Optional<User> user = userRepository.findByUserId(userId);
            if (user.isEmpty()) throw new NoSuchElementException(NoSuchElementException.NO_SUCH_USER);
    
            Date now = new Date();
    
            String accessToken = JWT.create()
                    .withSubject(ACCESS_TOKEN_SUBJECT)
                    .withIssuedAt(now)
                    .withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod))
                    .withClaim(USER_ID_CLAIM, userId)
                    .sign(Algorithm.HMAC512(secretKey));
    
            String refreshToken = JWT.create()
                    .withSubject(REFRESH_TOKEN_SUBJECT)
                    .withIssuedAt(now)
                    .withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod))
                    .withClaim(USER_ID_CLAIM, user.get().getUserId())
                    .withClaim(USER_EMAIL_CLAIM, user.get().getEmail())
                    .sign(Algorithm.HMAC512(secretKey));
    
    
            //redis에 refreshToken 객체 생성하여 저장
            RefreshTokenDto refreshTokenDto = RefreshTokenDto.builder().
                    email(user.get().getEmail()).
                    refreshToken(refreshToken).build();
    
            tokenRepository.saveRefreshToken(refreshTokenDto);
    
            log.info("Access Token 발급 완료 : {}", accessToken);
            log.info("Refresh Token 발급 완료 : {}", refreshToken);
            return TokenDto.builder().accessToken(accessToken).refreshToken(refreshToken).build();
    
    
        }
    • auth0의 java-jwt라이브러리
      • 사용 쉽고 가독성 높음
      • Base64로 encoding된 값을 decoding할 수 있는 JWT.decode() 메서드 지원
      • JWT 생성 : JWT.create()
      • JWT 검증 : JWT.require()
      • JWT 클레임 추가 : withClaim() 또는 with~~~() 사용
      • 예시코드(jwt생성)
      // Jwt 토큰 생성
          public TokenDto createToken(String userPk, List<String> role) {
              //claims에 회원을 구분할 수 있는 값을 세팅
              Claims claims = Jwts.claims().setSubject(userPk);
      					// 사용자 구분을 위해 pk넣어줌
              claims.put("role", role);
              Date now = new Date();
      
              String accessToken =  Jwts.builder() //JwtBuilder객체 생성
                      .setClaims(claims) // 데이터
                      .setIssuedAt(now) // 토큰 발행일자
                      .setExpiration(new Date(now.getTime() + accessTokenValidMillisecond)) // 토큰 유효시간 설정
                      .signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘, 암호키
                      .compact(); //압축 및 서명
      
              String refreshToken =  Jwts.builder()
                      .setClaims(claims) // 정보 저장
                      .setIssuedAt(now) // 토큰 발행 시간 정보
                      .setExpiration(new Date(now.getTime() + refreshTokenValidMillisecond)) // set Expire Time
                      .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                      // signature 에 들어갈 secret값 세팅
                      .compact();
      
              return TokenDto.builder().accessToken(accessToken).refreshToken(refreshToken).key(userPk).build();
          }
      
  • JWT 관련 클래스 생성
    • JwtService
package dream.security.jwt.service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import dream.common.exception.InvalidAccessTokenException;
import dream.common.exception.InvalidRefreshTokenException;
import dream.common.exception.NoSuchElementException;
import dream.security.jwt.dto.RefreshTokenDto;
import dream.security.jwt.dto.TokenDto;
import dream.security.jwt.domain.TokenRepository;
import dream.user.domain.User;
import dream.user.domain.UserRepository;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Getter
@Slf4j
public class JwtService {

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

    @Value("${jwt.access.expiration}")
    private Long accessTokenExpirationPeriod;

    @Value("${jwt.refresh.expiration}")
    private Long refreshTokenExpirationPeriod;

    @Value("${jwt.access.header}")
    private String accessHeader;

    @Value("${jwt.refresh.header}")
    private String refreshHeader;


    private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
    private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
    private static final String USER_ID_CLAIM = "userId";
    private static final String BEARER = "Bearer ";
    private static final String USER_EMAIL_CLAIM = "email";

    private final RedisTemplate redisTemplate;
    private final TokenRepository tokenRepository;
    private final UserRepository userRepository;

    /**
     * JWT TokenDto 생성
     **/

    public TokenDto createTokenDto(Long userId) {
        Optional<User> user = userRepository.findByUserId(userId);
        if (user.isEmpty()) throw new NoSuchElementException(NoSuchElementException.NO_SUCH_USER);

        Date now = new Date();

        String accessToken = JWT.create()
                .withSubject(ACCESS_TOKEN_SUBJECT)
                .withIssuedAt(now)
                .withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod))
                .withClaim(USER_ID_CLAIM, userId)
                .sign(Algorithm.HMAC512(secretKey));

        String refreshToken = JWT.create()
                .withSubject(REFRESH_TOKEN_SUBJECT)
                .withIssuedAt(now)
                .withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod))
                .withClaim(USER_ID_CLAIM, user.get().getUserId())
                .withClaim(USER_EMAIL_CLAIM, user.get().getEmail())
                .sign(Algorithm.HMAC512(secretKey));


        //redis에 refreshToken 객체 생성하여 저장
        RefreshTokenDto refreshTokenDto = RefreshTokenDto.builder().
                email(user.get().getEmail()).
                refreshToken(refreshToken).build();

        tokenRepository.saveRefreshToken(refreshTokenDto);

        log.info("Access Token 발급 완료 : {}", accessToken);
        log.info("Refresh Token 발급 완료 : {}", refreshToken);
        return TokenDto.builder().accessToken(accessToken).refreshToken(refreshToken).build();


    }

    /**
     * refreshToken을 redis에서 삭제
     */
    public void removeRefreshToken(String refreshToken) {

        Optional<String> email = extractEmailFromRefreshToken(refreshToken);
        if (email.isEmpty()) throw new NoSuchElementException(NoSuchElementException.NO_SUCH_EMAIL_IN_REFRESH_TOKEN);
        tokenRepository.findByEmail(email.get())

                .ifPresent(refreshTokenDto -> {
                    tokenRepository.deleteByEmail(email.get());
                    log.info("Refresh Token 삭제 : {} ", refreshToken);


                });
    }

    /**
     * Token 헤더에 넣어 보내기
     */

    public void sendTokenDto(HttpServletResponse response, TokenDto tokenDto) {

        response.setStatus(HttpServletResponse.SC_OK);

        response.setHeader(accessHeader, BEARER+tokenDto.getAccessToken());
        response.setHeader(refreshHeader, BEARER+tokenDto.getRefreshToken());

        log.info("Access Token, Refresh Token 헤더 설정 완료");

    }
    /**
    * 토큰에서 userId 정보 꺼내기
     */

    public Optional<Long> extractUserIdFromAccessToken(String accessToken) {
        log.info("token in extractUserIdFromAccessToken : {}", accessToken);
        try {
            Optional<Long> userId = Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
                    .build().verify(accessToken.replace(BEARER, ""))
                    .getClaim(USER_ID_CLAIM).asLong());
            if (userId.isEmpty()) throw new NoSuchElementException(NoSuchElementException.NO_SUCH_USERID_IN_ACCESS_TOKEN);

            return userId;
        } catch (Exception e) {
            throw new InvalidAccessTokenException(InvalidAccessTokenException.INVALID_ACCESS_TOKEN);
        }
    }
    public Optional<String> extractEmailFromRefreshToken(String refreshToken) {

        Optional<String> email = Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
                .build().verify(refreshToken.replace(BEARER, ""))
                .getClaim(USER_EMAIL_CLAIM).asString());

        if (email.isEmpty()) throw new NoSuchElementException(NoSuchElementException.NO_SUCH_EMAIL_IN_REFRESH_TOKEN);


        return email;
    }


    public Optional<Long> extractUserIdFromRefreshToken(String refreshToken) {
        Optional<Long> userId = Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
                .build().verify(refreshToken.replace(BEARER, ""))
                .getClaim(USER_ID_CLAIM).asLong());

        if (userId.isEmpty()) throw new NoSuchElementException(NoSuchElementException.NO_SUCH_USERID_IN_REFRESH_TOKEN);


        return userId;
    }




    /**
     * 헤더에서 RefreshToken 추출
     */
    public Optional<String> extractRefreshToken(HttpServletRequest request) {
        log.info("extractRefreshToken 동작");
        return Optional.ofNullable(request.getHeader(refreshHeader))
                .filter(refreshToken -> refreshToken.startsWith(BEARER))
                .map(refreshToken -> refreshToken.replace(BEARER, ""));
    }

    /**
     * 헤더에서 AccessToken 추출
     */
    public Optional<String> extractAccessToken(HttpServletRequest request) {
        log.info("extract AccessToken");
        log.info("accessHeader Token in Header: {} ",request.getHeader(accessHeader));
        Optional<String> accessToken = Optional.ofNullable(request.getHeader(accessHeader))
                .map(token -> token.replace(BEARER, ""));
        if (accessToken.isEmpty())
            throw new NoSuchElementException(dream.common.exception.NoSuchElementException.NO_SUCH_ACCESSTOKEN_IN_HEADER);
        return accessToken;
    }
    public Long getExpiration(String accessToken) {
        Optional<Date> expiration = Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
                .build().verify(accessToken.replace(BEARER, ""))
                .getExpiresAt());
        if (expiration.isEmpty())
            throw new NoSuchElementException(NoSuchElementException.NO_SUCH_EXPIREAT_IN_ACCESS_TOKEN);
        Long now = new Date().getTime();
        return expiration.get().getTime() - now;

    }


    /**
     * Access Token 유효성 검증
     */
    public boolean isAccessTokenValid(String accessToken) {
        log.info("isAccessTokenValid 동작");

        //REDIS의 블랙리스트에 해당 AccessToken이 존재하는지 확인
        Optional<String> isLogoutOption = tokenRepository.findByKey(accessToken);
        //있다면 유효하지 않은 accessToken 예외 반환
        if (isLogoutOption.isPresent())
            throw new InvalidAccessTokenException(InvalidAccessTokenException.INVALID_ACCESS_TOKEN);

        try {
            JWT.require(Algorithm.HMAC512(secretKey)).build().verify(accessToken);
            return true;
        } catch (Exception e) {
            throw new InvalidAccessTokenException(InvalidAccessTokenException.INVALID_ACCESS_TOKEN);
        }
    }

    /**
     * Refresh Token 유효성 검증
     */
    public boolean isRefreshTokenValid(String refreshToken) {
        log.info("isRefreshTokenValid 동작");

        Optional<String> email = extractEmailFromRefreshToken(refreshToken);
        log.info("refresh에서 추출한 email : {} " , email.get());
        if (email.isEmpty()) throw new NoSuchElementException(NoSuchElementException.NO_SUCH_EMAIL_IN_REFRESH_TOKEN);
        Optional<RefreshTokenDto> savedRefreshTokenDto = tokenRepository.findByEmail(email.get());


        if (!savedRefreshTokenDto.isPresent()) {
            throw new InvalidRefreshTokenException(InvalidRefreshTokenException.INVALID_REFRESH_TOKEN);
        }

        // 여기서 JWT 라이브러리를 사용해 리프레시 토큰의 서명을 검증할 수 있습니다.
        try {
            JWT.require(Algorithm.HMAC512(secretKey)).build().verify(savedRefreshTokenDto.get().getRefreshToken());
        } catch (Exception e) {
            throw new InvalidRefreshTokenException(InvalidRefreshTokenException.INVALID_REFRESH_TOKEN);
        }

        return true;
    }
    public void saveBlackList(String accessToken) {


        if (isAccessTokenValid(accessToken)) {

            tokenRepository.saveBlackList(accessToken, getExpiration(accessToken));

        }
    }

    public boolean isTokenValid(String token) {
        try {
            JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token);
            return true;
        } catch (Exception e) {
            log.error("유효하지 않은 토큰입니다. {}", e.getMessage());
            return false;
        }
    }

}

 

  • E-mail 추출(필요한 claim 추출)
/**
 * AccessToken에서 Email 추출
 * 추출 전에 JWT.require()로 검증기 생성
 * verify로 AceessToken 검증 후
 * 유효하다면 getClaim()으로 이메일 추출
 * 유효하지 않다면 빈 Optional 객체 반환
 */
public Optional<String> extractEmail(String accessToken) {
    try {
        // 토큰 유효성 검사하는 데에 사용할 알고리즘이 있는 JWT verifier builder 반환
        return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
                .build() // 반환된 빌더로 JWT verifier 생성
                .verify(accessToken) // accessToken을 검증하고 유효하지 않다면 예외 발생
                .getClaim(EMAIL_CLAIM) // claim(Emial) 가져오기
                .asString());
    } catch (Exception e) {
        log.error("액세스 토큰이 유효하지 않습니다.");
        return Optional.empty();
    }

 

✅ accssToken에서 유저의 Email을 추출하는 메소드 extractEmail()

**JWT.require()**로 토큰 유효성을 검사하는 로직이 있는 JWT verifier builder를 반환한다.

그 후 반환된 builder를 사용하여 **.verify(accessToken)**로 Token을 검증한다.

이때, 토큰이 유효하지 않다면 예외가 발생하여 catch로 잡아 빈 값을 반환한다.

유효하다면, Token 생성 시 Claim으로 설정했던 Email Claim을 꺼내어

**.asString()**으로 String으로 변환 후 유저 Email을 반환한다.

 

  • 토큰 검증 다른 예제(jjwt)
/**
     * 토큰검증
     */
    public Claims verifyToken(String token) throws UnsupportedEncodingException{
        String secretKeyEncodeBase64 = Encoders.BASE64.encode(secretKey.getBytes());
        Claims claims = null;
        try {
            claims = Jwts.parserBuilder()
                    .setSigningKey(secretKeyEncodeBase64)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) { // 토큰 만료
            claims = null;
        }catch (Exception e) { //그외 오류
            claims = null;
        }
        return claims;
    }
  • Bearer란?
    • JWT (JSON Web Token)을 사용할 때 "Bearer"는 인증 스키마의 일종이다. HTTP에서의 "Authorization" 헤더는 다양한 타입의 인증 메커니즘을 지원하기 위해 설계되었고, "Bearer"는 그 중 하나입니다. 다른 인증 메커니즘에는 "Basic", "Digest" 등이 있다  
    • Bearer의 의미:
      • Bearer: 이 단어는 "보유자"라는 의미입니다. "Bearer" 인증 스키마는 토큰을 "보유"하고 있는 클라이언트에 대한 접근을 허용한다는 의미로 사용된다.
      Bearer를 사용하는 이유:
      1. 명확성: "Bearer"를 명시함으로써, 서버와 클라이언트는 이 토큰이 Bearer 토큰임을 명확하게 알 수 있다. 이렇게 해서 다른 종류의 인증 메커니즘이 혼동되는 것을 방지한다.
      2. 표준화: "Bearer"는 OAuth 2.0 표준에서도 정의되어 있다. 따라서, 표준을 따르는 여러 시스템과 라이브러리에서 쉽게 인식하고 처리할 수 있다.
      3. 간결성: "Bearer" 다음에 공백을 하나 두고 토큰을 붙이는 것으로 간결하고 명확한 표현이 가능하다. 이는 파싱을 쉽게 해주고, 오류의 여지를 줄여준다다.
      4. 확장성: 추후에 다른 인증 메커니즘을 도입하더라도, "Bearer" 스키마는 그대로 유지될 수 있으므로 확장성이 좋다.
      따라서, Bearer 인증은 간결하면서도 명확한 인증 메커니즘을 제공하며, OAuth 2.0과 같은 널리 사용되는 표준에서도 채택되고 있다.

 

UserId를 추출하는 애노테이션 만들기

JWT 토큰을 해석하여 사용자 정보를 추출하는 Spring Boot 어노테이션을 만들어, 백엔드 코드를 간편하게 만들어보려고 한다.

우선, **UserId**라는 이름의 어노테이션을 정의한다.

javaCopy code
// UserId.java
import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId {
}

이어서, JWT를 파싱하여 사용자 ID를 얻는 서비스를 작성합니다.

javaCopy code
// JwtService.java
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class JwtService {
    @Value("${jwt.secretKey}")
    private String secretKey;

    public String extractUserId(String token) {
        return Jwts.parser()
                   .setSigningKey(secretKey)
                   .parseClaimsJws(token.replace("Bearer ", ""))
                   .getBody()
                   .getSubject();
    }
}

그 다음에는 HandlerMethodArgumentResolver 인터페이스를 구현하여 어노테이션을 처리하는 로직을 추가합니다.

javaCopy code
// UserIdArgumentResolver.java
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {

    private final JwtService jwtService;

    public UserIdArgumentResolver(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(UserId.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        String token = request.getHeader("Authorization");
        return jwtService.extractUserId(token);
    }
}

마지막으로, 이 **HandlerMethodArgumentResolver**를 Spring에 등록합니다.

javaCopy code
// WebConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final JwtService jwtService;

    public WebConfig(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new UserIdArgumentResolver(jwtService));
    }
}

이제, 어느 컨트롤러 메소드에서든 @UserId 어노테이션을 사용하여 사용자 ID를 얻을 수 있다.

javaCopy code
// SomeController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SomeController {

    @GetMapping("/someEndpoint")
    public String someEndpoint(@UserId String userId) {
        return "The user ID is: " + userId;
    }
}

 

- 참고블로그

JWT 토큰과 무상태성(Stateless)

Access Token & Refresh Token

[JWT] 세션 의존성 제거하기 - 커스텀 어노테이션 활용