'스프링 부트3 백엔드 개발자 되기' 책을 참고하며 작성 중 입니다.


사전 지식

토큰 기반 인증?

대표적인 사용자 인증 확인 방법

  1. 서버 기반 인증

    Spring Security : 세션 기반 인증

  2. 토큰 기반 인증

    Token : 서버에서 클라이언트를 구분하기 위한 유일한 값

    (서버) 토큰 제작 -> 클라이언트에게 제공  
    -> (클라이언트) 토큰을 포함한 요청 -> (서버) 토큰으로 사용자의 유효성 확인

    토큰을 전달하고 인증받는 과정

  1. 클라이언트가 아이디와 비밀번호를 서버에게 전달하면서 인증 요청
  2. 서버는 아이디와 비밀번호를 확인해 유효한 사용자인지 검증, 유효한 사용자인 경우 토큰 생성하여 응답
  3. 클라이언트는 서버에서 준 토큰 저장
  4. 인증이 필요한 API를 사용할 때 헤더에 토큰을 담아서 요청
  5. 서버는 헤더에 있는 토큰이 유효한지 검증
  6. 토큰이 유효하다면 클라이언트가 요청한 내용 처리

만약 유효하지 않다면? RefreshToken 처리 (아래 설명)

토큰 기반 인증의 특징

1. 무상태성

사용자의 인증 정보가 있는 토큰이 클라이언트에 있으므로
서버가 클라이언트의 인증 정보를 저장하거나 유지하지 않아도 된다.

완전한 무상태로 효율적인 검증

2. 확장성

서버를 확장할 때 상태 관리를 신경 쓸 필요가 없음

세션 인증 기반 : 각각 API에서 인증을 해야 하는 것과 달리
토큰 기반 인증 : 하나의 토큰으로 모든 요청 가능

카카오, 구글 로그인 같이 토큰 기반 인증을 사용하는 다른 시스템에 접근해 로그인 방식을 확장할 수도 있고, 이를 활용해 다른 서비스의 권한 공유 가능

3. 무결성

토큰 방식 : HMAC (Hash-based Message AuthentiCation) 기법
토큰을 발급하면 토큰 정보 변경 불가 (무결성)
한 글자라도 변경될 시 유효하지 않은 토큰


JWT

JWT를 이용해 인증을 하려면 HTTP 요청 헤더 중

Authorization : Bearer + JWT 토큰 값

구조

aaaa.bbbb.cccc
헤더 .내용 . 서명

헤더

토큰의 타입 , 해싱 알고리즘을 지정하는 정보 포함

"typ" : "JWT",
"alg" : "HS256"

JWT 토큰, HS256 해싱 알고리즘을 사용

내용

토큰과 관련된 내용

Claim(클레임) : 내용의 한 덩어리

등록된 클레임

이름설명
iss토큰 발급자(issuer)
sub토큰 제목(subject)
aud토큰 대상자 (audience)
exp토큰의 만료 시간(expiration), NumericDate 형식, 항상 현재 시간 이후로 설정
nbfnbf = Not Before, NumericDate 형식, 이 날짜가 지나기 전까지는 토큰이 처리되지 않음.
iat토큰이 발급된 시간으로 iat은 issued at을 의미
jtiJWT의 고유 식별자로서 주로 일회용 토큰에서 사용

공개 클레임

공개되어도 상관없는 클레임
보통 URI로 지음

비공개 클레임

공개되면 안되는 클레임
클라이언트와 서버 간의 통신에 사용

"iss" : "spring@gmail.com",
"iat" : 1622370878,
"exp" : 1622372678, //등록된 클레임
"https://spring.com/jwt_claims/is_admin" : true, //공개 클레임
"email" : "spring@gmail.com", //비공개 클레임
"hello" : "안녕하세요"  //비공개 클레임

서명

해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도
헤더의 인코딩 값 + 내용의 인코딩 값 + 비밀키 = 해시값

토큰 유효기간

토큰이 이미 발급되면 그 자체로 인증 수단이 되어 토큰이 탈취되어도 알 수 없음.

RefreshToken

사용자의 인증 용도가 아닌 액세스 토큰(AccessToken)이 만료되었을 때 새로운 액세스 토큰 발급하기 위해 사용

