Spring Security와 JWT(JSON Web Token)를 이용한 인증 및 권한 부여의 동작 과정을 정리하겠습니다. JWT는 클라이언트와 서버 간에 인증 정보를 안전하게 주고받을 수 있도록 도와주는 토큰 기반 인증 방식입니다. 아래는 Spring Security에서 JWT가 동작하는 과정을 순차적으로 설명한 것입니다.
클라이언트는 사용자 자격 증명(예: username, password)을 서버에 전송하여 로그인 요청을 합니다.
POST /auth/signin
{
"username": "user",
"password": "password"
}
AuthenticationManager를 통한 인증컨트롤러에서 AuthenticationManager.authenticate() 메서드를 통해 사용자 인증을 진행합니다. 이 과정에서 UsernamePasswordAuthenticationToken을 생성하여 사용자 자격 증명을 넘깁니다.
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
이때 Spring Security는 UserDetailsServiceImpl을 통해 사용자 정보를 로드하고, PasswordEncoder를 사용하여 비밀번호를 검증합니다.
인증에 성공하면 JwtTokenProvider를 사용해 JWT 토큰을 생성합니다.
String jwt = jwtTokenProvider.generateToken(authentication);
JwtTokenProvider.generateToken() 메서드에서는 인증된 사용자 정보를 토대로 JWT를 생성하며, 이를 클라이언트에게 응답으로 전송합니다.
public String generateToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) // 만료 시간 설정
.signWith(SignatureAlgorithm.HS512, jwtSecret) // 서명 알고리즘과 비밀 키 사용
.compact();
}
클라이언트는 서버로부터 받은 JWT를 로컬 스토리지 또는 세션에 저장합니다. 이후 요청을 보낼 때 이 JWT를 Authorization 헤더에 추가합니다.
Authorization: Bearer <JWT 토큰>
클라이언트는 이후 서버에 요청을 보낼 때, Authorization 헤더에 JWT를 포함하여 요청을 보냅니다.
GET /api/todos
Authorization: Bearer <JWT 토큰>
서버는 요청이 들어오면 Spring Security의 JwtAuthenticationFilter를 통해 JWT가 유효한지 검증합니다. 이 필터는 모든 요청 전에 JWT를 검사하며, JWT가 유효하다면 인증 객체를 SecurityContextHolder에 저장합니다.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (jwt != null && jwtTokenProvider.validateJwtToken(jwt)) { // 토큰 유효성 검사
String username = jwtTokenProvider.getUsernameFromJwtToken(jwt); // 토큰에서 사용자명 추출
UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 사용자 정보 로드
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보 설정
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
주요 과정:
getJwtFromRequest()로 요청 헤더에서 JWT 토큰을 추출합니다.JwtTokenProvider.validateJwtToken()을 사용해 유효성을 검사합니다.getUsernameFromJwtToken()으로 사용자 이름을 추출하고, 해당 사용자 정보를 다시 UserDetailsServiceImpl에서 가져옵니다.SecurityContextHolder에 인증 정보를 설정하여 이후 요청에서도 인증된 사용자로 동작하게 만듭니다.인증된 사용자의 정보를 SecurityContextHolder에 저장함으로써, 이후 요청에서 해당 사용자가 인증된 사용자임을 유지할 수 있습니다.
SecurityContextHolder.getContext().setAuthentication(authentication);
이제 SecurityContextHolder에 저장된 인증 정보를 기반으로 사용자의 권한을 확인하며, 권한에 따라 요청이 허용되거나 거부됩니다. 만약 권한이 필요한 API에 접근할 경우, 권한을 확인하고 요청을 처리할 수 있습니다.
서버는 요청 처리 후 클라이언트로 응답을 전송합니다. 이 과정에서 사용자 권한이 부족할 경우 403 Forbidden을 반환하고, 정상적으로 요청이 처리되면 200 OK 등의 응답을 반환합니다.
AuthenticationManager를 통해 자격을 확인하고 JWT를 생성해 클라이언트로 전송합니다.Authorization 헤더에 담아 보냅니다.SecurityContextHolder에 인증 정보를 설정합니다.SecurityContextHolder에 저장된 인증 정보를 기반으로, 사용자가 요청한 API에 접근할 수 있는 권한이 있는지 확인하고 요청을 처리합니다.이렇게 JWT를 통한 인증 및 권한 부여가 이루어집니다.