diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 15bc21377..ce79d85f1 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.security.Principal; + @RestController @RequestMapping("auth") @RequiredArgsConstructor @@ -32,4 +34,10 @@ public CustomResponse signUp(@RequestBody SignUpRequestDto signUpRequestDto) { boolean status = authService.signUp(signUpRequestDto); return new StatusResponse(status); } + + @PostMapping("/sign-out") + public CustomResponse signOut(Principal principal) { + boolean status = authService.signOut(principal.getName()); + return new StatusResponse(status); + } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index fa76f7602..6c50de055 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -3,6 +3,7 @@ import com.example.solidconnection.auth.dto.SignUpRequestDto; import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.config.token.TokenValidator; import com.example.solidconnection.country.CountryRepository; import com.example.solidconnection.country.InterestedCountyRepository; @@ -15,12 +16,14 @@ import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.type.Role; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.example.solidconnection.custom.exception.ErrorCode.*; @@ -28,6 +31,8 @@ @Service @RequiredArgsConstructor public class AuthService { + + private final RedisTemplate redisTemplate; private final TokenValidator tokenValidator; private final TokenService tokenService; private final SiteUserRepository siteUserRepository; @@ -50,6 +55,16 @@ public boolean signUp(SignUpRequestDto signUpRequestDto) { return true; } + public boolean signOut(String email){ + redisTemplate.opsForValue().set( + TokenType.REFRESH.getPrefix() + email, + "signOut", + TokenType.REFRESH.getExpireTime(), + TimeUnit.MILLISECONDS + ); + return true; + } + private void validateUserNotDuplicated(SignUpRequestDto signUpRequestDto){ String email = tokenService.getEmail(signUpRequestDto.getKakaoOauthToken()); if(siteUserRepository.existsByEmail(email)){ @@ -113,5 +128,4 @@ private void saveInterestedRegion(SignUpRequestDto signUpRequestDto, SiteUser sa .collect(Collectors.toList()); interestedRegionRepository.saveAll(interestedRegions); } - } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 2bad197e7..7ca357158 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -21,11 +22,20 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { + AuthenticationException authException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } + + public void customCommence(HttpServletRequest request, HttpServletResponse response, + CustomException customException) throws IOException { + ErrorResponse errorResponse = new ErrorResponse(customException); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 2ea95cffe..4b37e3077 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -2,6 +2,7 @@ import com.example.solidconnection.config.token.TokenService; import com.example.solidconnection.config.token.TokenValidator; +import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -48,6 +49,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 } catch (AuthenticationException e) { jwtAuthenticationEntryPoint.commence(request, response, e); + } catch (CustomException e){ + jwtAuthenticationEntryPoint.customCommence(request, response, e); } } diff --git a/src/main/java/com/example/solidconnection/config/token/TokenService.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java index 58548f94d..31617f607 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenService.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenService.java @@ -1,6 +1,6 @@ package com.example.solidconnection.config.token; -import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.userdetails.CustomUserDetailsService; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -16,13 +16,12 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.custom.exception.ErrorCode.EMAIL_NOT_FOUND; - @Component @RequiredArgsConstructor public class TokenService { private final RedisTemplate redisTemplate; private final SiteUserRepository siteUserRepository; + private final CustomUserDetailsService customUserDetailsService; @Value("${jwt.secret}") private String secretKey; @@ -50,8 +49,7 @@ public void saveToken(String token, TokenType tokenType) { public Authentication getAuthentication(String token) { String email = getClaim(token).getSubject(); - UserDetails userDetails = (UserDetails) siteUserRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(EMAIL_NOT_FOUND)); + UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java index 99d4bf3fd..e66846196 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java @@ -25,17 +25,24 @@ public class TokenValidator { public void validateAccessToken(String token) { validateTokenNotEmpty(token); validateTokenNotExpired(token, TokenType.ACCESS); + validateNotSignOut(token); validateRefreshToken(token); - // TODO : validateNotLogOut 함수 생성 및 추가 } private void validateRefreshToken(String token) { String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email) != null) { + if (redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email) == null) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } } + private void validateNotSignOut(String token) { + String email = getClaim(token).getSubject(); + if ("signOut".equals(redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email))) { + throw new CustomException(USER_ALREADY_SIGN_OUT); + } + } + public void validateKakaoToken(String token) { validateTokenNotEmpty(token); validateTokenNotExpired(token, TokenType.KAKAO_OAUTH); diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 6b14c997e..10bd81800 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,6 +7,7 @@ @Getter @AllArgsConstructor public enum ErrorCode { + USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), USER_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 회원입니다."), JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱 에러"), INVALID_REGION_NAME(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 지역명입니다."), @@ -15,7 +16,7 @@ public enum ErrorCode { NICKNAME_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 닉네임입니다."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), - EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), INVALID_KAKAO_AUTH_CODE(HttpStatus.BAD_REQUEST.value(),"사용할 수 없는 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 사용자 정보 조회에 실패했습니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java new file mode 100644 index 000000000..53af94c94 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.custom.userdetails; + +import com.example.solidconnection.entity.SiteUser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + private final SiteUser siteUser; + + public CustomUserDetails(SiteUser siteUser) { + this.siteUser = siteUser; + } + + public String getEmail(){ + return siteUser.getEmail(); + } + + @Override + public String getUsername() { + return siteUser.getEmail(); + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } +} diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java new file mode 100644 index 000000000..51cfb78b9 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.custom.userdetails; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final SiteUserRepository siteUserRepository; + + @Override + public UserDetails loadUserByUsername(String username) { + SiteUser siteUser = siteUserRepository.findByEmail(username) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND, username)); + return new CustomUserDetails(siteUser); + } +}