JWT란?

기 원·2025년 5월 1일

JWT란?

  • 서버가 사용자에게 인증 완료 증명서를 발급해주는 디지털 쪽지

어떻게 생겨먹었나?

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiIxMjMiLCJyb2xlIjoiUk9MRV9VU0VSIn0.
1234567890abcdef

디코딩 한다면

{"alg":"HS256","typ":"JWT"}{"userId":"123","role":"ROLE_USER"}5~9=Ѧu
  1. Header: 어떤 암호화 알고리즘을 썼는가? ex. HS256
  2. Payload: 사용자 정보
  3. signature: 위 정보를 변조 못하게 서명한 것

JWT 흐름

  1. 로그인 요청
  2. 서버가 이메일/비밀번호 확인
  3. 성공하면 JWT 발급
  4. 클라이언트는 발급 받은 토큰을 Authorization: Bearer {token}으로 사용
  5. 서버는 매 요청마다 JWT를 해석해서 사용자 인증 처리

Step. 1 - 로그인 컨트롤러와 레파지토리

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

    private final AuthService authService;

    @PostMapping("/login")
    public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest request) {
        return ResponseEntity.ok(authService.login(request));
    }
}
  • 로그인 API가 있어야 토큰을 발급받겠죠?
  • 이미 많이 만들어 보셨을 테니 자세한 설명은 패스
public record LoginRequest(String email, String password) {

}
public record TokenResponse(String accessToken, String refreshToken) {

}
  • 레파지토리는 아무 내용도 없어도 됩니다!

Step. 2 - JWT 생성 클래스( JwtTokenProvider )

@Component // Spring Bean으로 등록해 다른 컴포넌트에서 주입 받을 수 있게 해줌
public class JwtTokenProvider {

	// jwt.secret 값을 주입받음 -> 사용자가 아무 값이나 지정해주면 됨(한 20자리 정도?)
    // ex. jwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecret
    @Value("${jwt.secret}") 
    private String secret;

	// 엑세스 토큰 유효기간 설정 ms단위임!
    private final long accessTokenValidity = 1000L * 60 * 60; // 1시간
    
    // 리프레쉬 토큰 유효기간 설정 역시 ms단위임!
    private final long refreshTokenValidity = 1000L * 60 * 60 * 24 * 7; // 7일

	// 사용자 ID와 역할을 바탕으로 액세스 토큰을 생성 -> 최대한 적은 정보가 들어가는게 좋음
    public String createAccessToken(Long userId, String role) {
        return createToken(userId, role, accessTokenValidity);
    }

	// ID와 역할을 바탕으로 뭐? 리프레쉬 토큰을 만들어 준다~
    public String createRefreshToken(Long userId, String role) {
        return createToken(userId, role, refreshTokenValidity);
    }

	// 공통 로직 -> 토큰 생성 claims(정보) 설정, 발급 시간/ 만료 시간 지정, 서명
    private String createToken(Long userId, String role, long validity) {
        Claims claims = Jwts.claims().setSubject(String.valueOf(userId)); // subject에 userId 설정
        claims.put("role", role); //claims에 룰 추가

        Date now = new Date(); // 지금 시간
        Date expiry = new Date(now.getTime() + validity); // 만료 시간 계산

		// JWT 빌더를 이용해서 토큰 생성해줌
        return Jwts.builder()
                .setClaims(claims) // 위에 열심히 적은 정보들
                .setIssuedAt(now) // 발급 시간
                .setExpiration(expiry) // 만료 시간
                .signWith(SignatureAlgorithm.HS256, secret.getBytes()) // 서명 알고리즘 + secretkey 지정
                .compact(); // 최종적으로 토큰 생성
    }

	// 토큰에서 사용자 ID(subject) 추출
    public Long getUserId(String token) {
        return Long.valueOf(parseClaims(token).getSubject());
    }

	// 토큰에서 룰 추출
    public String getRole(String token) {
        return parseClaims(token).get("role", String.class);
    }

