JWT 구현

뚜우웅이·2025년 2월 4일

토큰 기반 인증

Spring Security를 사용하면 사용자의 정보를 담은 세션을 생성하고 저장해서 인증하는 세션 기반 인증 방식을 사용한다. 이와 반대로 토큰 기반 인증 방식은 서버에서 클라이언트를 구분하기 위한 유일한 값이다. 서버에서 토큰을 생성하여 클라이언트에게 제공하면, 클라이언트는 토큰과 함께 요청을 전달한다. 서버는 이 토큰을 가지고 유효한 사용자인지 확인하는 방식이다.

특징

무상태성
토큰을 서버에 저장할 필요가 없다

확장성
서버를 확장할 때 상태 관리를 신경 쓸 필요가 없어진다.

무결성
토큰을 발급한 이후에 토큰 정보를 변경할 수가 없다.

JWT

JWT는 .을 기준으로 header, payload, signature로 이뤄져있다.

header
헤더에는 토큰의 타입과 해싱 알고리즘을 지정하는 정보를 담는다.

payload
내용에는 토큰과 관련된 정보를 담는다. 내용의 한 덩어리를 claim이라고 하며 키와 값 한 쌍으로 이뤄져있다. 클레임은 등록된 클레임, 공개 클레임, 비공개 클레임으로 나눌 수 있다.

  • 등록된 클레임은 토큰에 대한 정보를 담는다.
  • 공개 클레임은 공개되어도 상관없는 클레임이다. 보통 클레임 이름을 URI로 작성한다.
  • 비공개 클레임은 공개되어서는 안 되며, 클라이언트와 서버 간의 통신에 사용된다.

signature
서명은 해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용된다.

JWT 구현

의존성 추가

    // jwt
    implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6', 'io.jsonwebtoken:jjwt-jackson:0.12.6'

yml 설정

jwt:
  secret-key: # 별도 문자열을 Base64로 암호화한 값

  access:
    expiration: 3600000 # 1시간
    header: Authorization

  refresh:
    expiration: 1209600000 # 2주
    header: Authorization-refresh

spring:
  profiles:
    include: secret, jwt # 파일 인식
#    active: dev # 파일 적용

TokenProvider

package org.example.springbootdeveloper.global.jwt;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
public class TokenProvider {

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

    @Value("${jwt.access.expiration}")
    private long accessTokenExpiration;


    @Value("${jwt.refresh.expiration}")
    private long refreshTokenExpiration;

    // SecretKey 생성
    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(secretKey.getBytes());
    }

    // AccessToken 생성
    public String createAccessToken(String username, String role) {
        return createToken(username, role, accessTokenExpiration);
    }

    // RefreshToken 생성
    public String createRefreshToken(String username) {
        return createToken(username, "REFRESH", refreshTokenExpiration);
    }

    // Token 공통 생성 로직
    private String createToken(String username, String role, long expiration) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .subject(username) // 사용자 이름
                .claim("role", role)
                .issuedAt(now)
                .expiration(validity)
                .signWith(getSigningKey()) // 서명
                .compact();
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                    .verifyWith(getSigningKey()) // 서명 검증
                    .build()
                    .parseSignedClaims(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // 토큰에서 사용자 이름 추출
    public String getUsernameFromToken(String token) {
        return Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .getSubject();
    }

    public long getRefreshTokenExpiration() {
        return refreshTokenExpiration;
    }
}
  1. 서명 키 설정
    Keys.hmacShaKeyFor()를 사용해 시크릿 키를 생성한다.
    signWith() 메서드에 키를 전달한다.

  2. 토큰 생성
    Jwts.builder()를 사용해 토큰을 생성한다.

  3. 토큰 검증
    Jwts.parser()를 사용해 토큰을 파싱하고 검증한다.
    verifyWith() 메서드로 서명 키를 설정한다.

  4. 토큰 파싱
    parseSignedClaims()를 사용해 토큰의 클레임을 추출한다.
    getPayload()를 통해 클레임에 접근한다.

JwtAuthenticationFilter

package org.example.springbootdeveloper.global.jwt;


