우와한 개발자

[JWT] Spring Security JWT 인증 구현 –JwtTokenProvider, JwtAuthenticationFilter

by 우와한개발자

1. pom.xml : 라이브러리 의존성

  • Spring Security 적용을 위해 spring-boot-starter-security를 추가
  • JWT 생성/파싱을 위해 jjwt 라이브러리를 추가
  • JJWT는 최신 버전에서 jjwt-api, jjwt-impl, jjwt-jackson으로 분리된 구조를 사용
  • 과거에는 jjwt 단일 의존성을 사용할 수 있었지만, 최신 환경(Spring Boot 3 이상)에서는 분리된 모듈 방식 사용을 권장
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JJWT (최신 버전은 3개 모듈로 분리) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.13.0</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>
의존성 설명
spring-boot-starter-security Spring Security 기본 기능 제공
jjwt-api JWT 생성/파싱 API
jjwt-impl JWT 실제 구현체
jjwt-jackson JWT Payload JSON 처리

2. application.properties 설정

  • JWT 서명에 사용할 비밀키와 만료시간을 properties에 설정
jwt.secret=abcdefghijklmnopqrstuvwxyz123456
jwt.expiration=3600000
속성 설명
jwt.secret JWT 서명에 사용할 비밀키
jwt.expiration 토큰 만료시간(밀리초), 예: 3600000 = 1시간

 

3. JwtTokenProvider 클래스

  • 토큰 관련 공통 기능을 모아두는 역할
  • JWT 생성, JWT에서 사용자 정보 추출, JWT 유효성 검사, HTTP 요청 헤더에서 토큰 추출 등
@Component
public class JwtTokenProvider {

    private final Key key;
    private final long tokenValidTime;

    private static final String AUTH_HEADER = "Authorization";
    private static final String TOKEN_PREFIX = "Bearer ";

    public JwtTokenProvider(
            @Value("${jwt.secret}") String secretKey,
            @Value("${jwt.expiration}") long tokenValidTime
    ) {
        this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
        this.tokenValidTime = tokenValidTime;
    }

    // JWT 생성
    public String createToken(String username, String role) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + tokenValidTime);

        return Jwts.builder()
                .subject(username)
                .claim("role", role)
                .issuedAt(now)
                .expiration(expiry)
                .signWith(key)
                .compact();
    }

    // JWT에서 Authentication 생성
    public Authentication getAuthentication(String token) {
        String username = getUsername(token);
        String role = getRole(token);

        return new UsernamePasswordAuthenticationToken(
                username,
                null,
                List.of(new SimpleGrantedAuthority(role))
        );
    }

    // 토큰에서 username 추출
    public String getUsername(String token) {
        return getClaims(token).getSubject();
    }

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

    // 토큰 유효성 검사
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                    .verifyWith(key)
                    .build()
                    .parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    // HTTP 요청 헤더에서 토큰 추출
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTH_HEADER);

        if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) {
            return bearerToken.substring(TOKEN_PREFIX.length());
        }

        return null;
    }

    // Claims 추출
    private Claims getClaims(String token) {
        return Jwts.parser()
                .verifyWith(key)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
}
구분 코드 / 메서드 설명
Key 생성 Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)) 문자열 형태의 secretKey를 JWT 서명에 사용할 Key 객체로 변환
토큰 생성 createToken(String username, String role) 사용자 ID와 권한을 담아 JWT 생성 (subject, claim, issuedAt, expiration 포함)
인증 객체 생성 getAuthentication(String token) JWT에서 사용자 정보 추출 후 Authentication 객체 생성
토큰 추출 resolveToken(HttpServletRequest request) Authorization: Bearer ... 헤더에서 실제 토큰 문자열만 추출

 

3. JwtAuthenticationFilter 클래스

  • 매 요청마다 실행되응 필처
  • 토큰을 확인하고 유효하면 사용자 정보를 꺼내서 Authentication 객체 생성해 SecurityContextHolder에 저장
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

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

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        String token = jwtTokenProvider.resolveToken(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            String username = jwtTokenProvider.getUsername(token);
            String role = jwtTokenProvider.getRole(token);
						Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }
}
구분 SecurityContext Session
저장 위치 서버 메모리 (ThreadLocal) 서버 메모리/스토리지
유지 범위 현재 요청 동안만 여러 요청 동안 유지
목적 지금 요청의 인증 정보 로그인 상태 유지
JWT에서 사용 ❌ (JWT는 세션 불필요)

 

4. Securityconig

  • 구현한 JwtAuthenticationFilter 등록
  • JWT 방식은 보통 서버 세션을 사용하지 않으므로, 세션 정책을 STATELESS로 설정(JSESSIONID 생성되지 않음)
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
구분 설정 코드 설명
CSRF 비활성화 csrf.disable() JWT는 세션 기반 인증이 아니므로 CSRF 방어가 필요 없어 비활성화
세션 정책 SessionCreationPolicy.STATELESS 서버에 인증 정보를 저장하지 않고, 매 요청마다 JWT로 인증 처리
필터 등록 .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) JWT 필터를 Security 필터 체인에 등록하고, 기존 인증 필터보다 먼저 실행

 

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기