Access 토큰과 Refresh 토큰
- 로그인 성공시 생명주기와 활용도가 다른 토큰 2개 발급 : Access/Refresh
Refresh 토큰 : Access 토큰이 만료되었을 때 재발급 받기 위한 용도로만 사용되며 약 24시간 이상의 긴 생명주기를 가진다.
Access 토큰 : 권한이 필요한 모든 요청 헤더에 사용될 JWT로 탈취 위험을 낮추기 위해 약 10분 정도의 짧은 생명주기를 가진다. - 권한이 필요한 모든 요청 : Access 토큰을 통해 요청
Access 토큰만 사용하여 요청하기 때문에 Refresh 토큰은 호출 및 전송을 빈도가 낮음. - 권한이 알맞다는 가정하에 2가지 상황 : 데이터 응답, 토큰 만료 응답
- 토큰이 만료된 경우 Refresh 토큰으로 Access 토큰 발급
Access 토큰이 만료되었다는 요청이 돌아왔을 경우 프론트엔드 로직에 의해 “1”에서 발급 받은 Refresh 토큰을 가지고 서버의 특정 경로(Refresh 토큰을 받는 경로)에 요청을 보내어 Access 토큰을 재발급 받는다. - 서버측에서는 Refresh 토큰을 검증 후 Access 토큰을 새로 발급한다.
Access/Refresh 토큰 저장 위치
클라이언트에서 발급 받은 JWT를 저장하기 위해 로컬 스토리지와 쿠키에 대해 많은 고려를 한다. 각 스토리지에 따른 특징과 취약점은 아래와 같다.
- 로컬 스토리지 : XSS 공격에 취약함 : Access 토큰 저장
- httpOnly 쿠키 : CSRF 공격에 취약함 : Refresh 토큰 저장
Access 토큰
Access 토큰은 주로 로컬 스토리지에 저장됩니다. 짧은 생명 주기로 탈취에서 사용까지 기간이 매우 짧고, 에디터 및 업로더에서 XSS를 방어하는 로직을 작성하여 최대한 보호 할 수 있지만 CSRF 공격의 경우 클릭 한 번으로 단시간에 요청이 진행되기 때문입니다.
권한이 필요한 모든 경로에 사용되기 때문에 CSRF 공격의 위험보다는 XSS 공격을 받는 게 더 나은 선택일 수 있습니다.
Refresh 토큰
Refresh 토큰은 주로 쿠키에 저장됩니다. 쿠키는 XSS 공격을 받을 수 있지만 httpOnly를 설정하면 완벽히 방어할 수 있습니다. 그럼 가장 중요한 CSRF 공격에 대해 위험하지 않을까라는 의구심이 생깁니다.
하지만 Refresh 토큰의 사용처는 단 하나인 토큰 재발급 경로입니다. CSRF는 Access 토큰이 접근하는 회원 정보 수정, 게시글 CRUD에 취약하지만 토큰 재발급 경로에서는 크게 피해를 입힐 만한 로직이 없기 때문입니다.
https://harrykang.tistory.com/entry/Spring-%EA%B0%9C%EC%9D%B8-Spring-Security-JWT-%EA%B8%B0%EB%B3%B8-%EC%85%8B%ED%8C%85
[Spring 개인] Spring Security JWT - 기본 셋팅
구분 회원가입 - 세션방식과 동일함 로그인(인증) - 세션은 서버 세션이 유저정보를 저장하지만 JWT는 토큰을 생성해서 응답 경로 접근(인가) - JWT Filter를 통해 요청의 헤더에서 JWT를 찾아서 검
harrykang.tistory.com
[Spring 개인] Spring Security JWT - 회원가입
JoinDTOpackage kr.bit.springsecurityjwt.dto;import lombok.Getter;import lombok.Setter;@Setter@Getterpublic class JoinDTO { private String username; private String password;} JoinControllerpackage kr.bit.springsecurityjwt.controller;import kr.bit.springsec
harrykang.tistory.com
[Spring 개인] Spring Security JWT - 로그인 검증
AuthenticationManger로그인필터 LoginFilterpackage kr.bit.springsecurityjwt.jwt;import jakarta.servlet.FilterChain;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.springframework.security.authe
harrykang.tistory.com
[Spring 개인] Spring Security - JWT 토큰 발급, 검증 구현
JWT 발급 및 검증로그인시 -> "성공!" -> JWT 발급접근시 -> JWT 검증 JWT (Json Web Token) 개요JWT는 JSON 기반의 토큰 형식으로, 클레임(Claim)이라는 사용자 정보가 포함된 JSON 객체를 인코딩하여 생성됩니
harrykang.tistory.com
이어서 구현한다
LoginFilter
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, 600000L);
String refresh = jwtUtil.createJwt("refresh", username, role, 86400000L);
//응담 설정
response.setHeader("access", access);
response.addCookie(createCookie("refresh", refresh));
response.setStatus(HttpStatus.OK.value());
}
JWTUtil
category 추가
package kr.bit.springsecurityjwt.jwt;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JWTUtil {
private SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}")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();
}
}
LoginFilter - createCookie
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;
}
}
리프레시 토큰 발급
access 토큰 발급
Access 토큰 필터
프론트의 API Client로 서버측에 요청을 보낸 후 데이터를 획득한다. 권한이 필요한 경우 Access토큰을 요청 헤더에 첨부하는데 Access 토큰 검증은 서버측 JWTFilter에 의해 진행된다.
이 때 Access 토큰이 만료된 경우 특정한 상태 코드 및 메세지를 응답해야된다.
JWTFilter
doFilter 변경
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 헤더에서 access키에 담긴 토큰을 꺼냄
String accessToken = request.getHeader("access");
// 토큰이 없다면 다음 필터로 넘김
if (accessToken == null) {
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);
UserEntity userEntity = new UserEntity();
userEntity.setUsername(username);
userEntity.setRole(role);
CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
Refresh로 Access 토큰 재발급
요청로직
ReissueController
package kr.bit.springsecurityjwt.controller;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kr.bit.springsecurityjwt.jwt.JWTUtil;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ReissueController {
private final JWTUtil jwtUtil;
public ReissueController(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@PostMapping("/reissue")
public ResponseEntity<?> reisuue(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);
}
//expired check
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
//response status code
return new ResponseEntity<>("refresh token expired", HttpStatus.BAD_REQUEST);
}
// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
//response status code
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
}
String username = jwtUtil.getUsername(refresh);
String role = jwtUtil.getRole(refresh);
//make new JWT
String newAccess = jwtUtil.createJwt("access", username, role, 600000L);
//response
response.setHeader("access", newAccess);
return new ResponseEntity<>(HttpStatus.OK);
}
}
SecurityConfig
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/reissue").permitAll()
.anyRequest().authenticated());
Refresh Rotate(Access 토큰 갱신 시 Refresh 토큰도 함께 갱신)
- Refresh 토큰 교체로 인한 보안성 강화
- 로그인 지속시간 길어짐
=> Rotate 이전에 발급받은 토큰 무효화가 중요!
ReissueController
코드추가
// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
//response status code
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
}
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);
//response
response.setHeader("access", newAccess);
response.addCookie(createCookie("refresh", newRefresh));
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;
}
}
POSTMAN을 통해서 토큰을 확인해본 결과
두 토큰 값이 다른것을 확인할 수 있다.
Refresh 토큰 서버측 저장
클라이언트에게 저장된 JWT가 탈취당하면 서버측에서 접근할 경우 JWT가 만료되기 전까지 서버측에서 막을 수 없으므로 프론트에서 토큰삭제 로그아웃을 구현해도 피해를 막을수 없다.
그래서 생명주기가 긴 Refresh 토큰은 발급시에 서버측 저장소에 기억 후 기억되어 있는 Refresh 토큰만 사용 할 수 있도록 한다.
RefreshEntity
package kr.bit.springsecurityjwt.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
@Entity
@Data
public class RefreshEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String refresh;
private String expiration;
}
RefreshRepository
package kr.bit.springsecurityjwt.repository;
import kr.bit.springsecurityjwt.entity.RefreshEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
public interface RefreshRepository extends JpaRepository<RefreshEntity, Long> {
Boolean existsByRefresh(String refresh);
@Transactional
void deleteByRefresh(String refresh);
}
LoginFilter
- RefreshRepository 의존성 주입
- successfulAuthentication() 일부 추가
- addRefreshEntity() 메소드 구현
package kr.bit.springsecurityjwt.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kr.bit.springsecurityjwt.dto.CustomUserDetails;
import kr.bit.springsecurityjwt.entity.RefreshEntity;
import kr.bit.springsecurityjwt.repository.RefreshRepository;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JWTUtil jwtUtil;
private RefreshRepository refreshRepository;
public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil, RefreshRepository refreshRepository) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.refreshRepository = refreshRepository;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
System.out.println(username);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
return authenticationManager.authenticate(authToken);
}
@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, 600000L);
String refresh = jwtUtil.createJwt("refresh", username, role, 86400000L);
//Refresh 토큰 저장
addRefreshEntity(username, refresh, 86400000L);
//응답 설정
response.setHeader("access", access);
response.addCookie(createCookie("refresh", refresh));
response.setStatus(HttpStatus.OK.value());
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
response.setStatus(401);
}
private void addRefreshEntity(String username, String refresh, Long expiredMs) {
Date date = new Date(System.currentTimeMillis() + expiredMs);
RefreshEntity refreshEntity = new RefreshEntity();
refreshEntity.setUsername(username);
refreshEntity.setRefresh(refresh);
refreshEntity.setExpiration(date.toString());
refreshRepository.save(refreshEntity);
}
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;
}
}
SecurityConfig
- RefreshRepository 의존성 주입
- LoginFilter 등록시 refreshRepository 의존성 주입
package kr.bit.springsecurityjwt.config;
import jakarta.servlet.http.HttpServletRequest;
import kr.bit.springsecurityjwt.jwt.JWTFilter;
import kr.bit.springsecurityjwt.jwt.JWTUtil;
import kr.bit.springsecurityjwt.jwt.LoginFilter;
import kr.bit.springsecurityjwt.repository.RefreshRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.Collections;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
private final RefreshRepository refreshRepository;
public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, RefreshRepository refreshRepository) {
this.authenticationConfiguration = authenticationConfiguration;
this.jwtUtil = jwtUtil;
this.refreshRepository = refreshRepository;
}
@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.setExposedHeaders(Collections.singletonList("Authorizaion"));
return configuration;
}
}));
//csrf disable
http
.csrf((auth) -> auth.disable());
//From 로그인 방식 disable
http
.formLogin((auth) -> auth.disable());
//http basic 인증 방식 disable
http
.httpBasic((auth) -> auth.disable());
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/reissue").permitAll()
.anyRequest().authenticated());
http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository), UsernamePasswordAuthenticationFilter.class);
//세션 설정
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
ReissueController
- RefreshRepository 의존성 주입
- PostMapping(”/reissue”) 경로 메소드
- Reissue DB 존재 검증
- Reissue rotate 삭제 및 저장
package kr.bit.springsecurityjwt.controller;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kr.bit.springsecurityjwt.entity.RefreshEntity;
import kr.bit.springsecurityjwt.jwt.JWTUtil;
import kr.bit.springsecurityjwt.repository.RefreshRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
public class ReissueController {
private final JWTUtil jwtUtil;
private final RefreshRepository refreshRepository;
public ReissueController(JWTUtil jwtUtil, RefreshRepository refreshRepository) {
this.jwtUtil = jwtUtil;
this.refreshRepository = refreshRepository;
}
@PostMapping("/reissue")
public ResponseEntity<?> reisuue(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);
}
//expired check
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
//response status code
return new ResponseEntity<>("refresh token expired", HttpStatus.BAD_REQUEST);
}
// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
//response status code
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
}
//DB에 저장되어 있는지 확인
Boolean isExist = refreshRepository.existsByRefresh(refresh);
if (!isExist) {
//response body
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
}
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);
//Refresh 토큰 저장 DB에 기존의 Refresh 토큰 삭제 후 새 Refresh 토큰 저장
refreshRepository.deleteByRefresh(refresh);
addRefreshEntity(username, newRefresh, 86400000L);
//response
response.setHeader("access", newAccess);
response.addCookie(createCookie("refresh", newRefresh));
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);
RefreshEntity refreshEntity = new RefreshEntity();
refreshEntity.setUsername(username);
refreshEntity.setRefresh(refresh);
refreshEntity.setExpiration(date.toString());
refreshRepository.save(refreshEntity);
}
}
로그아웃(Refresh 토큰 삭제)
CustomLogoutFilter
package kr.bit.springsecurityjwt.jwt;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kr.bit.springsecurityjwt.repository.RefreshRepository;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
public class CustomLogoutFilter extends GenericFilterBean {
private final JWTUtil jwtUtil;
private final RefreshRepository refreshRepository;
public CustomLogoutFilter(JWTUtil jwtUtil, RefreshRepository refreshRepository) {
this.jwtUtil = jwtUtil;
this.refreshRepository = refreshRepository;
}
@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);
}
}
SecurityConfig
http
.addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class);
'Frameworks > Spring(개인)' 카테고리의 다른 글
[Spring 개인] Web Server, Web Application Sever (1) | 2024.06.08 |
---|---|
[Spring 개인] 스프링 컨테이너 / 스프링 빈 조회 (0) | 2024.06.07 |
[Spring 개인] Spring Security OAuth2 + JWT (0) | 2024.05.29 |
[Spring 개인] Spring Security OAuth2.0(세션 클라이언트) (0) | 2024.05.28 |
[Spring 개인] Spring Security OAuth2.0(네이버, 구글, 카카오) II (0) | 2024.05.28 |