import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = resolveToken(request);

        if (token != null && tokenProvider.validateToken(token)) {
            String username = tokenProvider.getUsernameFromToken(token);
            JwtAuthenticationToken authentication = new JwtAuthenticationToken(username);
            // 인증 정보 설정
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

HTTP 요청에서 JWT를 추출하여 검증하고, 유효하면 인증 정보를 설정하는 역할을 한다.

  • OncePerRequestFilter
    Spring Security에서 제공하는 필터로, 한 요청당 한 번만 실행된다.

  • resolveToken(request)
    HTTP 요청 헤더에서 Authorization: Bearer <토큰> 형식의 JWT를 추출한다.(Authorization 헤더에서 "Bearer " 접두사가 붙은 경우, “Bearer “ 이후의 문자열(토큰) 을 반환)

  • tokenProvider.validateToken(token)
    토큰의 유효성을 검사한다. (예: 만료 여부, 서명 검증 등)

  • tokenProvider.getUsernameFromToken(token)
    JWT에서 사용자 이름(또는 ID)을 추출한다.

  • JwtAuthenticationToken authentication = new JwtAuthenticationToken(username);
    인증 객체를 생성합니다. (일반적으로 UsernamePasswordAuthenticationToken을 사용하지만, 여기서는 커스텀 JwtAuthenticationToken을 사용했다.)

  • SecurityContextHolder.getContext().setAuthentication(authentication);
    Spring Security의 인증 컨텍스트(SecurityContext)에 인증 정보 저장한다. 이렇게 하면 이후의 요청 처리 과정에서 @AuthenticationPrincipal 등을 통해 사용자 정보를 가져올 수 있다.

  • filterChain.doFilter(request, response);
    다음 필터로 요청을 전달

JwtAuthenticationToken

package org.example.springbootdeveloper.global.jwt;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;
import java.util.Collections;

@RequiredArgsConstructor
public class JwtAuthenticationToken implements Authentication {

    private final String username;
    private boolean authenticated = true;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList(); // 권한 정보가 없으면 빈 리스트 반환
    }

    @Override
    public Object getCredentials() {
        return null; // 자격 증명 정보(비밀번호 등)는 필요 없음
    }

    @Override
    public Object getDetails() {
        return null; // 추가 정보는 필요 없음
    }

    @Override
    public Object getPrincipal() {
        return username; // 사용자 이름 반환
    }

    @Override
    public boolean isAuthenticated() {
        return authenticated; // 인증 여부 반환
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        this.authenticated = isAuthenticated;
    }

    @Override
    public String getName() {
        return username; // 사용자 이름 반환
    }
}
  • JWT 기반 인증에서 Authentication 객체를 구현한 클래스
  • JWT 검증 후 인증이 완료된 사용자의 정보를 담는 역할
  • Spring Security의 SecurityContextHolder에 저장되어, 이후 요청에서 사용자 정보를 조회할 때 사용된다.
  • 일반적인 UsernamePasswordAuthenticationToken 대신 JWT 인증 방식에 맞게 커스텀 인증 객체로 사용된다.

WebSecurityConfig

package org.example.springbootdeveloper.global.security.config;

import lombok.RequiredArgsConstructor;
import org.example.springbootdeveloper.global.jwt.JwtAuthenticationFilter;
import org.example.springbootdeveloper.global.jwt.TokenProvider;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final TokenProvider tokenProvider;

    // Spring Security 기능 비활성화
    @Bean
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring()
                .requestMatchers(PathRequest.toH2Console())
                .requestMatchers(new AntPathRequestMatcher("/static/**"));
    }

    // 특정 HTTP 요청에 대한 웹 기반 보안 구성
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/login", "/signup", "/api/login", "/api/signup", "/api/reissue").permitAll()
                        .requestMatchers("/user/**").hasRole("USER")
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated())  // 나머지 url은 인증 후에 접근 가능
                .exceptionHandling(exception -> exception
                        .accessDeniedPage("/login")) // 인가되지 않은 페이지 접근 시 리다이렉트
                .logout(logout -> logout
                        .logoutSuccessUrl("/login")
                        .invalidateHttpSession(true) // 로그아웃 이후 세션 전체 삭제 여부
                        .deleteCookies("JSESSIONID") // 로그아웃 시 쿠키 삭제
                        .clearAuthentication(true) // 로그아웃 시 인증 정보 삭제
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용 안 함
                .csrf(AbstractHttpConfigurer::disable)
                .addFilterBefore(new JwtAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class)
                .build();
    }

    // 패스워드 인코더로 사용할 빈 등록
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // AuthenticationManager 빈 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}
  • @EnableWebSecurity
    Spring Security를 활성화

  • JwtAuthenticationFilter(JWT 검증 필터)를 UsernamePasswordAuthenticationFilter 앞에 추가한다.

  • UsernamePasswordAuthenticationFilter는 기본적으로 폼 로그인 방식의 인증을 처리하는데, JWT 기반 인증에서는 필요하지 않으므로 JWT 인증 필터를 먼저 실행하도록 설정한다.

  • 비밀번호를 안전하게 해싱하기 위한 BCryptPasswordEncoder를 빈으로 등록한다. 회원가입 시 비밀번호를 해싱하여 저장하고, 로그인 시 입력한 비밀번호와 비교할 때 사용한다.

  • Spring Security의 AuthenticationManager를 빈으로 등록
    AuthenticationManager는 인증을 처리하는 주요 컴포넌트이며,
    이를 사용하여 사용자 인증을 수행할 수 있다.

