Skip to content

Commit d2bbc59

Browse files
authored
Merge pull request #17 from WhosInRoom/feat/#16-delete-account
[Feat] 회원 탈퇴 기능
2 parents 06171b6 + f6676c4 commit d2bbc59

8 files changed

Lines changed: 124 additions & 57 deletions

File tree

src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ public BaseResponse<Void> logout(HttpServletRequest request,
3333
}
3434

3535
@PostMapping("/reissue")
36-
public BaseResponse<ReissueResponse> reissueTokens(@RequestBody RefreshTokenRequest tokenRequest) {
37-
ReissueResponse response = jwtService.reissueTokens(tokenRequest);
36+
public BaseResponse<ReissueResponse> reissueTokens(@RequestBody RefreshTokenRequest tokenRequest,
37+
@CurrentUserId Long userId) {
38+
ReissueResponse response = jwtService.reissueTokens(tokenRequest, userId);
3839
return BaseResponse.ok(response);
3940
}
4041

src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException;
44
import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomJwtException;
5+
import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomAuthenticationEntryPoint;
56
import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal;
67
import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService;
78
import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil;
@@ -14,6 +15,7 @@
1415
import lombok.RequiredArgsConstructor;
1516
import lombok.extern.slf4j.Slf4j;
1617
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
18+
import org.springframework.security.authentication.AuthenticationManager;
1719
import org.springframework.security.authentication.BadCredentialsException;
1820
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1921
import org.springframework.security.core.Authentication;
@@ -37,71 +39,77 @@
3739
public class JwtAuthenticationFilter extends OncePerRequestFilter {
3840
private final JwtUtil jwtUtil;
3941
private final JwtService jwtService;
42+
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
4043

4144
// 인증을 안해도 되니 토큰이 필요없는 URL들 (에러: 로그인이 필요합니다)
4245
public final static List<String> PASS_URIS = Arrays.asList(
43-
"/api/users/signup", "/api/auth/**"
46+
"/api/users/signup", "/api/auth/login",
47+
"/api/auth/email/send", "/api/auth/email/validation"
4448
);
4549

4650
private static final AntPathMatcher ANT = new AntPathMatcher();
4751

4852
@Override
4953
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
5054

51-
if(isPassUri(request.getRequestURI())) {
52-
log.info("JWT Filter Passed (pass uri) : {}", request.getRequestURI());
53-
filterChain.doFilter(request, response);
54-
return;
55-
}
55+
try {
5656

57-
// 엑세스 토큰이 없으면 Authentication도 없음 -> EntryPoint (401)
58-
log.info("Request URI: {}", request.getRequestURI()); // 요청 URI 로깅
59-
String accessToken = jwtUtil.extractAccessToken(request)
60-
.orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_UNAUTHORIZED));
57+
if (isPassUri(request.getRequestURI())) {
58+
log.info("JWT Filter Passed (pass uri) : {}", request.getRequestURI());
59+
filterChain.doFilter(request, response);
60+
return;
61+
}
6162

62-
// 토큰 유효성 검사
63-
jwtUtil.validateToken(accessToken);
63+
// 엑세스 토큰이 없으면 Authentication도 없음 -> EntryPoint (401)
64+
log.info("Request URI: {}", request.getRequestURI()); // 요청 URI 로깅
65+
String accessToken = jwtUtil.extractAccessToken(request)
66+
.orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_UNAUTHORIZED));
6467

65-
// 토큰 타입 검사
66-
if(!"access".equals(jwtUtil.getTokenType(accessToken))) {
67-
throw new CustomJwtException(ErrorCode.INVALID_TOKEN_TYPE);
68-
}
68+
// 토큰 유효성 검사
69+
jwtUtil.validateToken(accessToken);
6970

70-
// 로그아웃 체크
71-
jwtService.checkLogout(accessToken);
71+
// 토큰 타입 검사
72+
if (!"access".equals(jwtUtil.getTokenType(accessToken))) {
73+
throw new CustomJwtException(ErrorCode.INVALID_TOKEN_TYPE);
74+
}
7275