보통 액세스 토큰의 유효 기간을 짧게,
리프레시 토큰의 유효 기간을 길게 하여
액세스 토큰이 탈취되어도 최대한 안전하게 한다.

  1. 클라이언트가 서버에게 인증 요청
  2. 서버는 전달된 정보를 바탕으로 인증 정보가 유효한지 확인한 뒤, 액세스 토큰과 리프레시 토큰을 만들어 클라이언트에게 전달
    클라이언트는 전달받은 토큰 저장
  3. 서버에서 생성한 리프레시 토큰은 DB에도 저장
  4. 인증이 필요한 API를 호출할 때 클라이언트에 저장된 액세스 토큰과 함께 API 요청
  5. 서버는 전달받은 액세스 토큰이 유효한지 검사한 뒤에 유효하면 응답 처리
  6. 시간이 지나 만료된 액세스 토큰과 함께 API 요청
  7. 만료된 토큰이기에 토큰이 만료되었다는 에러 전달
  8. 클라이언트는 이 응답을 받고 저장해둔 리프레시 토큰과 함께 새로운 액세스 토큰을 발급하는 요청 전송
  9. 전달받은 리프레시 토큰이 유효한지,
    DB의 리프레시 토큰과 비교하여 같은지 확인
  10. 유효한 리프레시 토큰이면 새로운 액세스 토큰을 생성하여 응답
    그 이후는 다시 4로 돌아가서 진행

JWT 서비스 구현하기

의존성 추가하기

build.gradle

JWT를 사용하기 위한 라이브러리 추가
XML 문서와 자바 객체 간 매핑을 자동화하는 jax-api

dependencies {
  testAnnotationProcessor 'org.projectlombok:lombok'
  testImplementation 'org.projectlombok:lombok'
  
  implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
  implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
  implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
  //jwt를 사용하기 위해서는 위 3개의 의존성을 모두 추가 (버전 업데이트)
  implementation 'javax.xml.bind:jaxb-api:2.3.1'
}
  

토큰 제공자 추가

application.yml

jwt:
  issuer: minkyeong0244@gmail.com
  secretKey: c29tZVNlY3VyZUtleVRleHRUZXh0VGV4dFRleHRUZXh0VGV4dFRleHRUZXh0
	#HMAC-SHA 알고리즘의 보안 요구 사항 준수
study-springboot로 설정할 시 '-'를 decode할 수 없다는 오류가 뜬다.
HMAC-SHA 알고리즘의 보안 요구 사항을 준수하여 HS256으로 encode 된 값을 사용

JwtProperties

해당 값들을 변수로 접근하는 데 사용

@Setter
@Getter
@Component
@ConfigurationProperties("jwt") //해당 클래스에 프로피티값을 가져와서 사용 가능
public class JwtProperties {
   private String issuer;
   private String secretKey;
}

@ConfigurationProperties("jwt") : application.yml에서 설정한 jwt 매개변수 값들을 가져와서 사용하는 애너테이션
issuer 필드에는 jwt.issuer 값이, secretKey에는 jwt.secretKey 값이 매핑

TokenProvider

토큰 생성
올바른 토큰인지 유효성 검사
토큰에서 필요한 정보를 반환

import java.security.Key

@Service
public class TokenProvider {

   private final JwtProperties jwtProperties;

   private final Key key;

   public TokenProvider(JwtProperties jwtProperties){
       this.jwtProperties = jwtProperties;
       byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecretKey());
       this.key = Keys.hmacShaKeyFor(keyBytes);
   }

   public String generateToken(User user, Duration expiredAt){
       Date now = new Date();
       return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
   }