domain

RefreshToken

package org.example.springbootdeveloper.global.auth.domain;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@Entity
public class RefreshToken {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username; // 사용자 식별자

    @Column(nullable = false)
    private String refreshToken;

    @Column(nullable = false)
    private Long expiration;


    @Builder
    public RefreshToken(String username, String refreshToken, long expiration) {
        this.username = username;
        this.refreshToken = refreshToken;
        this.expiration = expiration;
    }

    private void updateToken(String refreshToken) {
        this.refreshToken = refreshToken;
        this.expiration = expiration;
    }
}

RefreshTokenRepository

package org.example.springbootdeveloper.global.auth.domain;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
    Optional<RefreshToken> findByUsername(String username);
    void deleteByUsername(String username);
}

api

dto

Request

LoginRequest

package org.example.springbootdeveloper.global.auth.api.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;

public record LoginRequest(
        @Email
        @NotEmpty
        String email,
        @NotEmpty
        String password
) {
}

ReissueRequest

package org.example.springbootdeveloper.global.auth.api.dto.request;

public record ReissueRequest(
        String refreshToken
) {
}

signupRequest

package org.example.springbootdeveloper.global.auth.api.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.example.springbootdeveloper.User.domain.Role;
import org.example.springbootdeveloper.User.domain.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public record SignUpUserRequest(
        @Email
        String email,
        @NotEmpty
        String password,
        @NotEmpty
        String nickname,
        @NotNull
        int age
) {
    public User toEntity(BCryptPasswordEncoder bCryptPasswordEncoder) {
        return User.builder()
                .email(email)
                .password(bCryptPasswordEncoder.encode(password))
                .nickname(nickname)
                .age(age)
                .role(Role.USER)
                .build();
    }
}

Response

LoginResponse

package org.example.springbootdeveloper.global.auth.api.dto.response;

