앞서, 스프링 시큐리티에서 JSESSION기반 로그인을 처리하는 과정에 대해 간단히 알아보자.
1. 클라이언트의 로그인 요청
POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
username=user&password=password123
2. 서버의 응답
HTTP/1.1 302 Found
Location: /home
Set-Cookie: JSESSIONID=1234567890abcdef; Path=/; HttpOnly
로그인 성공 시 서버는 클라이언트로 세션ID를 반환해주고, 서버에 인증정보를 저장해 관리하게 된다.
과정을 조금 더 자세히 보면 다음과 같다.
로그인 요청
-> UsernamePasswordAuthenticationFilter: 로그인 요청인지 확인
입력데이터(id,pw) 기반으로 UsernamePasswordAuthenticationToken객체 생성
->AuthenticationManager로 Token 전달
->AuthenticationManager는 AuthenticationProvider로 위임
->Provider가 UserDetailsService호출
->UserDetailsService가 UserDetails객체 반환
->이후 PasswordEncoder를 사용해 입력값과 db비밀번호 비교
->인증성공
JWT기반 인증을 위해, 기존 세션 기반 인증에서 사용되는 UsernamePasswordAuthenticationFilter를 상속받아 JWT 생성 및 반환기능을 재정의해야한다. 그리고 이 필터가 우리가 사용할 LoginFilter가 될 것이다.
그럼 먼저 JWT를 발급하고 검증하는 JWTUtil 클래스를 구현해보자.
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
build.gradle에 jwt 의존성 추가
spring.jwt.secret=your_secret_key;
properties에 시크릿 키 추가(your_secret_key 자리에 본인의 키로 대체)
io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 128 bits which is not secure enough for any JWT HMAC-SHA algorithm. The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size). Consider using the Jwts.SIG.HS256.key() builder (or HS384.key() or HS512.key()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.
secret key의 길이가 짧다면 위와 같 에러가 발생한다.
JWTUtil
package com.example.jwt_practice.security;
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.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JWTUtil {
private final SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}") String secret)
{
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateJwt(String username, Long expiredMs)
{
return Jwts.builder()
.claim("username",username)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
public String getUsernameFromToken(String token)
{
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("username",String.class);
}
public boolean isExpired(String token)
{
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.before(new Date());
}
}
- generateJwt(): 파라미터로 username, expiredMs를 받아 토큰 생성
- get~FromToken(): 토큰을 검증해 토큰으로부터 ~를 얻음
JWT는 클라이언트 쪽에서 쉽게 디코딩될 수 있으므로, 민감한 정보를 담지 않아야 하며, 주로 사용자 식별자(username) 또는 권한 정보(role) 정도를 포함해야함.
JWTUtil클래스가 구현되었으니, JWTUtil을 사용하는 LoginFilter를 구현해보자.
LoginFilter는 3개의 메서드로 구성된다.
- attemptAuthentication: 로그인 요청 처리
로그인 요청시 UsernamePasswordAuthenticationToken을 생성해 authenticationManager로 위임
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException
{
if(request.getContentType().equals("application/json"))
{
try{
Map credentials = objectMapper.readValue(request.getInputStream(),Map.class);
String username = credentials.get("username").toString();
String password = credentials.get("password").toString();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,password);
return authenticationManager.authenticate(authRequest);
} catch (IOException e)
{
throw new RuntimeException(e);
}
}
//x-www-form-urlencoded
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password, null);
return authenticationManager.authenticate(authRequest);
}
json형식의 로그인 요청도 처리할 수 있도록 ObjectMapper를 주입받아 사용한다.
username과 password로 UsernamePasswordAuthenticationToken을 생성한 뒤, AuthenticationManager로 위임.
- successfulAuthentication: 인증 성공 시 JWT 토큰 생성 및 반환
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException
{
UserDetails userDetails = (UserDetails) authResult.getPrincipal();
String username = userDetails.getUsername();
//액세스 토큰 발급(10분)
String token = jwtUtil.generateJwt(username,1000*60*10L); //10분
response.setStatus(HttpServletResponse.SC_OK);
response.addHeader("Authorization", "Bearer " + token);
// 리프레시 토큰 발급 (7일 유효)
String refreshToken = jwtUtil.generateJwt(username, 1000 * 60 * 60 * 24 * 7L);
Cookie refreshCookie = new Cookie("refreshToken", refreshToken);
refreshCookie.setHttpOnly(true); // JavaScript 접근 금지
refreshCookie.setSecure(true); // HTTPS에서만 전송
refreshCookie.setPath("/"); // 쿠키 경로 설정
refreshCookie.setMaxAge(60 * 60 * 24 * 7); // 7일 유효
response.addCookie(refreshCookie);
}
로그인 성공 시, 액세스 토큰과 리프레시 토큰 두 개를 발급해 응답에 포함시켜준다.
액세스 토큰의 유효기간은 10분으로, 응답 헤더에 넣어주고
리프레시 토큰의 유효기간은 7일로, 쿠키에 넣어주며, HTTP Only로 js에서의 접근을 막아준다.
- unsuccessfulAuthentication: 인증 실패 시 처리
인증 실패시, 적절한 응답을 반환할 수 있도록 구현했다.
LoginFilter
package com.example.jwt_practice.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.util.Map;
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JWTUtil jwtUtil;
private final ObjectMapper objectMapper;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException
{
if(request.getContentType().equals("application/json"))
{
try{
Map credentials = objectMapper.readValue(request.getInputStream(),Map.class);
String username = credentials.get("username").toString();
String password = credentials.get("password").toString();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,password);
return authenticationManager.authenticate(authRequest);
} catch (IOException e)
{
throw new RuntimeException(e);
}
}
//x-www-form-urlencoded
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password, null);
return authenticationManager.authenticate(authRequest);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException
{
UserDetails userDetails = (UserDetails) authResult.getPrincipal();
String username = userDetails.getUsername();
//액세스 토큰 발급(10분)
String token = jwtUtil.generateJwt(username,1000*60*10L); //10분
response.setStatus(HttpServletResponse.SC_OK);
response.addHeader("Authorization", "Bearer " + token);
// 리프레시 토큰 발급 (7일 유효)
String refreshToken = jwtUtil.generateJwt(username, 1000 * 60 * 60 * 24 * 7L);
Cookie refreshCookie = new Cookie("refreshToken", refreshToken);
refreshCookie.setHttpOnly(true); // JavaScript 접근 금지
refreshCookie.setSecure(true); // HTTPS에서만 전송
refreshCookie.setPath("/"); // 쿠키 경로 설정
refreshCookie.setMaxAge(60 * 60 * 24 * 7); // 7일 유효
response.addCookie(refreshCookie);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException
{
if(failed instanceof BadCredentialsException)//401
{
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
else if(failed instanceof InternalAuthenticationServiceException)//서버오류
{
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
else
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);//그외
}
}
}
로그인 필터 구현이 완료되었으니, 기존 UsernamePasswordAuthenticationFilter를 LoginFilter로 대체해야한다.
public class SecurityConfig{
private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
private final ObjectMapper objectMapper;
LoginFilter 생성을 위해 필요한 의존성을 주입하기위해 다음 세 필드를 추가했다.
AuthenticationConfiguration과 ObjectMapper는 스프링에서 자동으로 생성해주는 스프링 빈이며, JWTUtil의 경우, @Component 로 등록했기에 주입받을 수 있다.
필터 대체는 다음과 같이 해주면 된다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception
{
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, objectMapper),
UsernamePasswordAuthenticationFilter.class);
JWT 토큰 발급을 구현했으니, 잘 작동하는 지 확인해보면

두 토큰 모두 정상적으로 발급되었음이 확인 가능하다.
다음으로
JWT발급은 가능하지만, JWT를 검증하는 필터는 구현되지 않아 아직 인증이 불가능하다. JWT 검증 과정은 이후 구현될 JWTFilter를 통해 처리될 예정이다. 추가로, 리프레시 토큰을 이용한 액세스 토큰 갱신도 다룰 예정이다.
'스프링' 카테고리의 다른 글
| [Spring Security] @PreAuthorize (0) | 2026.03.28 |
|---|---|
| AWS S3: Post Presign Url이 만료되지 않는 문제 (0) | 2026.01.20 |
| 스프링 JWT(3): JWTFilter (3) | 2025.01.09 |
| 스프링 JWT(1): 프로젝트 생성 (2) | 2025.01.06 |