Java 내에서 지원하는 Key 객체를 사용
Decoders.BASE64.decode() : Base64 인코딩된 문자열을 디코딩하여 바이트 배열로 반환
Keys.hmacShaKeyFor() : HMAC SHA 알고리즘을 사용하는 데 필요한 키 객체 생성

  • 토큰 생성 메서드 makeToken()

    
      //JWT 토큰 생성 메서드
      private String makeToken(Date expiry, User user) {
          Date now = new Date();
    
          return Jwts.builder()
                  .setHeaderParam(Header.TYPE, Header.JWT_TYPE) //헤더 typ : JWT
                  .setIssuer(jwtProperties.getIssuer()) //내용 iss : yml 파일 설정 값
                  .setIssuedAt(now) //내용 iat : 현재 시간
                  .setExpiration(expiry) //내용 exp : 현재 시간 + 유효 시간
                  .setSubject(user.getEmail()) //내용 sub : 유저의 email
                  .claim("id", user.getId()) //클레임 id : 유저 ID
                  .signWith(key, SignatureAlgorithm.HS256) //서명 : 비밀값과 함께 해시값을 HS256 방식으로 암호화
                  .compact();
      }

    인자 : 만료 시간(expiry), 유저 정보(user)
    set 계열의 메서드를 통해 여러 값을 지정
    헤더 : typ(타입)
    내용 : iss(발급자), iat(발급 일시), exp(만료 일시), sub(토큰 제목)
    클레임 : 유저 ID
    비밀값과 함께 HS256 방식으로 암호화

  • 토큰이 유효한지 검증하는 메서드 validToken()

    
      //JWT 토큰 유효성 검증 메서드
      public boolean validToken(String token){
          try {
              Jwts.parserBuilder()
                      .setSigningKey(key) //비밀값으로 복호화
                      .build()
                      .parseClaimsJws(token);
              return true;
          } catch (Exception e){
              return false; //복호화 과정에서 에러가 나면 유효하지 않은 토큰
          }
      }

    key 값으로 다시 복호화를 진행
    에러가 발생하면 유효하지 않은 토큰이므로 에러 -> false

  • 토큰을 받아 인증 정보 (Authentication)을 반환하는 메서드 getAuthentication()

     
     private Claims getClaims(String token){
         return Jwts.parserBuilder() //클레임 조회
                 .setSigningKey(key)
                 .build()
                 .parseClaimsJws(token)
                 .getBody();
     }
     
     //토큰 기반으로 인증 정보를 가져오는 메서드
     public Authentication getAuthentication(String token){
         Claims claims = getClaims(token);
         Set<SimpleGrantedAuthority> authorities = Collections.singleton(
                 new SimpleGrantedAuthority("ROLE_USER")
         );
    
         return new UsernamePasswordAuthenticationToken(
                 new org.springframework.security.core.userdetails.User(
                         claims.getSubject(), "", authorities),
                 token, authorities
         );
     }

    key 값으로 복호화 진행 후 Claim을 가져오는 getClaims()을 사용
    클레임 정보를 반환받아 사용자 이메일이 들어 있는 토큰 제목 sub와
    토큰 기반으로 인증 정보 생성
    !! 여기서 UsernamePasswordAuthenticationToken객체에 들어가는 User 객체는 spring security에서 제공하는 User 객체

  • 토큰 기반으로 사용자 ID를 가져오는 메서드 getUserId()

    //토큰 기반으로 유저 ID를 가져오는 메서드
    public Long getUserId(String token){
        Claims claims = getClaims(token);
        return claims.get("id", Long.class);
    }
}

key 값으로 복호화 진행 후 Claim을 가져오는 getClaims()을 사용
클레임 정보를 반환받고 클레임에서 id 키로 저장된 값을 가져와 반환


Test

JwtFactory

JWT 토큰 서비스를 테스트하는 데 사용할 모킹(moking)용 객체
모킹 : 테스트를 실행할 때 객체를 대신하는 가짜 객체

@Getter
public class JwtFactory {
  private String subject = "test@email.com";
  private Date issuedAt = new Date();
  private Date expiration = new Date(new Date().getTime() + Duration.ofDays(14).toMillis());
  private Map<String, Object> claims = Collections.emptyMap();

  //빌더 패턴을 사용해 설정이 필요한 데이터만 선택 설정
  @Builder
  public JwtFactory(String subject, Date issuedAt, Date expiration, Map<String, Object> claims) {
      this.subject = subject != null ? subject : this.subject;
      this.issuedAt = issuedAt != null ? issuedAt : this.issuedAt;
      this.expiration = expiration != null ? expiration : this.expiration;
      this.claims = claims != null ? claims : this.claims;
  }

  public static JwtFactory withDefaultValues() {
      return JwtFactory.builder().build();
  }
	