public record LoginResponse(
        String accessToken,
        String refreshToken
) {
    public LoginResponse(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
}

ReissueResponse

package org.example.springbootdeveloper.global.auth.api.dto.response;

public record ReissueResponse(
        String accessToken
) {
    public ReissueResponse(String accessToken) {
        this.accessToken = accessToken;
    }
}

SignUpUserResponse

package org.example.springbootdeveloper.global.auth.api.dto.response;

import lombok.Builder;
import org.example.springbootdeveloper.User.domain.User;

import java.time.LocalDateTime;

@Builder
public record SignUpUserResponse(
        Long id,
        String email,
        String nickname,
        int age,
        LocalDateTime createdAt,
        LocalDateTime lastModifiedAt
) {
    public static SignUpUserResponse toDto(User user) {
        return SignUpUserResponse.builder()
                .id(user.getId())
                .email(user.getEmail())
                .nickname(user.getNickname())
                .age(user.getAge())
                .createdAt(user.getCreatedAt())
                .lastModifiedAt(user.getLastModifiedAt())
                .build();
    }
}

Controller

package org.example.springbootdeveloper.global.auth.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.example.springbootdeveloper.User.application.UserService;
import org.example.springbootdeveloper.global.auth.api.dto.request.LoginRequest;
import org.example.springbootdeveloper.global.auth.api.dto.request.ReissueRequest;
import org.example.springbootdeveloper.global.auth.api.dto.request.SignUpUserRequest;
import org.example.springbootdeveloper.global.auth.api.dto.response.LoginResponse;
import org.example.springbootdeveloper.global.auth.api.dto.response.ReissueResponse;
import org.example.springbootdeveloper.global.auth.api.dto.response.SignUpUserResponse;
import org.example.springbootdeveloper.global.auth.application.RefreshTokenService;
import org.example.springbootdeveloper.global.jwt.TokenProvider;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class AuthController {

    private final UserService userService;
    private final AuthenticationManager authenticationManager;
    private final TokenProvider tokenProvider;
    private final RefreshTokenService refreshTokenService;

    @Operation(summary = "회원 가입", description = "파라미터로 넘어온 정보로 회원 가입을 한다.")
    @ApiResponse(responseCode = "201", description = "생성")
    @ApiResponse(responseCode = "400", description = "파라미터 오류")
    @PostMapping("/signup")
    public ResponseEntity<SignUpUserResponse> signUp(@Parameter(description = "email, password, nickname, age")
                                                     @RequestBody @Valid SignUpUserRequest signUpUserRequest) {
        SignUpUserResponse signUpUserResponse = userService.signUp(signUpUserRequest);
        return ResponseEntity.status(HttpStatus.CREATED).body(signUpUserResponse);
    }

    @Operation(summary = "로그인", description = "파라미터로 넘어온 정보로 로그인 한다.")
    @ApiResponse(responseCode = "200", description = "성공")
    @ApiResponse(responseCode = "400", description = "파라미터 오류")
    @ApiResponse(responseCode = "401", description = "인증 실패")
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody @Valid LoginRequest loginRequest) {
        try {        // 사용자 인증
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginRequest.email(), loginRequest.password())
            );

            // 인증된 사용자 정보 조회
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();

            // role 추출
            String role = userDetails.getAuthorities().stream()
                    .findFirst()
                    .orElseThrow(() -> new RuntimeException("Role not found"))
                    .getAuthority();

            // JWT 토큰 생성
            String accessToken = tokenProvider.createAccessToken(userDetails.getUsername(), role);
            String refreshToken = tokenProvider.createRefreshToken(userDetails.getUsername());

            // RefreshToken 저장
            refreshTokenService.saveRefreshToken(userDetails.getUsername(), refreshToken);

            return ResponseEntity.status(HttpStatus.OK).body(new LoginResponse(accessToken, refreshToken));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid email or password");
        }
    }

    @PostMapping("/reissue")
    public ResponseEntity<ReissueResponse> reissueAccessToken(@RequestBody ReissueRequest reissueRequest) {
        String newAccessToken = refreshTokenService.reissueAccessToken(reissueRequest.refreshToken());
        return ResponseEntity.status(HttpStatus.OK).body(new ReissueResponse(newAccessToken));
    }


    @Operation(summary = "로그아웃", description = "로그아웃을 한다.")
    @ApiResponse(responseCode = "200", description = "성공")
    @GetMapping("/logout")
    public ResponseEntity<?> logout(@RequestHeader("Authorization") String refreshToken) {
        refreshToken = refreshToken.substring(7); // "Bearer" 제거
        String username = tokenProvider.getUsernameFromToken(refreshToken);

        // RefreshToken 삭제
        refreshTokenService.deleteRefreshTokenByUsername(username);

        return ResponseEntity.ok("Logout successfully");
    }

}
  • 로그인

    • authenticationManager.authenticate()를 사용하여 이메일과 비밀번호 검증한다.
    • Spring Security의 UserDetailsService가 내부적으로 실행되어 DB에서 사용자 정보 조회한다.
    • 인증이 성공하면 UserDetails 객체를 가져온다.
    • 사용자의 GrantedAuthority에서 첫 번째 역할을 가져옴
      • 만약 역할이 없으면 예외 발생
    • tokenProvider를 이용해 액세스 토큰과 리프레시 토큰 생성한다.
    • 데이터베이스 또는 캐시에 리프레시 토큰을 저장한다.
    • 로그인 성공 시 액세스 토큰과 리프레시 토큰을 응답으로 반환
      • 로그인 실패 시 401 Unauthorized 반환
  • 로그아웃

    • @GetMapping("/logout") → GET /api/logout 요청을 처리
    • @RequestHeader("Authorization") → 요청 헤더에서 리프레시 토큰을 가져온다.
    • "Bearer " 접두어 제거 후 사용자명 추출
    • refreshTokenService.deleteRefreshTokenByUsername(username);
      • 저장된 리프레시 토큰 삭제
        - "Logout successfully" 응답 반환