	// 토큰이 유효한지 검사 -> 성공하면 혁며..아니고 ture, 실패하면 false
    public boolean validateToken(String token) {
        try {
            parseClaims(token); // 내부적으로 예외 던짐
            return true;
        } catch (Exception e) {
            return false;
        }
    }

	// 토큰을 파싱해서 Claims 객체 반환 (위에 적은 jwt.secret 이용해서 검증)
    private Claims parseClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(secret.getBytes()) // 서명 키
                .build()
                .parseClaimsJws(token) //JWT 파싱
                .getBody(); // Claims 반환
    }
}
  • 자세한 설명은 주석으로 대신했습니다.

Step. 3 JWT 발급 - 만든거 줘야겠지?

@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository userRepository;
    private final JwtTokenProvider jwtTokenProvider;

    public TokenResponse login(LoginRequest request) {
        User user = userRepository.findByEmail(request.email())
            .orElseThrow(() -> new RuntimeException("유저 없음"));

        if (!user.getPassword().equals(request.password())) {
            throw new RuntimeException("비밀번호 틀림");
        }

		// 엑세스 토큰 생성
        String accessToken = jwtTokenProvider.createAccessToken(user.getId(), user.getRole().name());
        
        // 리프레쉬 토큰 생성
        String refreshToken = jwtTokenProvider.createRefreshToken(user.getId(), user.getRole().name());

		// 토큰 두개 담아서 반환
        return new TokenResponse(accessToken, refreshToken);
    }
}
  • Service에서 가장 중요한건 열심히 만든 토큰을 똑바로 주는것!

Step. 4 인증 필터 (Spring Security)

@Configuration // 이 클래스가 Spring의 설정 클래스임을 알려줌
@EnableWebSecurity // Spring Security 기능을 활성화
@RequiredArgsConstructor 
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider; // JWT 관련 로직을 처리할 Provider 주입

    @Bean // SecurityFilterChain을 Bean으로 등록
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 (JWT 기반 인증 할꺼니까?)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션을 사용하지 않도록 설정 (역시 JWT 인증 이니까)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll() // /auth/** 경로는 인증 없이 접근 허용
                .anyRequest().authenticated() // 그 외의 모든 요청은 인증 필요
            )
            .addFilterBefore(new JwtAuthFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); // JWT 인증 필터를 Spring Security 필터 체인 앞단에 추가

        return http.build(); // 최종 SecurityFilterChain 객체 생성 및 반환
    }
}

Step. 5 Filter

// 매 요청마다 실행됨
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

	// 실제 필터 동작을 정의함
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        String token = resolveToken(request); // Authorization 헤더에서 토큰 추출
        
        // 토큰이 존재하고 유효하면 인증 처리
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Long userId = jwtTokenProvider.getUserId(token); // 토큰에서 userId 추출
            String role = jwtTokenProvider.getRole(token); // 토큰에서 role 추출

			// 인증 객체 생성: principal에 커스텀 AuthUser, credentials는 null, 권한 목록 설정
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                new AuthUser(userId, role),
                null,
                List.of(new SimpleGrantedAuthority(role))
            );
            
			// 인증 객체를 현재 SecurityContext에 설정 → 컨트롤러에서 @AuthenticationPrincipal 등으로 접근 가능
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
		// 다음 필터로 요청 전달 -체인 계속 진행
        chain.doFilter(request, response);
    }

	// Authorization 헤더에서 Bearer 토큰 파싱
    private String resolveToken(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        return (bearer != null && bearer.startsWith("Bearer ")) ? bearer.substring(7) : null;
    }
}
  • 여기까지가 발급에 필요한 기본 포맷입니다~

그 다음은 어떤게 있냐고요?
1. 리프레쉬 토큰을 저장 -> 로그아웃, 재발급 대비
2. 로그아웃 시 블랙리스트 처리
3. PasswordEncoder 적용
등이 있죠!

profile
노력하고 있다니까요?

1개의 댓글

comment-user-thumbnail
2025년 5월 1일

와 JWT도 정리하셨군요.. 대단하십니다

답글 달기