  //jjwt 라이브러리를 사용해 JWT 토큰 생성
  public String createToken(JwtProperties jwtProperties){
      byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecretKey());
      Key key = Keys.hmacShaKeyFor(keyBytes);
      return Jwts.builder()
              .setSubject(subject)
              .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
              .setIssuer(jwtProperties.getIssuer())
              .setIssuedAt(issuedAt)
              .setExpiration(expiration)
              .addClaims(claims)
              .signWith(key)
              .compact();
  }
}

TokenProviderTest

@SpringBootTest
public class TokenProviderTest {
 @Autowired
 private TokenProvider tokenProvider;

 @Autowired
 private UserRepository userRepository;

 @Autowired
 private JwtProperties jwtProperties;

 private Key key;

 @BeforeEach
 void setUp() {
     byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecretKey());
     this.key = Keys.hmacShaKeyFor(keyBytes);
 }

 /*
 Given : 토큰에 유저 정보를 추가 하기 위한 테스트 유저 제작
 When : 토큰 제공자의 generateToken() 메서드를 호출해 토큰 제작
 Then : jjwt 라이브러리를 사용해 토큰 복호화, 토큰을 만들 때 claim으로 넣어둔 id 값이
         given 절에서 만든 유저의 id와 일치하는 지
  */

 @DisplayName("generateToken() : 유저 정보와 만료 기간을 전달해 토큰을 만들 수 있다.")
 @Test
 void generateToken(){
     //given
     User testUser = userRepository.save(User.builder()
             .email("user@gmail.com")
             .password("test")
             .build()
     );

     //when
     String token = tokenProvider.generateToken(testUser, Duration.ofDays(14));

     //then
     Long userId = Jwts.parserBuilder()
             .setSigningKey(key)
             .build()
             .parseClaimsJws(token)
             .getBody()
             .get("id", Long.class);

     assertThat(userId).isEqualTo(testUser.getId());
 }

 /*
 Given : jjwt 라이브러리를 사용해 토큰 생성, 만료된 토큰으로 생성
 When : validToken() 메서드를 호출해 유효한 토큰인지 검증한 뒤 결과값 반환
 Then : 반환값이 false 인 것 확인
  */

 @DisplayName("validToken() : 만료된 토큰인 때에 유효성 검증에 실패한다.")
 @Test
 void validToken_invalidToken() {
     //given
     String token = JwtFactory.builder()
             .expiration(new Date(new Date().getTime() - Duration.ofDays(7).toMillis()))
             .build()
             .createToken(jwtProperties);

     //when
     boolean result = tokenProvider.validToken(token);

     //then
     assertThat(result).isFalse();
 }

 /*
 Given : jjwt 라이브러리를 사용해 토큰 생성, 만료일은 현재 부터 14일 뒤로 만료되지 않은 토큰
 When : validToken() 메서드를 호출해 유효한 토큰인지 검증한 뒤 결과값 반환
 Then : 반환값이 true 인 것 확인
  */

 @DisplayName("validToken() : 유효한 토큰인 때에 유효성 검증에 성공한다.")
 @Test
 void validToken_validToken() {
     //given
     String token = JwtFactory.withDefaultValues().createToken(jwtProperties);

     //when
     boolean result = tokenProvider.validToken(token);

     //then
     assertThat(result).isTrue();
 }

 /*
 Given : jjwt 라이브러리를 사용해 토큰 생성, 토큰 제목(subject) : 유저 이메일
 When : getAuthentication() 메서드를 호출해 인증 객체 반환
 Then : 반환받은 인증 객체의 유저 이름을 가져와 given절에서 설정한 subject 값(userEmail)과 같은 지 확인
  */

 @DisplayName("getAuthentication() : 토큰 기반으로 인증 정보를 가져올 수 있다.")
 @Test
 void getAuthentication(){
     //given
     String userEmail = "test@gmail.com";
     String token = JwtFactory.builder()
             .subject(userEmail)
             .build()
             .createToken(jwtProperties);

     //when
     Authentication authentication = tokenProvider.getAuthentication(token);

     //then
     assertThat(((UserDetails) authentication.getPrincipal()).getUsername()).isEqualTo(userEmail);
 }

 /*
 Given : jjwt 라이브러리를 사용해 토큰 생성, 토큰 claim에 "id" 키를 추가 ("id" : userId")
 When : getUserId() 메서드를 호출해 유저 Id 반환
 Then : 반환받은 유저 Id와 given절에서 설정한 userId이 같은 지 확인
  */

 @DisplayName("getUserId() : 토큰으로 유저 ID를 가져올 수 있다.")
 @Test
 void getUserId(){
     //given
     Long userId = 1L;
     String token = JwtFactory.builder()
             .claims(Map.of("id", userId))
             .build()
             .createToken(jwtProperties);

     //when
     Long userIdByToken = tokenProvider.getUserId(token);

     //then
     assertThat(userIdByToken).isEqualTo(userId);
 }
}

