
이전 포스트 Spring Security, JWT에서 이론적인 부분을 다루었습니다. 이번 포스트에서는 간단하게 정리하고 코드 위주로 보여드리겠습니다.
OS_CSPM 팀 프로젝트에서 구현한 JWT로 로그인 세션 관리하는 방식에대해 설명하겠습니다.
Spring을 활용하여 개발한 웹 어플리케이션들은 일부 혹은 모든 사용자에게 서비스를 제공하기에 보안 필수입니다.
Spring Security 에서 모든 보안처리는 Filter의 집합(SecurityFilterChain)을 통해 동작

start.spring.io 에서 Spring Security 의존성을 추가해줍니다.
의존성을 추가하지 못했다면 의존성에 아래와 같이 추가해 주면 됩니다.
implementation 'org.springframework.boot:spring-boot-starter-security'
설정을 모아두는 폴더가 있다면 그 아래에 SecurityConfig 파일을 생성해 줍니다.

이렇게 설정하면 모든 코드를 다 작성하게 되었습니다.
제가 완성한 코드입니다. 이것만 있다고 실행되지 않으니 꼭 마지막까지 따라서 해주시면 문제없이 동작할 겁니다.
/confing/SecurityConfing
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity // 스프링 스큐리티 필터가 스프링 필터체인에 등록이 됩니다.
public class SecurityConfig {
//AuthenticationManager가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입
private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
private final RefreshRepository refreshRepository;
private final MemberRepository memberRepository;
//AuthenticationManager Bean 등록
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors((cors -> cors.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
// 헤더 cors에 허용
configuration.setExposedHeaders(Arrays.asList("access", "Set-Cookie"));
return configuration;
}
})) );
//csrf disable
http
.csrf(AbstractHttpConfigurer::disable);
// From 로그인 방식 disable
http
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable);
// 경로별 인가 작업
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
.requestMatchers("/api/account/**").permitAll()
.requestMatchers("/login").permitAll()
.requestMatchers("/reissue").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
);
http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository, memberRepository), UsernamePasswordAuthenticationFilter.class);
http
.addFilterBefore(new CustomLogoutFilter(refreshRepository, jwtUtil), LogoutFilter.class);
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
}
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor: final로 선언된 필드를 자동으로 주입하는 생성자를 생성하는 Lombok 어노테이션@Configuration: 스프링 설정 파일임을 나타냄 (중요!!)@EnableWebSecurity: Spring Security 필터가 스프링 필터 체인에 등록됨을 의미 이 어노테이션이 없다면 이전 버전입니다. private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
private final RefreshRepository refreshRepository;
private final MemberRepository memberRepository;
AuthenticationConfiguration: AuthenticationManager를 생성할 때 사용됨JWTUtil: JWT 관련 기능을 제공하는 유틸리티 클래스 (토큰 생성, 검증 등)RefreshRepository: 리프레시 토큰을 관리하는 저장소 (DB 또는 In-Memory 저장소)MemberRepository: 사용자 정보를 관리하는 JPA 리포지토리.http
.cors((cors -> cors.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
configuration.setExposedHeaders(Arrays.asList("access", "Set-Cookie"));
return configuration;
}
})));
CORS 설정하지 않으면 토큰이 프론트로 응답이 안되어서 생기는 문제가 생깁니다 꼭 해줍시다.
setAllowedMethods("*"): 모든 HTTP 메서드 허용, "POST", "GET" 이렇게도 가능setAllowCredentials(true): 쿠키를 포함한 요청 허용.setExposedHeaders(Arrays.asList("access", "Set-Cookie")) : 브라우저가 읽을 수 있는 응답 헤더 지정http
.csrf(AbstractHttpConfigurer::disable);
http
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable);
httpBasic().disable() : HTTP Basic 인증 비활성화formLogin().disable(): 기본 로그인 폼 비활성화http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
.requestMatchers("/api/account/**").permitAll()
.requestMatchers("/login").permitAll()
.requestMatchers("/reissue").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
);
permitAll(): 누구나 접근 가능한 경로 지정 (Swagger API, 회원가입 및 로그인 관련 API 허용)hasRole("ADMIN"): /admin 경로는 ADMIN 권한이 있는 사용자만 접근 가능.anyRequest().authenticated(): 그 외 모든 요청은 인증 필요http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
JWTFilter를 LoginFilter 앞에 배치JWTFilter는 JWT 토큰을 검증하여 사용자 인증을 수행http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository, memberRepository), UsernamePasswordAuthenticationFilter.class);
LoginFilter: UsernamePasswordAuthenticationFilter 위치에 추가LoginFilter는 사용자 로그인 요청을 처리하고 JWT를 생성http
.addFilterBefore(new CustomLogoutFilter(refreshRepository, jwtUtil), LogoutFilter.class);
CustomLogoutFilter를 LogoutFilter 앞에 배치.http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
SessionCreationPolicy.STATELESS: 세션을 사용하지 않음JWT 기반 로그아웃 기능을 구현하는 필터입니다.
사용자가 /logout 엔드포인트로 POST 요청을 보내면, 제검증 토큰을 삭제하고 세션을 종료합니다.
/jwt/CustomLogoutFilter
@RequiredArgsConstructor
public class CustomLogoutFilter extends GenericFilterBean {
private final RefreshRepository refreshRepository;
private final JWTUtil jwtUtil;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
//path and method verify
String requestUri = request.getRequestURI();
if (!requestUri.matches("^\\/logout$")) {
filterChain.doFilter(request, response);
return;
}
String requestMethod = request.getMethod();
if (!requestMethod.equals("POST")) {
filterChain.doFilter(request, response);
return;
}
//get refresh token
String refresh = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
}
}
//refresh null check
if (refresh == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
//expired check
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
//response status code
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
//response status code
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
//DB에 저장되어 있는지 확인
Boolean isExist = refreshRepository.existsByRefresh(refresh);
if (!isExist) {
//response status code
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
//로그아웃 진행
//Refresh 토큰 DB에서 제거
refreshRepository.deleteByRefresh(refresh);
//Refresh 토큰 Cookie 값 0
Cookie cookie = new Cookie("refresh", null);
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
response.setStatus(HttpServletResponse.SC_OK);
}
}
@RequiredArgsConstructor
public class CustomLogoutFilter extends GenericFilterBean {
extends GenericFilterBean: Spring Security의 필터로 동작하도록 GenericFilterBean을 확장private final RefreshRepository refreshRepository;
private final JWTUtil jwtUtil;
RefreshRepository: Refresh 토큰을 저장하고 관리하는 Repository(DB에서 조회 및 삭제)JWTUtil: JWT 관련 유틸 클래스(토큰 만료 여부 확인, 토큰 정보 추출)@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
HttpServletRequest, HttpServletResponse로 캐스팅 후, 아래 doFilter 메서드를 실행.private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
//path and method verify
String requestUri = request.getRequestURI();
if (!requestUri.matches("^\\/logout$")) {
filterChain.doFilter(request, response);
return;
}
String requestMethod = request.getMethod();
if (!requestMethod.equals("POST")) {
filterChain.doFilter(request, response);
return;
}
//get refresh token
String refresh = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
}
}
//refresh null check
if (refresh == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
//DB에 저장되어 있는지 확인
Boolean isExist = refreshRepository.existsByRefresh(refresh);
if (!isExist) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 로그아웃 진행
// Refresh 토큰 DB에서 제거
refreshRepository.deleteByRefresh(refresh);
// Refresh 토큰 Cookie 값 0
Cookie cookie = new Cookie("refresh", null);
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
response.setStatus(HttpServletResponse.SC_OK);
클라이언트가 요청 시 보낸 JWT Access 토큰을 검증하는 역할을 합니다.
/jwt/JWTFilter
@Slf4j
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 헤더에서 access키에 담긴 토큰을 꺼냄
String accessToken = request.getHeader("access");
log.info("토큰을 꺼냄");
// 토큰이 없다면 다음 필터로 넘김
if (accessToken == null) {
log.info("토큰이 없음");
filterChain.doFilter(request, response);
return;
}
// 토큰 만료 여부 확인, 만료시 다음 필터로 넘기지 않음
try {
jwtUtil.isExpired(accessToken);
} catch (ExpiredJwtException e) {
//response body
PrintWriter writer = response.getWriter();
writer.print("access token expired!!");
//response status code
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
// 토큰이 access인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(accessToken);
if (!category.equals("access")) {
//response body
PrintWriter writer = response.getWriter();
writer.print("invalid access token");
//response status code
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
// username, role 값을 획득
String username = jwtUtil.getUsername(accessToken);
String role = jwtUtil.getRole(accessToken);
Member member = new Member();
member.setEmail(username);
member.setRole(role);
CustomUserDetails customUserDetails = new CustomUserDetails(member);
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
@Slf4j
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
extends OncePerRequestFilter: 요청당 한 번만 실행되는 필터 (Spring Security 필터 체인에서 사용)@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request, response를 받아서 JWT 검증 후 SecurityContext에 인증 정보 저장// 헤더에서 access키에 담긴 토큰을 꺼냄
String accessToken = request.getHeader("access");
log.info("토큰을 꺼냄");
// 토큰이 없다면 다음 필터로 넘김
if (accessToken == null) {
log.info("토큰이 없음");
filterChain.doFilter(request, response);
return;
}
// 토큰 만료 여부 확인, 만료시 다음 필터로 넘기지 않음
try {
jwtUtil.isExpired(accessToken);
} catch (ExpiredJwtException e) {
// response body
PrintWriter writer = response.getWriter();
writer.print("access token expired!!");
// response status code
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
// 토큰이 access인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(accessToken);
if (!category.equals("access")) {
// response body
PrintWriter writer = response.getWriter();
writer.print("invalid access token");
// response status code
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
"access" 토큰인지 검증String username = jwtUtil.getUsername(accessToken);
String role = jwtUtil.getRole(accessToken);
Member member = new Member();
member.setEmail(username);
member.setRole(role);
CustomUserDetails customUserDetails = new CustomUserDetails(member);
UserDetails 구현체로 변환Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
UsernamePasswordAuthenticationToken을 사용하여 Authentication 객체 생성.filterChain.doFilter(request, response);
JWT 토큰을 생성 및 검증하는 역할을 수행합니다.
/jwt/JWTUtil/
@Component
public class JWTUtil {
private SecretKey secretKey;
public JWTUtil(@Value("${jwt.secretKey}") String secret) {
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}
public String getUsername(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}
public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}
public String getCategory(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("category", String.class);
}
public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
public String createJwt(String category,String username, String role, Long expiredMs) {
return Jwts.builder()
.claim("category", category)
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
@Component
public class JWTUtil {
@Component: Spring 빈으로 등록되어 의존성 주입 가능private SecretKey secretKey;
public JWTUtil(@Value("${jwt.secretKey}") String secret) {
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),
Jwts.SIG.HS256.key().build().getAlgorithm());
}
@Value("${jwt.secretKey}")public String getUsername(String token) {
return Jwts.parser()
.verifyWith(secretKey) // 서명 검증
.build()
.parseSignedClaims(token)
.getPayload()
.get("username", String.class);
}
public String getRole(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("role", String.class);
}
public String getCategory(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("category", String.class);
}
getUsername: 이메일 추출getRole: 권한 추출getCategory: 카테코리 정보 추출public Boolean isExpired(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.before(new Date());
}
public String createJwt(String category, String username, String role, Long expiredMs) {
return Jwts.builder()
.claim("category", category) // 토큰 타입 (Access/Refresh)
.claim("username", username) // 사용자 이메일
.claim("role", role) // 권한 (예: "USER", "ADMIN")
.issuedAt(new Date(System.currentTimeMillis())) // 발급 시간
.expiration(new Date(System.currentTimeMillis() + expiredMs)) // 만료 시간
.signWith(secretKey) // 서명 (HMAC SHA-256)
.compact();
}
claim(키, 값) : 토큰에 키와 값 입력.issuedAt(new Date()): 토큰 발급 시간 설정.expiration(new Date(System.currentTimeMillis() + expiredMs)) : 만료 시간 설정.signWith(secretKey): 서명을 생성하여 토큰의 무결성 보장사용자가 로그인할 때 실행되는 Spring Security 필터입니다.
사용자 인증을 수행하고, JWT 토큰을 생성하여 응답으로 반환하는 역할을 합니다.
/jwt/LoginFilter
@Slf4j
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JWTUtil jwtUtil;
private final RefreshRepository refreshRepository;
private final MemberRepository memberRepository;
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
LoginRequestDto loginDTO = new LoginRequestDto();
try {
ObjectMapper objectMapper = new ObjectMapper();
ServletInputStream inputStream = req.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
loginDTO = objectMapper.readValue(messageBody, LoginRequestDto.class);
//클라이언트 요청에서 username, password 추출
}catch(IOException e) {
throw new RuntimeException(e);
}
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
System.out.println(username);
//스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password, null);
//token에 담은 검증을 위한 AuthenticationManager로 전달
return authenticationManager.authenticate(authRequest);
}
//로그인 성공시 실행하는 메소드 (여기서 JWT를 발급하면 됨)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
//유저 정보
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
//토큰 생성
String access = jwtUtil.createJwt("access", username, role, 3600000L); // 수정
String refresh = jwtUtil.createJwt("refresh", username, role, 86400000L);
//Refresh 토큰 저장
addRefreshEntity(username, refresh, 86400000L);
log.info("로그인 성공 : " + username );
log.info("AccessToken 생성 시간 - {}", Instant.now());
//응답 설정
response.setHeader("access", access);
response.addCookie(createCookie("refresh", refresh));
response.setStatus(HttpStatus.OK.value());
}
//로그인 실패시 실행하는 메소드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
System.out.println("로그인 안됨");
response.setStatus(401); // 기본적으로 인증 실패 시 401 반
}
private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24*60*60);
//cookie.setSecure(true);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
private void addRefreshEntity(String username, String refresh, Long expiredMs) {
Date date = new Date(System.currentTimeMillis() + expiredMs);
LocalDateTime expirationDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
RefreshEntity refreshEntity = new RefreshEntity();
refreshEntity.setMember(memberRepository.findByEmail(username).orElseThrow());
refreshEntity.setRefresh(refresh);
refreshEntity.setExpiration(expirationDateTime);
refreshRepository.save(refreshEntity);
}
}
attemptAuthentication() - 로그인 요청 처리@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
LoginRequestDto loginDTO = new LoginRequestDto();
try {
ObjectMapper objectMapper = new ObjectMapper();
ServletInputStream inputStream = req.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
loginDTO = objectMapper.readValue(messageBody, LoginRequestDto.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password, null);
return authenticationManager.authenticate(authRequest);
UsernamePasswordAuthenticationToken을 생성하여 스프링 시큐리티 인증 매니저에 전달authenticationManager.authenticate(authRequest)를 호출하여 사용자 인증 수행.successfulAuthentication() - 로그인 성공 시 실행@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
String access = jwtUtil.createJwt("access", username, role, 3600000L);
String refresh = jwtUtil.createJwt("refresh", username, role, 86400000L);
addRefreshEntity(username, refresh, 86400000L);
response.setHeader("access", access);
response.addCookie(createCookie("refresh", refresh));
response.setStatus(HttpStatus.OK.value());
unsuccessfulAuthentication() - 로그인 실패 시 실행@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
System.out.println("로그인 안됨");
response.setStatus(401);
}
createCookie() - Refresh 토큰을 쿠키에 저장private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24 * 60 * 60);
// cookie.setSecure(true);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
addRefreshEntity() - Refresh 토큰을 DB에 저장private void addRefreshEntity(String username, String refresh, Long expiredMs) {
토근 재발급 컨트롤러입니다.
기존 Access 토큰이 만료되었을때, 저장된 Refresh 토큰을 이용하여 새로운 Access 토큰을 발급합니다.
/controller/ReissueController
@Slf4j
@RestController
@RequiredArgsConstructor
public class ReissueController {
private final RefreshService refreshService;
@PostMapping("/reissue")
public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {
log.info("토큰 재발급");
return refreshService.reissue(request, response);
}
}
@PostMapping("/reissue"): /reissue 경로로 POST 요청을 받을 때 실행ResponseEntity로 클라이언트에게 응답JWT Access 토큰을 재발급하는 서비스 클래스입니다.
Refresh 토큰을 검증하고, 새로운 Access and Refresh 토큰을 발급합니다.
토큰 재발급 서비스
/service/RefreshService
@Service
@Slf4j
@RequiredArgsConstructor
public class RefreshService {
private final RefreshRepository refreshRepository;
private final JWTUtil jwtUtil;
private final MemberRepository memberRepository;
public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {
//get refresh token
String refresh = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
}
}
if (refresh == null) {
//response status code
return new ResponseEntity<>("refresh token null", HttpStatus.BAD_REQUEST); //400
}
//expired check
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
//response status code
return new ResponseEntity<>("refresh token expired", HttpStatus.BAD_REQUEST); //400
}
// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
//response status code
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST); //400
}
//DB에 저장되어 있는지 확인
Boolean isExist = refreshRepository.existsByRefresh(refresh);
if (!isExist) {
log.info("refresh 토큰 만료");
//response body
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST); //400
}
String username = jwtUtil.getUsername(refresh);
String role = jwtUtil.getRole(refresh);
//make new JWT
String newAccess = jwtUtil.createJwt("access", username, role, 600000L);
String newRefresh = jwtUtil.createJwt("refresh", username, role, 86400000L);
log.info("새로운 토큰 발급");
//Refresh 토큰 저장 DB에 기존의 Refresh 토큰 삭제 후 새 Refresh 토큰 저장
refreshRepository.deleteByRefresh(refresh);
addRefreshEntity(username, newRefresh, 86400000L);
log.info("refresh 토큰 데이터베이스에 저장");
//response
response.setHeader("access", newAccess);
response.addCookie(createCookie("refresh", newRefresh));
log.info("access, refresh 토큰 헤더에 저장, 쿠키에 저장");
System.out.println("정상적으로 발급됨");
return new ResponseEntity<>(HttpStatus.OK);
}
private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24*60*60);
// cookie.setSecure(true);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
private void addRefreshEntity(String username, String refresh, Long expiredMs) {
Date date = new Date(System.currentTimeMillis() + expiredMs);
LocalDateTime expirationDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
RefreshEntity refreshEntity = new RefreshEntity();
refreshEntity.setMember(memberRepository.findByEmail(username).orElseThrow());
refreshEntity.setRefresh(refresh);
refreshEntity.setExpiration(expirationDateTime);
refreshRepository.save(refreshEntity);
}
public String getEmail(HttpServletRequest request){
String refresh = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
}
}
if (refresh == null) {
log.info("refresh 토큰이 없습니다.");
return null;
}
return jwtUtil.getUsername(refresh);
}
}
reissue() - 토큰 재발급 메서드//get refresh token
String refresh = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
}
}
if (refresh == null) {
return new ResponseEntity<>("refresh token null", HttpStatus.BAD_REQUEST); // 400
}
// expired check
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
return new ResponseEntity<>("refresh token expired", HttpStatus.BAD_REQUEST); // 400
}
jwtUtil.isExpired(refresh)를 호출하여 토큰이 만료되었는지 검사// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST); // 400
}
jwtUtil.getCategory(refresh)를 이용해 토큰 타입을 확인Boolean isExist = refreshRepository.existsByRefresh(refresh);
if (!isExist) {
log.info("refresh 토큰 만료");
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST); // 400
}
String username = jwtUtil.getUsername(refresh);
String role = jwtUtil.getRole(refresh);
//make new JWT
String newAccess = jwtUtil.createJwt("access", username, role, 600000L);
String newRefresh = jwtUtil.createJwt("refresh", username, role, 86400000L);
log.info("새로운 토큰 발급");
//Refresh 토큰 저장 DB에 기존의 Refresh 토큰 삭제 후 새 Refresh 토큰 저장
refreshRepository.deleteByRefresh(refresh);
addRefreshEntity(username, newRefresh, 86400000L);
log.info("refresh 토큰 데이터베이스에 저장");
//response
response.setHeader("access", newAccess);
response.addCookie(createCookie("refresh", newRefresh));
log.info("access, refresh 토큰 헤더에 저장, 쿠키에 저장");
System.out.println("정상적으로 발급됨");
return new ResponseEntity<>(HttpStatus.OK);
정상적으로 완료되면 200 OK 응답
createCookie() - 쿠키 생성 메서드private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24*60*60);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
쿠키 생성 만료시간을 24시간으로 설정
addRefreshEntity() - Refresh 토큰 DB 저장private void addRefreshEntity(String username, String refresh, Long expiredMs) {
Date date = new Date(System.currentTimeMillis() + expiredMs);
LocalDateTime expirationDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
RefreshEntity refreshEntity = new RefreshEntity();
refreshEntity.setMember(memberRepository.findByEmail(username).orElseThrow());
refreshEntity.setRefresh(refresh);
refreshEntity.setExpiration(expirationDateTime);
refreshRepository.save(refreshEntity);
}
getEmail - Refresh 토큰에서 이메일 추출public String getEmail(HttpServletRequest request){
String refresh = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
}
}
if (refresh == null) {
log.info("refresh 토큰이 없습니다.");
return null;
}
return jwtUtil.getUsername(refresh);
}
요청에서 Refresh 토큰을 가져와 username(이메일)을 반환
이제 실제로 로그인에 성공하면 Access and Refresh 토큰이 응답이 되는지 확인해 봅니다.
Postman으로 로그인하기

로그인 성공

DB에 refresh 토큰이 저장된 모습 확인

토큰이 있을때

토큰이 없을때


Access 토큰이 만료 새 Access 토큰 발급할때는 access 토큰은 제외합니다.
응답으로 access 토큰이 오는지 확인합니다.

DB에 기존 Refresh 토큰을 삭제하고 새로운 Refresh 토큰이 저장이 되었는지 확인합니다.

이렇게하여 Spring Boot에서 Spring Security로 JWT 토큰을 활용하여 세션을 관리하는 방법에 대해서 자세히 다루어 보았습니다.
[참고]