Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat]: 회원탈퇴 API 구현 #73

Merged
merged 17 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f2d5838
refactor: 업무 환경 enum에 value 필드 추가하여 요청/응답 수정
wu-seong Feb 8, 2024
b37eb09
feat: 추가정보 DTO에 닉네임 추가
wu-seong Feb 8, 2024
744fbd3
feat: 회원 완전 탈퇴 테스트 생성, 근데 순환참조 오류땜에 테스트 못함
wu-seong Feb 10, 2024
6dd0d07
refactor: WebConfig, JwtAuthFilter 의존성 구조 리팩토링
wu-seong Feb 10, 2024
924365b
feat: socialType 받아서 판단하는 메소드 생성
wu-seong Feb 10, 2024
b7610e6
feat: 소셜계정 서비스 연동해지 및 토큰만료 요청 API 요청 DTO 생성
wu-seong Feb 10, 2024
1d472b5
feat: 애플 토큰만료 및 앱 연동 해지 요청 메서드 생성
wu-seong Feb 10, 2024
61e079a
feat: 카카오 토큰만료 및 앱 연동 해지 요청 메서드 생성
wu-seong Feb 10, 2024
4781b87
feat: 카카오 api 클라이언트 연결끊기 요청 추가
wu-seong Feb 10, 2024
ced901b
feat: 애플 auth 클라이언트 토큰폐기 요청 추가
wu-seong Feb 10, 2024
bf9cd14
feat: 유저 hard delete 메서드에 앱 연동해지/토큰폐기 로직 추가
wu-seong Feb 10, 2024
258c168
refactor: WebConfig, JwtAuthFilter 의존성 구조 리팩토링
wu-seong Feb 10, 2024
299f6e3
feat: 유저 완전 회원탈퇴 테스트 API 추가
wu-seong Feb 10, 2024
73cbb74
feat: 애플 auth client 토큰 폐기 요청 수정
wu-seong Feb 10, 2024
43f05c4
Merge dev into feat/#66 and resolve conflicts
wu-seong Feb 10, 2024
03cfc59
remove: 회원탈퇴 테스트 삭제
wu-seong Feb 11, 2024
9fd17d7
feat: 회원 완전 탈퇴 테스트 API 수정
wu-seong Feb 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/main/java/com/onnoff/onnoff/auth/config/FilterConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.onnoff.onnoff.auth.config;

import com.onnoff.onnoff.auth.jwt.filter.JwtAuthFilter;
import com.onnoff.onnoff.auth.jwt.filter.UserInterceptor;
import com.onnoff.onnoff.auth.jwt.service.JwtUtil;
import com.onnoff.onnoff.auth.jwt.service.TokenProvider;
import com.onnoff.onnoff.domain.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class FilterConfig {
private final TokenProvider tokenProvider;
private final UserService userService;
private final JwtUtil jwtUtil;

@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(tokenProvider);
}

@Bean
public UserInterceptor userInterceptor() {
return new UserInterceptor(userService, jwtUtil);
}
}
12 changes: 4 additions & 8 deletions src/main/java/com/onnoff/onnoff/auth/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@