RefreshToken 도메인 구현하기

테이블 구조

칼럼명자료형null 허용설명
idBIGINTN기본키일련번호, 기본키
user_idBIGINTN유저 ID
refresh_tokenVARCHAR(255)N토큰값

RefreshToken

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class RefreshToken {
   @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
   @Column(name="id", updatable = false)
   private Long id;

   @Column(name = "user_id", nullable = false, unique = true)
   private Long userId;

   @Column(name = "refresh_token", nullable = false)
   private String refreshToken;

   public RefreshToken(Long userId, String refreshToken) {
       this.userId = userId;
       this.refreshToken = refreshToken;
   }

   public RefreshToken update(String newRefreshToken){
       this.refreshToken = newRefreshToken;
       return this;
   }
}

RefreshTokenRepository

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
   Optional<RefreshToken> findByUserId(Long userId);
   Optional<RefreshToken> findByRefreshToken(String refreshToken);
}

토큰 필터 구현하기

필터?

실제로 각종 요청을 처리하기 위한 로직으로
전달되기 전후에 URL 패턴에 맞는 모든 요청을 처리하는 기능 제공

요청이 오면 헤더값을 비교해서 토큰이 있는지 확인
유효 토큰이라면?
Security Context Holder (시큐리티 콘텍스트 홀더)에 인증 정보 저장

Security Context?

인증 객체가 저장되는 보관소

  1. 인증 정보가 필요할 때 언제든지 인증 객체 사용 가능
  2. 스레드마다 공간을 할당하는, 스레드 로컬에 저장되므로 코드 아무곳에서나 참조 가능
  3. 다른 스레드와 공유하지 않아 독립적으로 사용 가능

이런 Security Context 객체를 저장하는 객체가 Security Context Holder

TokenAuthenticationFilter

액세스 토큰값이 담긴 Authorization 헤더값을 가져온 뒤 액세스 토큰이 유효하다면 인증 정보 설정

@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
  private final TokenProvider tokenProvider;
  private final static String HEADER_AUTHORIZATION = "Authorization";
  private final static String TOKEN_PREFIX = "Bearer";
  @Override
  protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain) throws ServletException, IOException {
  
      //요청 헤더의 Authorization 키의 값 조회
      String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
      //가져온 값에서 접두사 제거
      String token = getAccessToken(authorizationHeader);
      //가져온 토큰이 유효한지 확인하고, 유효한 때는 인증 정보 설정
      if (tokenProvider.validToken(token)){
          Authentication authentication = tokenProvider.getAuthentication(token);
          SecurityContextHolder.getContext().setAuthentication(authentication);
      }
      filterChain.doFilter(request,response);
  }

  private String getAccessToken(String authorizationHeader) {
      if (authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)){
          return authorizationHeader.substring(TOKEN_PREFIX.length());
      }
      return null;
  }
}

요청 헤더에서 키가 'Authorization' 인 필드의 값을 가져온 다음 토큰의 접두사 'Bearer'을 제외한 값 = 토큰 값
토큰이 유효하다면 Security Context에 인증 정보 설정

코드가 실행되고 인증 정보가 설정되면
Context Holder에서 getAuthentication() 메서드를 통해 유저 객체 반환 가능
유저 객체에는 유저 이름 (username)과 권한 목록(authorities) 포함


토큰 API 구현하기

Service

UserService

유저 ID로 유저 검색

@RequiredArgsConstructor
@Service
public class UserService {
   //..생략
   
   public User findById(Long userId){
       return userRepository.findById(userId)
               .orElseThrow(() -> new IllegalArgumentException("Unexpected user"));
   }
}

RefreshTokenService

RefreshToken으로 RefreshToken 객체 검색

@RequiredArgsConstructor
@Service
public class RefreshTokenService {
   private final RefreshTokenRepository refreshTokenRepository;

