이 글은
스프링 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
를 이용해서 제작하였고, JWT발급방식은
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
JWT 0.12.3입니다.
소셜로그인 순서도
1. 클라이언트에서 spring-boot-starter-oauth2-client에 저장되어 있는 로그인창 주소인
window.location.href = "http://localhost:8080/oauth2/authorization/kakao"
window.location.href = "http://localhost:8080/oauth2/authorization/naver"
OAuth2AuthorizationRequestRedirectFilter에서 필터에 로그인 요청 기본 주소는
기본 요청 경로는 /oauth2/authorization/{registrationId} 이다.
2. 주소로 넘어오면 OAuth2AuthorizationRequestRedirectFilter가 요청을 잡아서 외부 인증로그인서버에 요청을 대신 보내준다.
2-1. OAuth2AuthorizationRequestRedirectFilter 의 내부에서는 authorizationRequestResolver가 resolve() 과정을 통해 최종적으로 OAuth2AuthorizationRequest 를 반환함.
resolve()과정이란?
OAuth2AuthorizationRequest를 만들어내는것(객체에 값을 담는것)
2-2. OAuth2AuthorizationRequest객체란? 모든 인증 정보가 담겨있는 객체임
예시로) registerationId = kakao, redirectUriAction= login
등이 담기고 kakao로 넘어온 정보를 통해서 application.yml에 있는 clientRegistration정보를 치환함.
spring.security.oauth2.client.registration.kakao.client-id=3f03f1f9a33bf28416a4ecd7d6deba5a
이런 식으로 담겨있는 값이 registerationId = 3f03f1f9a33bf28416a4ecd7d6deba5a 에 들어감에 따라 어떤 디벨로퍼인지 구별하는 주소를 만들어내는 거임. 의 resolve과정을 진행하여 최종 객체를 만들면
https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=69779556a1e86bbc4883911ac6062eb8&scope=profile%20talk_message&state=Lf_Z7buQNi87ryfmqOMtJ497_9hziNqpqRR5M3QxZjA%3D&redirect_uri=http://localhost:8080/login/oauth2/code/kakao
이런 식의 uri가 만들어짐.
3. 인증서버는 리다이렉트 요청하는 곳으로 로그인페이지를 리다이렉트 시켜줌
4. 로그인페이지에서 로그인 성공을 하면 디벨로퍼에 등록해 둔 리다이렉트 URI로 인가코드가 넘어옴.
5. 넘어올 때 OAuth2LoginAuthenticationFilter가 가로채게 된다.
6. 인가코드를 꺼내서 그 뒷 필터인 OAuth2LoginAuthenticationProvider가 꺼냄.
7. OAuth2LoginAuthenticationProvider는 다시 인증서버로 가서 인가코드로 엑세스 토큰발급
8. OAuth2LoginAuthenticationProvider는 엑세스토큰을 가지고 리소스 서버로 가서 유저 정보를 획득한다.
9. 유저정보를 획득하면 OAuth2LoginAuthenticationProvider에서 호출하는 OAuth2Service에서 유저 정보를 알맞게 파싱하고
OAuth2User에 담아서 DB에 저장한다.
10. 로그인이 성공하면 LoginSuccessHandler가 JWT를 발급하여 유저 브라우저에 set-cookie에(엑세스 토큰, 리프레시 토큰)을 보낸다. 또한 JWT를 여기서 발급하기에 해당하는 유저 DB에 리프레시 토큰을 저장해 둔다(or Redis에 저장)
11. 브라우저 쿠키에 토큰을 발급받은 클라이언트는 앞으로 백엔드에 접근할 때 모든 요청에 대해서 credentials include 하여 보냄으로써 토큰을 계속 넘겨주고 가장 먼저 JWT필터에서 검증한다.
12. 이때, JWTFilter는 JWT 검증 후 내부에 임시적인 세션을 만들어서 그 세션을 참고해서 들고 다니면서 API작업을 처리하며 작업이 끝나면 세션을 종료시킨다.
소셜 로그아웃
로그아웃은 3가지를 진행해야 한다.
1. 브라우저에 있는 쿠키(엑세스토큰, 리프레시토큰) 지우기
2. 카카오처럼 리소스 서버의 엑세스토큰 만료시키기 - 각각의 리소스마다 로그아웃 방법 상이
3. WAS에서 관리하는 JWT 리프레시 토큰 만료시키기
코드레벨에서 바라보기
securityConfig.java
@RequiredArgsConstructor
@EnableMethodSecurity
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
private final JWTUtil jwtUtil;
private final CustomSignOutProcessHandler customSignOutProcessHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors(corsCustomizer -> corsCustomizer.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(Collections.singletonList("Authorization"));
return configuration;
}
}));
// CSRF 보호 비활성화
http.csrf(AbstractHttpConfigurer::disable);
// 폼 로그인 비활성화
http.formLogin(AbstractHttpConfigurer::disable);
// HTTP Basic 인증 비활성화
http.httpBasic(AbstractHttpConfigurer::disable);
// OAuth2 로그인 설정
http.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
// .failureHandler(oAuth2LoginFailureHandler) # 실패핸들러 추가하기
);
// 로그아웃 설정
http.logout(logout -> logout.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(customSignOutProcessHandler)
.deleteCookies("JSESSIONID", "Authorization", "RefreshToken")
);
// JWT 필터 설정
http.addFilterBefore(new JWTFilter(jwtUtil), LogoutFilter.class); // 로그아웃 필터전에 jwt필터실행
http.addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
// http.addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class);
// 경로별 인가 작업
http.authorizeHttpRequests(auth -> auth
.requestMatchers(WHITE_LIST_URL).permitAll()
.anyRequest().authenticated());
// 세션 설정: STATELESS
http.sessionManagement(session -> session.sessionCreationPolicy(STATELESS));
return http.build();
}
private static final String[] WHITE_LIST_URL = {
// "/api/v1/auth/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/webjars/**",
"/swagger-ui.html"
};
}
처럼 의존성을 추가하게 되면 security가 요청에 관해서 필터처리를 진행한다.
implementation 'org.springframework.boot:spring-boot-starter-security'
securityConfig는
1. cors 처리
2. Oauth2 로그인 설정
3. JWT 필터 설정
4. 경로별 인가 작업
5. Oauth2 로그아웃 설정
를 처리한다.
1. CORS처리
http.cors(corsCustomizer -> corsCustomizer.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(Collections.singletonList("Authorization"));
return configuration;
}
}));
https://codewizard.tistory.com/60
[ERROR] CORS + SOP + preflight
백엔드 + 프론트 연결 중 CORS 에러 발생, 근데 preflight는 뭐야? 와이어샤크로 검출해보니tcp에서 syn -> (syn,ack) - > ack로 3way handshake로 잘 받고 options요청(preflight)으로 보내기 시작하는데 하던 중
codewizard.tistory.com
이 전 블로그를 참고하여 이해하면 된다. https에서 브라우저 정책을 이해하면 cors처리를 더 빨리 해결할 수 있다.
2. Oauth2 로그인 설정
// OAuth2 로그인 설정
http.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
// .failureHandler(oAuth2LoginFailureHandler) # 실패핸들러 추가하기
);
로그인 요청이 완료되고 리소스 서버(= http.oauth2Login)에서 사용자 정보가 날아오게 되면
customOAuth2UserService에서 파싱 하여 저장하는 과정을 거친다. (이때 리소스 서버마다 다루는 구조가 다르기 때문에 파싱방법을 다르게 가져가야 한다.)
사용자 프로필(카카오, 네이버) 구조 예시를 보자.
1. 카카오
HTTP/1.1 200 OK
{
"id":123456789,
"connected_at": "2022-04-11T01:45:28Z",
"kakao_account": {
// 프로필 또는 닉네임 동의항목 필요
"profile_nickname_needs_agreement ": false,
// 프로필 또는 프로필 사진 동의항목 필요
"profile_image_needs_agreement ": false,
"profile": {
// 프로필 또는 닉네임 동의항목 필요
"nickname": "홍길동",
// 프로필 또는 프로필 사진 동의항목 필요
"thumbnail_image_url": "http://yyy.kakao.com/.../img_110x110.jpg",
"profile_image_url": "http://yyy.kakao.com/dn/.../img_640x640.jpg",
"is_default_image":false,
"is_default_nickname": false
},
// 이름 동의항목 필요
"name_needs_agreement":false,
"name":"홍길동",
// 카카오계정(이메일) 동의항목 필요
"email_needs_agreement":false,
"is_email_valid": true,
"is_email_verified": true,
"email": "sample@sample.com",
// 연령대 동의항목 필요
"age_range_needs_agreement":false,
"age_range":"20~29",
// 출생 연도 동의항목 필요
"birthyear_needs_agreement": false,
"birthyear": "2002",
// 생일 동의항목 필요
"birthday_needs_agreement":false,
"birthday":"1130",
"birthday_type":"SOLAR",
// 성별 동의항목 필요
"gender_needs_agreement":false,
"gender":"female",
// 카카오계정(전화번호) 동의항목 필요
"phone_number_needs_agreement": false,
"phone_number": "+82 010-1234-5678",
// CI(연계정보) 동의항목 필요
"ci_needs_agreement": false,
"ci": "${CI}",
"ci_authenticated_at": "2019-03-11T11:25:22Z",
},
"properties":{
"${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
...
},
"for_partner": {
"uuid": "${UUID}"
}
}
위에 응답요청은 동의항목에 대해서 모든 동의를 했을 때 받을 수 있는 형식이며
https://devtalk.kakao.com/t/how-to-set-scopes-to-required-consent/115162
동의 항목별 "필수 동의" 설정 방법 / How to set scopes to ‘Required consent’
내 애플리케이션>제품 설정>카카오 로그인>동의항목 : 1. 디벨로퍼스앱 생성 후. 카카오 로그인 활성화 시, 기본 제공 [개인 정보] 닉네임 {profile_nickname} [필수] 프로필 사진 {profile_image} [필수] 카
devtalk.kakao.com
위에 3단계 권한신청이 완료되어야 받을 수 있는 요청임.
개발자 입장으로 본다면 3단계인 앱 권한 신청까지 완료되어야 [개인 정보]를 받을 수 있다. 이름 / 성별 / 연령대 / 생일 / 출생 연도 / 카카오계정(전화번호) / 배송지정보(수령인명, 배송지 주소, 전화번호)가 (앱 권한 신청)에 해당한다. |
만약 동의항목만 설정하고 사용자에게 동의를 받은 경우
HTTP/1.1 200 OK
{
"id":123456789,
"connected_at": "2022-04-11T01:45:28Z",
"kakao_account": {
"profile_nickname_needs_agreement": false,
"profile": {
"nickname": "홍길동"
}
},
"properties":{
"${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
...
}
}
받을 수 있는 정보는 다음과 같다.
2. 네이버
getAttributes :
{
resultcode=00,
message=success,
response={
id=,
nickname=,
name=
}
}
provider : naver
처럼 API가 넘어오면
if문으로 리소스마다 알맞게 파싱한 후 DB에 저장한다. 이때,
1) 신규유저가 로그인했는지
2) 기존유저가 로그인했는지
에 따라서
신규유저라면 객체를 만들어서 저장하고 기존 유저라면 기존의 위치에다가 다시 저장한다.
// DefaultOAuth2UserService: OAuth2에서 기본적으로 유저를 저장하는 메서드를 가지고 있다.
// super로 상속받아서 사용한다.
// OAuth2UserRequest: 리소스 서버에서 제공되는 유저정보
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
public CustomOAuth2UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest); // 유저 정보
System.out.println(oAuth2User);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if (registrationId.equals("naver")) {
oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
} else if (registrationId.equals("kakao")) {
oAuth2Response = new KakaoResponse(oAuth2User.getAttributes());
} else {
return null;
}
//리소스 서버에서 발급 받은 정보로 사용자를 특정할 아이디값을 만듬
String username = oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();
Optional<UserEntity> optionalUserEntity = userRepository.findByUsername(username);
// 1. 새로운 유저라면
if (optionalUserEntity.isEmpty()) {
UserEntity userEntity = new UserEntity();
userEntity.setUsername(username); // ex) kakao 3664463254
userEntity.setEmail(oAuth2Response.getEmail()); // ex) xxxx@naver.com
userEntity.setName(oAuth2Response.getName()); // ex) 홍길동
// userEntity.setRole("ROLE_USER"); // ex) ROLE_USER
// 리프레시 토큰 넣기
userRepository.save(userEntity);
UserDTO userDTO = new UserDTO();
userDTO.setUsername(username);
userDTO.setName(oAuth2Response.getName());
// userDTO.setRole("ROLE_USER");
return new CustomOAuth2User(userDTO);
}
// 2. 기존 유러라면
else {
UserEntity existData = optionalUserEntity.get();
existData.setEmail(oAuth2Response.getEmail());
existData.setName(oAuth2Response.getName());
userRepository.save(existData);
UserDTO userDTO = new UserDTO();
userDTO.setUsername(username);
userDTO.setName(oAuth2Response.getName());
// userDTO.setRole("ROLE_USER");
return new CustomOAuth2User(userDTO);
}
}
}
성공적으로 로그인이 되면 .successHandler(customSuccessHandler) 커스텀하여 제작한 customSuccessHandler로 넘어가게 된다.
customSuccessHandler.java
customSuccessHandler는 2가지 일을 한다.
1. 브라우저에 JWT쿠키 발행
2. DB에 리프레시 토큰 저장( or Redis에 저장, 아직 DB에 저장 중이고 추후에 Redis에 저장할 계획)
@Component
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JWTUtil jwtUtil;
private final UserRepository userRepository;
public CustomSuccessHandler(JWTUtil jwtUtil, UserRepository userRepository) {
this.jwtUtil = jwtUtil;
this.userRepository = userRepository;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//OAuth2User
CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal();
String username = customUserDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
// JWT 생성
String token = jwtUtil.createJwt(username, role, 60*60*60L);
// Refresh Token 생성 및 저장
String refreshToken = jwtUtil.createJwt(username, role, 7*24*60*60L); // 예: 7일간 유효한 리프레시 토큰
// UserEntity 업데이트
Optional<UserEntity> userEntityOptional = userRepository.findByUsername(username);
if (userEntityOptional.isPresent()) {
UserEntity userEntity = userEntityOptional.get();
userEntity.setRefreshToken(refreshToken);
userEntity.setLoginStatus(true); // 로그인 상태를 true로 설정
userRepository.save(userEntity); // 업데이트된 정보를 저장
}
// 쿠키 설정
response.addCookie(createCookie("Authorization", token)); // 쿠키를 넣어준다.
response.addCookie(createCookie("RefreshToken", refreshToken)); // 리프레시 토큰도 쿠키로 추가
response.sendRedirect("http://localhost:8080/"); // 프론트쪽으로 특정 uri로 리다이렉트
}
private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(60*60*60);
//cookie.setSecure(true);
cookie.setPath("/");
cookie.setHttpOnly(true); // js가 해당 쿠키를 가져가지 못하도록
return cookie;
}
}
JWT토큰은 JWTUtil에서 발급한다.
JWTUtil.java
빌더패턴에 맞춰서
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
=> 들어갈 정보(원하는 정보, 만료기간, 싸인)들을 맞춘다.
@Component
public class JWTUtil {
private SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}") String secret) {
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 Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
public String createJwt(String username, String role, Long expiredMs) {
return Jwts.builder()
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
3. JWT 필터 설정
로그인이 성공된 이후에, 앞으로 들어오는 권한이 필요한 모든 api요청에 대해서는 jwtfilter를 거치게 된다.
jwtfilter에서는 쿠키에 올바른 값이 들어있는지 검사하고 있다면
jwtUtil에서 jwt해독을 진행하고 claim에 들어있는 원하는 정보만 뽑아낸다.
그 후 (헤더 검증, 만료시간 검증)등을 진행하며 조건에 부합하면
CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO);
customOAuth2User에 객체를 생성하고 그 후 스프링 시큐리티가 관리하는 세션에 임시세션을 만들어서 토큰을 등록한다.
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken); // 세션에 사용자 등록
api작업을 처리하는 동안 사용자 정보를 조회할 때, 세션에 있는 값을 빼내서 사용하면 됩니다.
세션은 작업이 마무리되고 종료합니다.
filterChain.doFilter(request, response);
그 후 다음 체인으로 엮여있는 필터에게 위임합니다.
JWTFilter.java
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
public JWTFilter(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// jwt 기간 만료시, 무한 재로그인 방지 로직
String requestUri = request.getRequestURI();
if (requestUri.matches("^\\/login(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}
if (requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}
// cookie들을 불러온 뒤 Authorization Key에 담긴 쿠키를 찾음
String authorization = null;
Cookie[] cookies = request.getCookies();
// 쿠키가 null인지 확인
if (cookies != null) {
for (Cookie cookie : cookies) {
System.out.println(cookie.getName());
if (cookie.getName().equals("Authorization")) {
authorization = cookie.getValue();
}
}
}
// Authorization 헤더 검증
if (authorization == null) {
System.out.println("token null");
filterChain.doFilter(request, response);
return; // 조건이 해당되면 메소드 종료 (필수)
}
// 토큰
String token = authorization;
// 토큰 소멸 시간 검증
if (jwtUtil.isExpired(token)) {
System.out.println("token expired");
filterChain.doFilter(request, response);
return; // 조건이 해당되면 메소드 종료 (필수)
}
// 토큰에서 username과 role 획득
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
System.out.println("jwtfilter jwt확인: " + username + role);
// userDTO를 생성하여 값 set
UserDTO userDTO = new UserDTO();
userDTO.setUsername(username);
// userDTO.setRole(role); // buyer, seller받아오는걸로 바꾸기
// UserDetails에 회원 정보 객체 담기
CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO);
// 스프링 시큐리티 인증 토큰 생성, 스프링 시큐리티에서 세션을 생성해가지고 토큰을 등록하고 있음.
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
// 세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response); // jwtfilter작업을 다 했기 때문에 다음 필터에게 작업을 넘긴다는 doFilter작업을 진행해주시면 됩니다.
}
4. 경로별 인가 작업
// 경로별 인가 작업
http.authorizeHttpRequests(auth -> auth
.requestMatchers(WHITE_LIST_URL).permitAll()
.anyRequest().authenticated());
지정된 주소 이외에는 모두authenticated() 처리를 진행합니다.
5. Oauth2 로그아웃 설정
// 로그아웃 설정
http.logout(logout -> logout.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(customSignOutProcessHandler) // 코이꺼
.deleteCookies("JSESSIONID", "Authorization", "RefreshToken")
);
로그아웃은 위에서 말한 거처럼
1. 브라우저에 있는 쿠키(엑세스토큰, 리프레시토큰) 지우기
2. 카카오처럼 리소스 서버의 엑세스토큰 만료시키기 - 각각의 리소스마다 로그아웃 방법 상이
3. WAS에서 관리하는 JWT 리프레시 토큰 만료시키기
3가지를 진행한다.
일단 customSignOutProcessHandler로 가서
customSignOutProcessHandler.java
@Component
@RequiredArgsConstructor
@Slf4j
public class CustomSignOutProcessHandler implements LogoutHandler {
private final UserRepository userRepository;
@Override
@Transactional
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
if (authentication == null) {
return;
}
CustomOAuth2User userPrincipal = (CustomOAuth2User) authentication.getPrincipal(); // CustomOAuth2User 사용
System.out.println("로그아웃 정보 확인"+ userPrincipal + userPrincipal.getUsername());
userRepository.updateRefreshTokenAndLoginStatus(userPrincipal.getUsername(), null, false); // UserEntity에서 해당 유저를 찾아서 리프레시 토큰과 로그인 상태를 업데이트
}
}
authentication이 살아있는지 검사하고 살아있다면 로그아웃을 진행한다.
하지만 필자는 authentication == null이 계속되어서 문제가 생겼었다.
이유는
모든 API요청에 대해서 JWTFilter가 실행되어야 authentication이 활성화되는데
// 로그아웃 설정
http.logout(logout -> logout.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(customSignOutProcessHandler) // 코이꺼
.deleteCookies("JSESSIONID", "Authorization", "RefreshToken")
);
인 로그아웃필터가 먼저 실행되었기 때문이다.
정리하자면
logoutfilter -> JWTFilter로 실행되어서 authentication=null이 되었고
JWTFilter -> logoutfilter로 필터 위치를 바꿔주어야 한다.
securityconfig.java로 가서
http.addFilterBefore(new JWTFilter(jwtUtil), LogoutFilter.class); // 로그아웃 필터전에 jwt필터실행
필터위치를 먼저 실행되도록 하면 null에 걸리지 않는다.
처럼 걸리지 않았다면,
userRepository.updateRefreshTokenAndLoginStatus(userPrincipal.getUsername(), null, false);
UserEntity에서 해당 유저를 찾아서
리프레시 토큰 = null
로그인 상태 = false 처리를 해주고
deleteCookies("JSESSIONID", "Authorization", "RefreshToken")로 요청이 온 브라우저에 쿠키를 지워준다.
참고: https://velog.io/@semi-cloud/Spring-Security-Spring-Security-Oauth2-Client-%EB%B6%84%EC%84%9D
'Solo Project > 소셜로그인+JWT' 카테고리의 다른 글
[ERROR] CORS + SOP + preflight (0) | 2024.08.06 |
---|---|
[카카오 소셜 로그인] 1. 순서도 (4) | 2024.07.23 |