application

package org.example.springbootdeveloper.global.auth.application;

import lombok.RequiredArgsConstructor;
import org.example.springbootdeveloper.global.auth.domain.RefreshToken;
import org.example.springbootdeveloper.global.auth.domain.RefreshTokenRepository;
import org.example.springbootdeveloper.global.jwt.TokenProvider;
import org.example.springbootdeveloper.global.security.UserDetailService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class RefreshTokenService {

    private final RefreshTokenRepository refreshTokenRepository;
    private final TokenProvider tokenProvider;
    private final UserDetailService userDetailService;

    // RefreshToken 저장
    @Transactional
    public void saveRefreshToken(String username, String refreshToken) {
        RefreshToken token = RefreshToken.builder()
                .username(username)
                .refreshToken(refreshToken)
                .expiration(Instant.now().toEpochMilli() + tokenProvider.getRefreshTokenExpiration())
                .build();

        refreshTokenRepository.save(token);
    }

    // RefreshToken 조회
    public Optional<RefreshToken> findRefreshTokenByUsername(String username) {
        return refreshTokenRepository.findByUsername(username);
    }

    // RefreshToken 삭제
    @Transactional
    public void deleteRefreshTokenByUsername(String username) {
        refreshTokenRepository.deleteByUsername(username);
    }

    // RefreshToken 유효성 검사 및 AccessToken 재발급
    @Transactional
    public String reissueAccessToken(String refreshToken) {
        // RefreshToken 검증
        if (!tokenProvider.validateToken(refreshToken)) {
            throw new RuntimeException("Invalid Refresh Token");
        }

        // RefreshToken에서 사용자 이름 추출
        String username = tokenProvider.getUsernameFromToken(refreshToken);

        // DB에서 RefreshToken 조회
        RefreshToken storedToken = refreshTokenRepository.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("RefreshToken not found"));

        // 저장된 RefreshToken과 요청된 RefreshToken 비교
        if (!storedToken.getRefreshToken().equals(refreshToken)) {
            throw new RuntimeException("RefreshToken mismatch");
        }

        // RefreshToken이 만료 되었는지 확인
        if (storedToken.getExpiration() < Instant.now().toEpochMilli()) {
            throw new RuntimeException("RefreshToken expired");
        }

        // role 가져오기
        UserDetails userDetails = userDetailService.loadUserByUsername(username);
        String role = userDetails.getAuthorities().stream()
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Role not found"))
                .getAuthority();

        // 새로운 AccessToken 발급
        return tokenProvider.createAccessToken(username, role);
    }
}
  • RefreshToken 저장

    • RefreshToken.builder()를 이용해 새로운 리프레시 토큰 객체 생성
    • username → 해당 사용자
    • refreshToken → RefreshToken 값
    • expiration → 현재 시간 + RefreshToken 만료 시간
    • refreshTokenRepository.save(token); → DB에 저장
  • RefreshToken 조회

    • refreshTokenRepository.findByUsername(username)
      • 사용자의 리프레시 토큰을 DB에서 조회
        - 결과가 없을 수도 있으므로 Optional로 반환'
  • RefreshToken 삭제

    • refreshTokenRepository.deleteByUsername(username)
      • 해당 사용자의 리프레시 토큰을 삭제
  • AccessToken 재발급 (RefreshToken 검증)

    • tokenProvider.validateToken(refreshToken)으로 토큰의 유효성을 검사한다. 유효하지 않으면 예외 발생
    • JWT에서 username을 추출
    • DB에서 해당 사용자의 리프레시 토큰을 조회
      • 존재하지 않으면 예외 발생
    • 요청된 리프레시 토큰과 DB에 저장된 리프레시 토큰이 다르면 예외 발생
    • 현재 시간이 저장된 만료 시간보다 크다면 토큰이 만료된 것
    • UserDetailService를 사용하여 사용자 정보 조회
      • 사용자 역할(Role)을 가져온다.
    • 새로운 액세스 토큰을 생성하여 반환
      '
      로그인 시 토큰 발급

      발급 받은 AccessToken으로 보호된 페이지 접근 가능

      AccessToken 재발급
profile
공부하는 초보 개발자

0개의 댓글