73-
// 권한 리스트 생성
74-
List<GrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(jwtUtil.getRole(accessToken)));
75-
log.info("Granted Authorities : {}", authorities);
76-
UserPrincipal principal = new UserPrincipal(
77-
jwtUtil.getUserId(accessToken),
78-
jwtUtil.getName(accessToken),
79-
null, // 패스워드는 필요 없음
80-
jwtUtil.getProviderId(accessToken),
81-
authorities
82-
);
83-
log.info("UserPrincipal.userId: {}", principal.getUserId());
84-
log.info("UserPrincipal.nickName: {}", principal.getUsername());
85-
log.info("UserPrincipal.providerId: {}", principal.getProviderId());
86-
log.info("UserPrincipal.role: {}", principal.getAuthorities().stream().findFirst().get().toString());
76+
// 로그아웃 체크
77+
jwtService.checkLogout(accessToken);
8778

88-
Authentication authToken = null;
89-
if ("localhost".equals(principal.getProviderId())) {
90-
// 폼 로그인(자체 회원)
91-
authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities);
92-
}
79+
// 권한 리스트 생성
80+
List<GrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(jwtUtil.getRole(accessToken)));
81+
log.info("Granted Authorities : {}", authorities);
82+
UserPrincipal principal = new UserPrincipal(
83+
jwtUtil.getUserId(accessToken),
84+
jwtUtil.getName(accessToken),
85+
null, // 패스워드는 필요 없음
86+
jwtUtil.getProviderId(accessToken),
87+
authorities
88+
);
89+
log.info("UserPrincipal.userId: {}", principal.getUserId());
90+
log.info("UserPrincipal.nickName: {}", principal.getUsername());
91+
log.info("UserPrincipal.providerId: {}", principal.getProviderId());
92+
log.info("UserPrincipal.role: {}", principal.getAuthorities().stream().findFirst().get().toString());
93+
94+
Authentication authToken = null;
95+
if ("localhost".equals(principal.getProviderId())) {
96+
// 폼 로그인(자체 회원)
97+
authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities);
98+
}
9399
// else {
94100
// // 소셜 로그인
95101
// authToken = new OAuth2AuthenticationToken(principal, authorities, loginProvider);
96102
// }
97-
log.info("Authentication set in SecurityContext: {}", SecurityContextHolder.getContext().getAuthentication());
98-
log.info("Authorities in SecurityContext: {}", authToken.getAuthorities());
103+
log.info("Authentication set in SecurityContext: {}", SecurityContextHolder.getContext().getAuthentication());
104+
log.info("Authorities in SecurityContext: {}", authToken.getAuthorities());
99105

100-
log.info("JWT Filter Success : {}", request.getRequestURI());
101-
SecurityContextHolder.getContext().setAuthentication(authToken);
102-
filterChain.doFilter(request, response);
106+
log.info("JWT Filter Success : {}", request.getRequestURI());
107+
SecurityContextHolder.getContext().setAuthentication(authToken);
108+
filterChain.doFilter(request, response);
109+
} catch (CustomAuthenticationException | AuthenticationException e) {
110+
customAuthenticationEntryPoint.commence(request, response, (org.springframework.security.core.AuthenticationException) e);
111+
}
103112
}
104-
105113
private boolean isPassUri(String uri) {
106114
return PASS_URIS.stream().anyMatch(pattern -> ANT.match(pattern, uri));
107115
}

src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.WhoIsRoom.WhoIs_Server.domain.auth.util.AuthenticationUtil;
55
import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil;
66
import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService;
7-
import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseErrorResponse;
87
import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse;
98
import com.fasterxml.jackson.databind.ObjectMapper;
109
import jakarta.servlet.http.HttpServletRequest;
@@ -35,7 +34,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
3534

3635
String providerId = authenticationUtil.getProviderId();
3736
String role = authenticationUtil.getRole();
38-
Long memberId = authenticationUtil.getMemberId();
37+
Long memberId = authenticationUtil.getUserId();
3938
String nickName = authenticationUtil.getUsername();
4039
log.info("[CustomAuthenticationSuccessHandler] providerId={}, role={}, memberId={}", providerId, role, memberId);
4140