   public RefreshToken findByRefreshToken(String refreshToken){
       return refreshTokenRepository.findByRefreshToken(refreshToken)
               .orElseThrow(() -> new IllegalArgumentException("Unexpected token"));
   }
}

TokenService

RefreshToken으로 토큰 유효성 검사를 진행
유효한 토큰 이면? RefreshToken으로 사용자 ID 찾기
사용자 ID로 사용자를 찾아 새로운 액세스 토큰 생성

@Service
@RequiredArgsConstructor
public class TokenService {
   private final TokenProvider tokenProvider;
   private final RefreshTokenService refreshTokenService;
   private final UserService userService;

   public String createNewAccessToken(String refreshToken){
       if(!tokenProvider.validToken(refreshToken)){
           throw new IllegalArgumentException("Unexpected token");
       }
       Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
       User user = userService.findById(userId);

       return tokenProvider.generateToken(user, Duration.ofHours(2));
   }
}
 

DTO

CreateAccessTokenRequest

@Getter
@Setter
public class CreateAccessTokenRequest {
  private String refreshToken;
}

CreateAccessTokenResponse

@Getter
@AllArgsConstructor
public class CreateAccessTokenResponse {
   private String accessToken;
}

Controller

TokenApiController

@RequiredArgsConstructor
@RestController
public class TokenApiController {
   private final TokenService tokenService;

   @PostMapping("/api/token")
   public ResponseEntity<CreateAccessTokenResponse> createNewAccessToken(@RequestBody CreateAccessTokenRequest request){
       String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken());

       return ResponseEntity.status(HttpStatus.CREATED)
               .body(new CreateAccessTokenResponse(newAccessToken));
   }
}

Test

@SpringBootTest
@AutoConfigureMockMvc
class TokenApiControllerTest {
   @Autowired
   protected MockMvc mockMvc;

   @Autowired
   protected ObjectMapper objectMapper;

   @Autowired
   private WebApplicationContext context;

   @Autowired
   JwtProperties jwtProperties;

   @Autowired
   UserRepository userRepository;

   @Autowired
   RefreshTokenRepository refreshTokenRepository;

   @BeforeEach
   public void mockMvcSetUp(){
       this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
               .build();
       userRepository.deleteAll();
   }

   /*
   Given : 테스트 유저 생성, jjwt 라이브러리를 사용하여 리프레시 토큰을 만들어 데이터베이스에 저장
       토큰 생성 API의 요청 본문에 리프레시 토큰을 포함하여 요청 객체를 생성
   When : 토큰 추가 API에 요청을 보냄, 요청 타입 : JSON, given 절에서 미리 만들어둔 객체를 요청 본문으로 함께 보냄
   Then : 응답 코드가 201 Created, 엑세스 토큰이 비어있지 않은 지 확인
    */

   @DisplayName("createNewAccessToken : 새로운 액세스 토큰을 발급한다.")
   @Test
   public void createNewAccessToken() throws Exception {
       //given
       final String url = "/api/token";

       User testUser = userRepository.save(User.builder()
               .email("user@gmail.com")
               .password("test")
               .build());

       String refreshToken = JwtFactory.builder()
               .claims(Map.of("id", testUser.getId()))
               .build()
               .createToken(jwtProperties);

       refreshTokenRepository.save(new RefreshToken(testUser.getId(), refreshToken));

       CreateAccessTokenRequest request = new CreateAccessTokenRequest();
       request.setRefreshToken(refreshToken);
       final String requestBody = objectMapper.writeValueAsString(request);

       //when
       ResultActions result = mockMvc.perform(post(url)
               .contentType(MediaType.APPLICATION_JSON_VALUE)
               .content(requestBody));

       //then
       result.andExpect(status().isCreated())
               .andExpect(jsonPath("$.accessToken").isNotEmpty());
   }
}

토큰 기반 인증의 특징과 토큰의 일종인 JWT를 알아보고 토큰 제공자를 만들었습니다.

토큰 기반 인증, JWT, RefreshToken, 필터, Security Context
profile
뭐든 기록할 수 있도록

2개의 댓글

comment-user-thumbnail
2024년 7월 24일

JWT 토큰에 대해서 잘 정리 해주셔서 감사합니다.
저도 공부중인데 유익한 글을 보고 많이 배워갑니다.

1개의 답글