import com.onnoff.onnoff.auth.jwt.filter.JwtAuthFilter;
import com.onnoff.onnoff.auth.jwt.filter.UserInterceptor;
import com.onnoff.onnoff.auth.jwt.service.JwtTokenProvider;
import com.onnoff.onnoff.auth.jwt.service.JwtUtil;
import com.onnoff.onnoff.domain.user.service.UserService;
import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
Expand All @@ -18,14 +15,13 @@
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtTokenProvider jwtTokenProvider;
private final JwtUtil jwtUtil;
private final UserService userService;
private final JwtAuthFilter jwtAuthFilter;
private final EntityManagerFactory entityManagerFactory;
private final UserInterceptor userInterceptor;
@Bean
public FilterRegistrationBean<JwtAuthFilter> jwtFilterRegistration() {
FilterRegistrationBean<JwtAuthFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new JwtAuthFilter(jwtTokenProvider)); // 필터 인스턴스 설정
registration.setFilter(jwtAuthFilter); // 필터 인스턴스 설정
registration.addUrlPatterns("/*"); //서블릿 컨택스트에서 /*는 모든 요청, /**는 인식되지 않음
registration.setOrder(1); // 필터의 순서 설정. 값이 낮을수록 먼저 실행
return registration;
Expand All @@ -37,7 +33,7 @@ public void addInterceptors(InterceptorRegistry registry) {
openEntityManagerInViewInterceptor.setEntityManagerFactory(entityManagerFactory);
registry.addWebRequestInterceptor(openEntityManagerInViewInterceptor);

registry.addInterceptor(new UserInterceptor(userService, jwtUtil))
registry.addInterceptor(userInterceptor)
.addPathPatterns("/**") // 스프링 경로는 /*와 /**이 다름
.excludePathPatterns("/swagger-ui/**", "/v3/api-docs/**", "/oauth2/**", "/health", "/token/**" ,
"/message/**", "/enums/**", "/users/nickname");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse;
import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO;
import com.onnoff.onnoff.auth.jwt.dto.JwtToken;
import com.onnoff.onnoff.auth.jwt.service.JwtTokenProvider;
import com.onnoff.onnoff.auth.jwt.service.JwtUtil;
import com.onnoff.onnoff.auth.jwt.service.TokenProvider;
import com.onnoff.onnoff.auth.service.AppleLoginService;
import com.onnoff.onnoff.auth.service.KakaoLoginService;
import com.onnoff.onnoff.domain.user.User;
Expand All @@ -34,7 +34,7 @@ public class LoginController {
private final KakaoLoginService kakaoLoginService;
private final AppleLoginService appleLoginService;
private final UserService userService;
private final JwtTokenProvider jwtTokenProvider;
private final TokenProvider tokenProvider;
private final JwtUtil jwtUtil;

@Value("${kakao.redirect-uri}")
Expand Down Expand Up @@ -135,14 +135,14 @@ public ApiResponse<UserResponseDTO.LoginDTO> validateAppleToken(@RequestBody Log
public ApiResponse<UserResponseDTO.LoginDTO> validateServerToken(@RequestBody JwtToken tokenDTO){
String accessToken = tokenDTO.getAccessToken();
String refreshToken = tokenDTO.getRefreshToken();
if( jwtTokenProvider.verifyToken(accessToken) ){
if( tokenProvider.verifyToken(accessToken) ){
// accessToken 유효
String userId = jwtUtil.getUserId(accessToken);
User user = userService.getUser(Long.valueOf(userId));
UserResponseDTO.LoginDTO loginDTO = UserConverter.toLoginDTO(accessToken, refreshToken);
return ApiResponse.onSuccess(loginDTO);
}
if (jwtTokenProvider.verifyToken(refreshToken)) {
if ( tokenProvider.verifyToken(refreshToken)) {
//refreshToken 유효
String userId = jwtUtil.getUserId(refreshToken);
User user = userService.getUser(Long.valueOf(userId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static class KakaoTokenValidateDTO{

@Getter
public static class AdditionalInfo{
private String nickname;
private String fieldOfWork;
private String job;
private String experienceYear;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.onnoff.onnoff.auth.feignClient.client;


import com.onnoff.onnoff.auth.feignClient.config.FeignConfig;
import com.onnoff.onnoff.auth.feignClient.dto.JwkResponse;
import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.Map;

@FeignClient(name = "apple-auth-client",url = "https://appleid.apple.com/auth")
@FeignClient(name = "apple-auth-client",url = "https://appleid.apple.com/auth", configuration = FeignConfig.class)
public interface AppleAuthClient{
@GetMapping("/keys")
JwkResponse.JwkSet getKeys();
Expand All @@ -20,6 +20,6 @@ public interface AppleAuthClient{
TokenResponse getToken(@RequestBody Map<String, ?> requestBody);

//회원 탈퇴 메서드
// @GetMapping("/revoke")
// KakaoOauth2DTO.TokenValidateResponseDTO getTokenValidate(@RequestHeader("Authorization") String accessToken);
@PostMapping(value ="/oauth2/v2/revoke", consumes = "application/x-www-form-urlencoded")
void revokeTokens(@RequestBody Map<String, ?> requestBody);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import com.onnoff.onnoff.auth.feignClient.config.FeignConfig;
import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/*
토큰 유효성 검증 하고 사용자 정보 가져오는 client
*/
Expand All @@ -15,5 +18,7 @@ public interface KakaoApiClient {
KakaoOauth2DTO.TokenValidateResponseDTO getTokenValidate(@RequestHeader("Authorization") String accessToken);
@GetMapping(value = "/v1/oidc/userinfo")
KakaoOauth2DTO.UserInfoResponseDTO getUserInfo(@RequestHeader("Authorization") String accessToken);
@PostMapping(value = "/v1/user/unlink", consumes = "application/x-www-form-urlencoded")
ResponseEntity unlink(@RequestHeader("Authorization") String adminKey, @RequestBody Map<String, ?> requestBody);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.onnoff.onnoff.auth.feignClient.dto.apple;


import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@Getter
@Builder
@AllArgsConstructor
public class RevokeTokenReqeust {
private String clientId;
private String clientSecret;
private String token;
private String tokenTypeHint;

public MultiValueMap<String, String> toUrlEncoded(){
LinkedMultiValueMap<String, String> urlEncoded = new LinkedMultiValueMap<>();
urlEncoded.add("client_id", clientId);
urlEncoded.add("client_secret", clientSecret);
urlEncoded.add("token", token);
urlEncoded.add("token_type_hint", "refresh_token");
return urlEncoded;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.onnoff.onnoff.auth.feignClient.dto.kakao;


import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@Builder
@AllArgsConstructor
public class UnlinkRequest {
private String targetIdType;
private String targetId;

public MultiValueMap<String, String> toUrlEncoded(){
LinkedMultiValueMap<String, String> urlEncoded = new LinkedMultiValueMap<>();
urlEncoded.add("target_id_type", targetIdType);
urlEncoded.add("target_id", targetId);
return urlEncoded;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,22 @@
*/


import com.onnoff.onnoff.auth.jwt.service.JwtTokenProvider;
import com.onnoff.onnoff.auth.jwt.service.TokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final TokenProvider tokenProvider;
private final static String[] ignorePrefix = {"/swagger-ui", "/v3/api-docs", "/oauth2", "/health", "/token/validate" , "/message", "/enums", "/users/nickname"};
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Expand All @@ -43,7 +42,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
if (authHeader != null && authHeader.startsWith("Bearer ")) {
accessToken = authHeader.substring(7);
}
if (jwtTokenProvider.verifyToken(accessToken)){
if (tokenProvider.verifyToken(accessToken)){
log.info("인증성공");
filterChain.doFilter(request, response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
@Slf4j
@Service
@RequiredArgsConstructor
public class JwtTokenProvider {
public class JwtTokenProvider implements TokenProvider {
@Value("${spring.jwt.secret}")
private String secret;
private SecretKey secretkey;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.onnoff.onnoff.auth.jwt.service;

public interface TokenProvider {
boolean verifyToken(String token);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import com.onnoff.onnoff.auth.UserContext;
import com.onnoff.onnoff.auth.feignClient.client.AppleAuthClient;
import com.onnoff.onnoff.auth.feignClient.dto.apple.RevokeTokenReqeust;
import com.onnoff.onnoff.auth.feignClient.dto.apple.TokenRequest;
import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse;
import com.onnoff.onnoff.auth.service.tokenValidator.SocialTokenValidator;
Expand All @@ -27,8 +28,6 @@
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -116,4 +115,15 @@ public void validate(String identityToken){
String cleanedIdentityToken = cleanToken(identityToken);
validator.validate(cleanedIdentityToken, SocialType.APPLE);
}

public void revokeTokens(String refreshToken) {
String clientSecret = createClientSecret();
MultiValueMap<String, String> urlEncoded = RevokeTokenReqeust.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.token(refreshToken)
.build().toUrlEncoded();
appleAuthClient.revokeTokens(urlEncoded);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
import com.onnoff.onnoff.auth.feignClient.client.KakaoOauth2Client;
import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse;
import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO;
import com.onnoff.onnoff.auth.feignClient.dto.kakao.UnlinkRequest;
import com.onnoff.onnoff.auth.service.tokenValidator.SocialTokenValidator;
import com.onnoff.onnoff.domain.user.enums.SocialType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;


@Service
Expand All @@ -25,6 +28,8 @@ public class KakaoLoginService implements LoginService{
private String clientId;
@Value("${kakao.redirect-uri}")
private String redirectUri;
@Value("${kakao.admin-key}")
private String adminKey;
/*
테스트 용으로 만든거, 실제로는 프론트에서 처리해서 액세스 토큰만 가져다 줌
*/
Expand All @@ -41,9 +46,22 @@ public void validate(String idToken){
String cleanedAccessToken = cleanToken(idToken);
validator.validate(cleanedAccessToken, SocialType.KAKAO);
}
/*
토큰으로 유저정보를 가져오는 메서드
*/

public void revokeTokens(String oauthId) {
MultiValueMap<String, String> urlEncoded = UnlinkRequest.builder()
.targetIdType("user_id")
.targetId(oauthId)
.build()
.toUrlEncoded();
adminKey = "KakaoAK " + adminKey;
log.info("adminkey = {}", adminKey);
ResponseEntity responseEntity = kakaoApiClient.unlink(adminKey, urlEncoded);
log.info("삭제된 회원 정보 = {}", responseEntity.getBody());
}

/*
토큰으로 유저정보를 가져오는 메서드
*/
public KakaoOauth2DTO.UserInfoResponseDTO getUserInfo(String accessToken) throws JsonProcessingException {
String cleanedAccessToken = cleanToken(accessToken);
accessToken = "bearer " + cleanedAccessToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ public ApiResponse<UserResponseDTO.UserDetailDTO> modifyUser(@RequestBody UserRe
return ApiResponse.onSuccess(UserConverter.toUserDetailDTO(userService.modifyUser(modifyUserDTO)));
}

//테스트용
@PutMapping("/hard-delete")
@Operation(summary = "회원 완전 탈퇴 테스트 API",description = "30일 뒤에 자동 완전삭제 수동 테스트")
public ApiResponse<String> hardDeleteTest(){
userService.deleteInactiveUsers() ;
return ApiResponse.onSuccess("삭제완");
}

@PostMapping("/nickname")
@Operation(summary = "닉네임 중복 체크 API")
public ApiResponse<String> checkNickname(@Valid @RequestBody UserRequestDTO.getNicknameDTO nicknameDTO){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,28 @@

public class UserConverter {
public static User toUser(KakaoOauth2DTO.UserInfoResponseDTO response, LoginRequestDTO.AdditionalInfo additionalInfo){
String fieldOfWork = additionalInfo.getFieldOfWork();
try{
FieldOfWork.valueOf(fieldOfWork);
}
catch (IllegalArgumentException e){
throw new GeneralException(ErrorStatus.INVALID_ENUM_VALUE);
}

FieldOfWork fieldOfWork = FieldOfWork.fromValue(additionalInfo.getFieldOfWork());
ExperienceYear experienceYear = ExperienceYear.fromValue(additionalInfo.getExperienceYear());
return User.builder()
.oauthId(response.getSub())
.email(response.getEmail())
.name(response.getNickname())
.socialType(SocialType.KAKAO)
.fieldOfWork(Enum.valueOf(FieldOfWork.class ,additionalInfo.getFieldOfWork() ) )
.fieldOfWork(fieldOfWork)
.job(additionalInfo.getJob())
.experienceYear(experienceYear)
.build();
}
public static User toUser(LoginRequestDTO.AppleTokenValidateDTO request, LoginRequestDTO.AdditionalInfo additionalInfo){
FieldOfWork fieldOfWork = FieldOfWork.fromValue(additionalInfo.getFieldOfWork());
ExperienceYear experienceYear = ExperienceYear.fromValue(additionalInfo.getExperienceYear());
String fullName = request.getFullName().getFamilyName() + request.getFullName().getGivenName();
return User.builder()
.oauthId(request.getOauthId())
.email(request.getEmail())
.name(fullName)
.socialType(SocialType.APPLE)
.fieldOfWork(Enum.valueOf(FieldOfWork.class ,additionalInfo.getFieldOfWork() ) )
.fieldOfWork(fieldOfWork)
.job(additionalInfo.getJob())
.experienceYear(experienceYear)
.build();
Expand Down
Loading