@@ -44,7 +43,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
4443
String refreshToken = jwtUtil.createRefreshToken(memberId, providerId, nickName);
4544

4645
// refresh token 저장
47-
jwtService.storeRefreshToken(refreshToken);
46+
jwtService.storeRefreshToken(refreshToken, memberId);
4847
log.info("[CustomAuthenticationSuccessHandler], refreshToken={}", refreshToken);
4948

5049
LoginResponse data = LoginResponse.builder()

src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public void logout(HttpServletRequest request, RefreshTokenRequest tokenRequest)
4545
String accessToken = jwtUtil.extractAccessToken(request)
4646
.orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_ACCESS_TOKEN));
4747

48+
log.info("LogOut Access Token: {}", accessToken);
49+
4850
String refreshToken = tokenRequest.getRefreshToken();
4951
jwtUtil.validateToken(refreshToken);
5052
if (!"refresh".equals(jwtUtil.getTokenType(refreshToken))) {
@@ -56,13 +58,13 @@ public void logout(HttpServletRequest request, RefreshTokenRequest tokenRequest)
5658
invalidAccessToken(accessToken);
5759
}
5860

59-
public ReissueResponse reissueTokens(RefreshTokenRequest tokenRequest) {
61+
public ReissueResponse reissueTokens(RefreshTokenRequest tokenRequest, Long userId) {
6062
String refreshToken = tokenRequest.getRefreshToken();
6163
jwtUtil.validateToken(refreshToken);
6264
if (!"refresh".equals(jwtUtil.getTokenType(refreshToken))) {
6365
throw new CustomJwtException(ErrorCode.INVALID_REFRESH_TYPE);
6466
}
65-
return reissueAndSendTokens(refreshToken);
67+
return reissueAndSendTokens(refreshToken, userId);
6668
}
6769

6870
public void checkLogout(String accessToken) {
@@ -72,30 +74,30 @@ public void checkLogout(String accessToken) {
7274
}
7375
}
7476

75-
public void storeRefreshToken(String refreshToken) {
76-
redisService.setValues(REFRESH_TOKEN_KEY_PREFIX, refreshToken, Duration.ofMillis(REFRESH_TOKEN_EXPIRED_IN));
77+
public void storeRefreshToken(String refreshToken, Long userId) {
78+
redisService.setValues(REFRESH_TOKEN_KEY_PREFIX+refreshToken, String.valueOf(userId), Duration.ofMillis(REFRESH_TOKEN_EXPIRED_IN));
7779
}
7880

7981
private void deleteRefreshToken(String refreshToken){
8082
if(refreshToken == null){
8183
throw new CustomJwtException(ErrorCode.INVALID_REFRESH_TYPE);
8284
}
83-
redisService.delete(refreshToken);
85+
redisService.delete(REFRESH_TOKEN_KEY_PREFIX+refreshToken);
8486
}
8587

86-
private void invalidAccessToken(String accessToken) {
88+
public void invalidAccessToken(String accessToken) {
8789
redisService.setValues(accessToken, LOGOUT_VALUE,
8890
Duration.ofMillis(ACCESS_TOKEN_EXPIRED_IN));
8991
}
9092

91-
private ReissueResponse reissueAndSendTokens(String refreshToken) {
93+
private ReissueResponse reissueAndSendTokens(String refreshToken, Long userId) {
9294

9395
// 새로운 Refresh Token 발급
9496
String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken));
9597
String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getName(refreshToken));
9698

9799
// 새로운 Refresh Token을 DB나 Redis에 저장
98-
storeRefreshToken(reissuedRefreshToken);
100+
storeRefreshToken(reissuedRefreshToken, userId);
99101

100102
// 기존 Refresh Token 폐기 (DB나 Redis에서 삭제)
101103
deleteRefreshToken(refreshToken);

src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public String getRole() {
2626
return grantedAuthority.getAuthority();
2727
}
2828

29-
public Long getMemberId() {
29+
public Long getUserId() {
3030
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
3131
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
3232
return principal.getUserId();

src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.WhoIsRoom.WhoIs_Server.domain.user.service.UserService;
88
import com.WhoIsRoom.WhoIs_Server.global.common.resolver.CurrentUserId;
99
import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse;
10+
import jakarta.servlet.http.HttpServletRequest;
1011
import lombok.RequiredArgsConstructor;
1112
import lombok.extern.slf4j.Slf4j;
1213
import org.springframework.web.bind.annotation.*;
@@ -44,4 +45,11 @@ public BaseResponse<Void> updatePassword(@CurrentUserId Long userId,
4445
userService.updateMyPassword(userId, request);
4546
return BaseResponse.ok(null);
4647
}
48+
49+
@PostMapping("/delete/account")
50+
public BaseResponse<Void> deleteAccount(HttpServletRequest request,
51+
@CurrentUserId Long userId) {
52+
userService.deleteAccount(request, userId);
53+
return BaseResponse.ok(null);
54+
}
4755
}

src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.MailRequest;
44
import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.PasswordRequest;
5+
import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException;
6+
import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService;
57
import com.WhoIsRoom.WhoIs_Server.domain.auth.service.MailService;
8+
import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil;
69
import com.WhoIsRoom.WhoIs_Server.domain.club.model.Club;
710
import com.WhoIsRoom.WhoIs_Server.domain.club.repository.ClubRepository;
811
import com.WhoIsRoom.WhoIs_Server.domain.member.model.Member;
@@ -15,12 +18,15 @@
1518
import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository;
1619
import com.WhoIsRoom.WhoIs_Server.global.common.exception.BusinessException;
1720
import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode;
21+
import jakarta.servlet.http.HttpServletRequest;
1822
import lombok.RequiredArgsConstructor;
1923
import lombok.extern.slf4j.Slf4j;
24+
import org.springframework.security.core.context.SecurityContextHolder;
2025
import org.springframework.security.crypto.password.PasswordEncoder;
2126
import org.springframework.stereotype.Service;
2227
import org.springframework.transaction.annotation.Transactional;
2328

29+
import java.net.http.HttpRequest;
2430
import java.util.*;
2531
import java.util.stream.Collectors;
2632

@@ -33,6 +39,8 @@ public class UserService {
3339
private final MailService mailService;
3440
private final MemberRepository memberRepository;
3541
private final ClubRepository clubRepository;
42+
private final JwtUtil jwtUtil;
43+
private final JwtService jwtService;
3644

3745
@Transactional
3846
public void signUp(SignupRequest request) {
@@ -169,4 +177,19 @@ private void validateClubExistence(Set<Long> clubIds) {
169177
throw new BusinessException(ErrorCode.CLUB_NOT_FOUND);
170178
}
171179
}
180+
181+
public void deleteAccount (HttpServletRequest request, Long userId) {
182+
User user = userRepository.findById(userId)
183+
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
184+
185+
userRepository.delete(user);
186+
187+
String accessToken = jwtUtil.extractAccessToken(request)
188+
.orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_ACCESS_TOKEN));
189+
if (accessToken != null) {
190+
jwtService.invalidAccessToken(accessToken); // Redis에 blacklist 저장
191+
}
192+
193+
SecurityContextHolder.clearContext();
194+
}
172195
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.WhoIsRoom.WhoIs_Server.global.common.redis;
2+
3+
import jakarta.annotation.PostConstruct;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.context.annotation.Profile;
7+
import org.springframework.data.redis.core.RedisTemplate;
8+
import org.springframework.stereotype.Component;
9+
10+
@Slf4j
11+
@Component
12+
@Profile("local")
13+
@RequiredArgsConstructor
14+
public class RedisInitializer {
15+
16+
private final RedisTemplate<String,String> redisTemplate;
17+
18+
@PostConstruct
19+
public void clearRedis() {
20+
redisTemplate.getConnectionFactory()
21+
.getConnection()
22+
.flushDb(); // 선택한 DB(예: database: 0)만 초기화
23+
log.info("✅ Redis DB 초기화 완료");
24+
}
25+
}
26+

0 commit comments

Comments
 (0)