Spring Security 프로젝트 설정 2 - JwtService와 Filter 설정

✒️ 2025-05-28 14:21 내용 수정



흐름

spring_security_authentication_flow.png


Service 추가

  1. config 패키지를 생성하고, JwtService를 추가한다.
    • JWT에서 사용자 이름을 추출하거나 토큰의 유효성 검사, 토큰 서명 등의 기능을 수행하는 메소드를 저장한 Service다.
    • 이 부분에서 JWT를 다루는 메소드가 필요하기에 JWT 의존성이 없으면 진행할 수 없다.
      • io.jsonwebtoken 패키지 메소드들이다.
package com.example.security.config;  
  
import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
import io.jsonwebtoken.io.Decoders;  
import io.jsonwebtoken.security.Keys;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.stereotype.Service;  
  
import java.security.Key;  
import java.util.Date;  
import java.util.HashMap;  
import java.util.Map;  
import java.util.function.Function;  
  
@Service  
public class JwtService {  
  
    private static final String SECRET_KEY = 
    "d7e3c737696c3b9241bbde3fdfb664b515f36bb634da1afac2e45a85faef6c37";  
  
    // 토큰에서 사용자 이름 추출  
    public String extractUsername(String token) {  
        return extractClaim(token, Claims::getSubject);  
    }  
  
    // 클레임 추출  
    public <T> T extractClaim(
	    String token, 
	    Function<Claims, T> claimsResolver
	) {  
        final Claims claims = extractAllClaims(token);  
        return claimsResolver.apply(claims);  
    }  
  
    // 토큰 생성 - UserDetail로만 생성  
    public String generateToken(UserDetails userDetails) {  
        return generateToken(new HashMap<>(), userDetails);  
    }  
  
    // 토큰 생성  
    public String generateToken(  
            Map<String, Object> extraClaims, // 토큰에 보낼 정보  
            UserDetails userDetails  
    ) {  
        return Jwts  
			.builder()  
			.setClaims(extraClaims) // 클레임 추가  
			.setSubject(userDetails.getUsername()) // subject 추가  
			.setIssuedAt(
				new Date(
					System.currentTimeMillis())) // 토큰 발행일  
			.setExpiration(
				new Date(
					System.currentTimeMillis() 
					+ 1000 * 60 * 60 * 24)) // 24시간  
			.signWith(getSignInKey(), SignatureAlgorithm.HS256)  
			.compact();  
    }  
  
    // 토큰 유효성 검사  
    public boolean isTokenValid(
	    String token, 
	    UserDetails userDetails
	) {  
        final String username = extractUsername(token);  
        return (username.equals(userDetails.getUsername())) 
	        && !isTokenExpired(token);  
    }  
  
    // 토큰 만료 확인  
    public boolean isTokenExpired(String token) {  
        return extraExpiration(token).before(new Date());  
    }  
  
    // 토큰에서 만료 기한 가져오기  
    public Date extraExpiration(String token) {  
        return extractClaim(token, Claims::getExpiration);  
    }  
  
    // jwt에서 모든 클레임 추출  
    private Claims extractAllClaims(String token) {  
        return Jwts  
                .parserBuilder()  
                .setSigningKey(getSignInKey())  
                .build()  
                .parseClaimsJws(token)  
                .getBody();  
    }  
  
    // jwt 서명에 사용하는 비밀 키 생성  
    private Key getSignInKey() {  
        byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);  
        return Keys.hmacShaKeyFor(keyBytes);  
    }  
}
  1. JwtService에서 사용할 UserDetailsService를 생성하기 위한 ApplicationConfig 클래스를 추가한다.
package com.example.security.config;  
  
import com.example.security.user.UserRepository;  
import lombok.RequiredArgsConstructor;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.security.core.userdetails.UserDetailsService;  
import org.springframework.security.core.userdetails.UsernameNotFoundException;  
  
@Configuration  
@RequiredArgsConstructor  
public class ApplicationConfig {  
  
    private final UserRepository userRepository;  
  
    @Bean  
    public UserDetailsService userDetailsService() {  
        return username -> userRepository.findByEmail(username)  
                .orElseThrow(()->new UsernameNotFoundException("User not found"));  
    }  
}

JwtAuthenticationFilter 추가

  1. config 패키지에 JwtAuthenticationFilter 클래스를 추가하고, OncePerRequestFilter를 상속 받는다.
    • OncePerRequestFilter : 매 HTTP 요청마다 한 번만 실행하는 Filter로, 요청 전후에 공통 작업을 수행할 때 사용한다.
      • 로그 기록, 보안 검증, session 관리 등
    • 사용자가 보호된 라우트나 자원에 접근하고자 할 때 사용자 에이전트는 Authorization header에 Bearer schema를 사용하여 JWT를 전송해야 하므로, 토큰 검증 시 Bearer 를 사용하여 확인할 수 있다.
package com.example.security.config;  
  
import jakarta.servlet.FilterChain;  
import jakarta.servlet.ServletException;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import lombok.RequiredArgsConstructor;  
import org.springframework.lang.NonNull;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.core.context.SecurityContextHolder;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.security.core.userdetails.UserDetailsService;  
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;  
import org.springframework.stereotype.Component;  
import org.springframework.web.filter.OncePerRequestFilter;  
  
import java.io.IOException;  
  
@Component  
@RequiredArgsConstructor  
public class JwtAuthenticationFilter extends OncePerRequestFilter {  
  
    private final JwtService jwtService;  
    private final UserDetailsService userDetailsService;  
  
    // 요청이 들어왔을 때 처리할 작업  
    @Override  
    protected void doFilterInternal(  
            @NonNull HttpServletRequest request, // 요청  
            @NonNull HttpServletResponse response, // 응답  
            @NonNull FilterChain filterChain // 필터들  
    ) throws ServletException, IOException {  
        // 요청으로부터 온 header의 내용 추출  
        final String authHeader = request.getHeader("Authorization");  
        // jwt  
        final String jwt;  
        // 사용자 이메일  
        final String userEmail;  
  
        // jwt가 없으면 요청을 이후 필터로 전달  
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {  
            // jwt는 Authorization header에 Bearer schema를 사용한다.  
            filterChain.doFilter(request, response);  
            return;  
        }  
  
        jwt = authHeader.substring(7); // "Bearer "는 7글자  
        // jwt로부터 사용자 이메일을 추출  
        userEmail = jwtService.extractUsername(jwt);  
  
        // 검증 절차  
        // 사용자가 존재하고, 아직 인증을 진행하지 않아 
        // SecurityContextHolder에 저장되지 않았을 때  
        if (userEmail != null 
	        && SecurityContextHolder
		        .getContext()
		        .getAuthentication() == null) {  
            
            UserDetails userDetails 
            = this
             .userDetailsService
             .loadUserByUsername(userEmail);  

			// jwt 유효성 확인  
            if (jwtService.isTokenValid(jwt, userDetails)) {  
                
                // Spring SecurityContext에 업데이트에 필요한 객체  
                UsernamePasswordAuthenticationToken authToken 
                = new UsernamePasswordAuthenticationToken(  
                        userDetails,  
                        null,  
                        userDetails.getAuthorities()  
                );  
                
                authToken.setDetails(  
                        new WebAuthenticationDetailsSource()
                        .buildDetails(request)  
                );  
                
                SecurityContextHolder
                .getContext()
                .setAuthentication(authToken);  
            }  
        }  
        
        // 항상 작업이 끝나면 다음 필터로 넘겨줘야 함  
        filterChain.doFilter(request, response);  
    }  
}