Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


import com.gpt.geumpumtabackend.global.jwt.JwtAuthenticationFilter;
import com.gpt.geumpumtabackend.global.oauth.handler.OAuth2AuthenticationFailureHandler;
import com.gpt.geumpumtabackend.global.oauth.handler.OAuth2AuthenticationSuccessHandler;
import com.gpt.geumpumtabackend.global.oauth.resolver.CustomAuthorizationRequestResolver;
import com.gpt.geumpumtabackend.global.oauth.service.CustomOAuth2UserService;
Expand Down Expand Up @@ -34,6 +35,7 @@ public class SecurityConfig {
private final AuthenticationManager authenticationManager;
private final CustomAuthorizationRequestResolver customAuthorizationRequestResolver;
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient;
private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;

@Bean
public SecurityFilterChain filterChainPermitAll(HttpSecurity http) throws Exception {
Expand All @@ -58,6 +60,7 @@ public HttpSecurity defaultSecurity(HttpSecurity http) throws Exception {
.userInfoEndpoint(ui ->
ui.userService(customOAuth2UserService))
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
)
.addFilterAfter(new JwtAuthenticationFilter(authenticationManager),
UsernamePasswordAuthenticationFilter.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum ExceptionType {
SCHOOL_EMAIL_ALREADY_REGISTERED(FORBIDDEN, "U002", "학교 이메일이 등록된 상태입니다"),
DUPLICATED_SCHOOL_EMAIL(FORBIDDEN, "U003", "이미 사용중인 이메일입니다"),
DEPARTMENT_NOT_FOUND(BAD_REQUEST, "U004", "존재하지 않는 학과 명입니다"),
USER_WITHDRAWN(FORBIDDEN, "U005", "탈퇴한 사용자입니다."),

// Mail
CANT_SEND_MAIL(INTERNAL_SERVER_ERROR, "M001", "인증코드 전송에 실패했습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@

public record JwtAuthentication(
Long userId,
UserRole role
UserRole role,
Boolean withdrawn
) implements Authentication {

public JwtAuthentication(JwtUserClaim claims) {
this(
claims.userId(),
claims.role()
claims.role(),
claims.withdrawn()
);
}

Expand Down Expand Up @@ -54,4 +56,8 @@ public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentExce
public String getName() {
return String.valueOf(userId);
}

public boolean isWithdrawn() {
return withdrawn;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import com.fasterxml.jackson.databind.ObjectMapper;
import com.gpt.geumpumtabackend.global.exception.ExceptionType;
import com.gpt.geumpumtabackend.global.jwt.exception.JwtAuthenticationException;
import com.gpt.geumpumtabackend.global.jwt.exception.JwtNotExistException;
import jakarta.servlet.FilterChain;
Expand Down Expand Up @@ -53,6 +54,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
String tokenValue = resolveToken(request).orElseThrow(JwtNotExistException::new);
JwtAuthenticationToken token = new JwtAuthenticationToken(tokenValue); // 인증되지 않은 토큰
Authentication authentication = this.authenticationManager.authenticate(token); // TokenProvider에게 위임
if (authentication instanceof JwtAuthentication jwtAuth) {

if (jwtAuth.isWithdrawn() && !isRestoreEndpoint(request)) {
throw new JwtAuthenticationException(ExceptionType.USER_WITHDRAWN);
}
}
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (JwtAuthenticationException e) {
Expand All @@ -77,4 +84,14 @@ private void handleServiceException(HttpServletResponse response, JwtAuthenticat
response.flushBuffer(); // 커밋
response.getWriter().close();
}


private boolean isRestoreEndpoint(HttpServletRequest request) {
String uri = request.getRequestURI();
String method = request.getMethod();

return method.equalsIgnoreCase("POST")
&& uri.equals("/api/v1/user/restore");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class JwtHandler {
public static final String USER_ID = "USER_ID";
public static final String USER_ROLE = "ROLE_USER";
private static final String KEY_ROLE = "role";
private static final String IS_WITHDRAWN = "WITHDRAWN";
private static final long MILLI_SECOND = 1000L;

public JwtHandler(JwtProperties jwtProperties, RefreshTokenRepository refreshTokenRepository) {
Expand Down Expand Up @@ -64,7 +65,8 @@ public Token createTokens(JwtUserClaim jwtUserClaim) {
public Map<String, Object> createClaims(JwtUserClaim jwtUserClaim) {
return Map.of(
USER_ID, jwtUserClaim.userId(),
USER_ROLE, jwtUserClaim.role()
USER_ROLE, jwtUserClaim.role(),
IS_WITHDRAWN, jwtUserClaim.withdrawn()
);
}

Expand All @@ -89,7 +91,8 @@ public Optional<JwtUserClaim> getClaims(String token) {
public JwtUserClaim convert(Claims claims) {
return new JwtUserClaim(
claims.get(USER_ID, Long.class),
UserRole.valueOf(claims.get(USER_ROLE, String.class))
UserRole.valueOf(claims.get(USER_ROLE, String.class)),
claims.get(IS_WITHDRAWN, Boolean.class)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

public record JwtUserClaim(
Long userId,
UserRole role
UserRole role,
Boolean withdrawn
) {
public static JwtUserClaim create(User user) {
return new JwtUserClaim(user.getId(), user.getRole());
return new JwtUserClaim(user.getId(), user.getRole(), user.getDeletedAt() == null);
}
public static JwtUserClaim create(Long userId, UserRole role) {
return new JwtUserClaim(userId, role);
public static JwtUserClaim create(Long userId, UserRole role, Boolean withdrawn) {
return new JwtUserClaim(userId, role, withdrawn);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@



import com.gpt.geumpumtabackend.global.exception.BusinessException;
import com.gpt.geumpumtabackend.global.exception.ExceptionType;
import com.gpt.geumpumtabackend.global.jwt.exception.JwtAccessDeniedException;
import com.gpt.geumpumtabackend.global.jwt.exception.JwtAuthenticationException;
import com.gpt.geumpumtabackend.global.jwt.exception.JwtTokenExpiredException;
import com.gpt.geumpumtabackend.global.jwt.exception.JwtTokenInvalidException;
import com.gpt.geumpumtabackend.user.domain.User;
import com.gpt.geumpumtabackend.user.domain.UserRole;
import com.gpt.geumpumtabackend.user.repository.UserRepository;
import com.gpt.geumpumtabackend.user.service.UserService;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.RequiredArgsConstructor;
Expand All @@ -23,6 +28,7 @@ public class TokenProvider implements AuthenticationProvider {

private final JwtHandler jwtHandler;
private final UserService userService;
private final UserRepository userRepository;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Expand All @@ -32,14 +38,13 @@ public Authentication authenticate(Authentication authentication) throws Authent
log.info("null이에요");
return null;
}
log.info("널이 아니에여ㅛ");
try {
JwtUserClaim claims = jwtHandler.parseToken(tokenValue);
this.validateFarmerRole(claims);
this.validateAdminRole(claims);
return new JwtAuthentication(claims);
} catch (ExpiredJwtException e) {
throw new JwtTokenExpiredException(e);
} catch (JwtAccessDeniedException e) {
} catch (JwtAuthenticationException e) {
throw e;
} catch (Exception e) {
throw new JwtTokenInvalidException(e);
Expand All @@ -51,7 +56,7 @@ public boolean supports(Class<?> authentication) {
return JwtAuthenticationToken.class.isAssignableFrom(authentication);
}

private void validateFarmerRole(JwtUserClaim claims) {
private void validateAdminRole(JwtUserClaim claims) {
Long userId = claims.userId();

// 토큰의 권한은 FARMER지만 DB에 저장된 권한이 FARMER가 아닌 경우 예외 반환
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
OAuth2UserPrincipal principal = (OAuth2UserPrincipal) authentication.getPrincipal();
Long userId = principal.getUser().getId();
UserRole role = principal.getUser().getRole();
Boolean isWithdrawn = principal.getUser().getDeletedAt() != null;

JwtUserClaim jwtUserClaim = new JwtUserClaim(userId,role);
JwtUserClaim jwtUserClaim = new JwtUserClaim(userId, role, isWithdrawn);
Token token = jwtHandler.createTokens(jwtUserClaim);

// 토큰 붙여서 리다이렉트
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,35 +99,33 @@ private Map<String, Object> loadAppleAttributes(OAuth2UserRequest userRequest) {

private User getOrSave(OAuth2UserInfo oAuth2UserInfo, String registrationId, String providerId) {

OAuth2Provider provider;
log.info("oauth2UserInfo: {}", oAuth2UserInfo);
if(registrationId.equals("google")) {
provider = OAuth2Provider.GOOGLE;
}
else if(registrationId.equals("kakao")) {
provider = OAuth2Provider.KAKAO;
}
else if(registrationId.equals("apple")) {
provider = OAuth2Provider.APPLE;
}
else {
provider = null;
}
OAuth2Provider provider = resolveProvider(registrationId);

return userRepository.findByProviderAndProviderId(provider, providerId)
.orElseGet(() -> createNewUser(oAuth2UserInfo, provider, providerId));
}

private OAuth2Provider resolveProvider(String registrationId) {
return switch (registrationId) {
case "google" -> OAuth2Provider.GOOGLE;
case "kakao" -> OAuth2Provider.KAKAO;
case "apple" -> OAuth2Provider.APPLE;
default -> throw new IllegalArgumentException("Unsupported provider: " + registrationId);
};
}

private User createNewUser(OAuth2UserInfo info, OAuth2Provider provider, String providerId) {
User user = User.builder()
.email(info.email())
.role(UserRole.GUEST)
.name(info.name())
.picture(info.profile())
.provider(provider)
.providerId(providerId)
.build();

User user = userRepository.findByEmail(oAuth2UserInfo.email())
.orElseGet(() ->
User.builder()
.email(oAuth2UserInfo.email())
.role(UserRole.GUEST)
.name(oAuth2UserInfo.name())
.picture(oAuth2UserInfo.profile())
.provider(provider)
.providerId(providerId)
.build()
);
return userRepository.save(user);
}


}
62 changes: 58 additions & 4 deletions src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.*;

@Tag(name = "사용자 API", description = "사용자 관련 API")
public interface UserApi {
Expand Down Expand Up @@ -117,4 +114,61 @@ public ResponseEntity<ResponseBody<Void>> updateProfile(
@RequestBody @Valid ProfileUpdateRequest request,
@Parameter(hidden = true) Long userId
);

@Operation(
summary = "로그아웃을 위한 api",
description = "로그아웃을 진행합니다."
)
@SwaggerApiResponses(
success = @SwaggerApiSuccessResponse(description = "로그아웃 완료"),
errors = {
@SwaggerApiFailedResponse(ExceptionType.NEED_AUTHORIZED),
@SwaggerApiFailedResponse(ExceptionType.USER_NOT_FOUND),
}
)
@DeleteMapping("/logout")
@AssignUserId
@PreAuthorize("isAuthenticated() and hasRole('USER')")
public ResponseEntity<ResponseBody<Void>> logout(
Long userId
);

@Operation(
summary = "회원탈퇴를 위한 api",
description = "회원탈퇴를 진행합니다."
)
@SwaggerApiResponses(
success = @SwaggerApiSuccessResponse(description = "회원탈퇴 완료"),
errors = {
@SwaggerApiFailedResponse(ExceptionType.NEED_AUTHORIZED),
@SwaggerApiFailedResponse(ExceptionType.USER_NOT_FOUND),
}
)
@DeleteMapping("/withdraw")
@AssignUserId
@PreAuthorize("isAuthenticated() and hasRole('USER')")
public ResponseEntity<ResponseBody<Void>> withdrawCurrentUser(
Long userId
);

@Operation(
summary = "회원탈퇴한 계정 복구를 위한 api",
description = "회원탈퇴한 계정을 복구합니다."
)
@ApiResponse(content = @Content(schema = @Schema(implementation = TokenResponse.class)))
@SwaggerApiResponses(
success = @SwaggerApiSuccessResponse(
response = TokenResponse.class,
description = "계정 복구 완료"),
errors = {
@SwaggerApiFailedResponse(ExceptionType.NEED_AUTHORIZED),
@SwaggerApiFailedResponse(ExceptionType.USER_NOT_FOUND),
}
)
@PostMapping("/restore")
@AssignUserId
@PreAuthorize("isAuthenticated() and hasRole('USER')")
public ResponseEntity<ResponseBody<TokenResponse>> restoreUser(
Long userId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,33 @@ public ResponseEntity<ResponseBody<Void>> updateProfile(
userService.updateUserProfile(request, userId);
return ResponseEntity.ok(ResponseUtil.createSuccessResponse());
}

@DeleteMapping("/logout")
@AssignUserId
@PreAuthorize("isAuthenticated() and hasRole('USER')")
public ResponseEntity<ResponseBody<Void>> logout(
Long userId
){
userService.logout(userId);
return ResponseEntity.ok(ResponseUtil.createSuccessResponse());
}
@DeleteMapping("/withdraw")
@AssignUserId
@PreAuthorize("isAuthenticated() and hasRole('USER')")
public ResponseEntity<ResponseBody<Void>> withdrawCurrentUser(
Long userId
){
userService.withdrawUser(userId);
return ResponseEntity.ok(ResponseUtil.createSuccessResponse());
}

@PostMapping("/restore")
@AssignUserId
@PreAuthorize("isAuthenticated() and hasRole('USER')")
public ResponseEntity<ResponseBody<TokenResponse>> restoreUser(
Long userId
){
TokenResponse response = userService.restoreUser(userId);
return ResponseEntity.ok(ResponseUtil.createSuccessResponse(response));
}
}
Loading