diff --git a/src/main/java/org/sophy/sophy/common/advice/ControllerExceptionAdvice.java b/src/main/java/org/sophy/sophy/common/advice/ControllerExceptionAdvice.java index 2a09062..fc701a8 100644 --- a/src/main/java/org/sophy/sophy/common/advice/ControllerExceptionAdvice.java +++ b/src/main/java/org/sophy/sophy/common/advice/ControllerExceptionAdvice.java @@ -1,5 +1,6 @@ package org.sophy.sophy.common.advice; +import io.jsonwebtoken.ExpiredJwtException; import org.sophy.sophy.common.dto.ApiResponseDto; import org.sophy.sophy.exception.ErrorStatus; import org.springframework.http.HttpStatus; @@ -8,7 +9,6 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.sophy.sophy.exception.model.SophyException; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice @@ -30,6 +30,12 @@ protected ApiResponseDto handleMethodArgumentNotValidException(final MethodArgum // return ApiResponseDto.error(ErrorStatus.INTERNAL_SERVER_ERROR); // } + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(ExpiredJwtException.class) + protected ApiResponseDto handleExpiredRefreshTokenException(final ExpiredJwtException e) { + return ApiResponseDto.error(ErrorStatus.REFRESH_TOKEN_TIME_EXPIRED_EXCEPTION); + } + /** * Sopt custom error */ diff --git a/src/main/java/org/sophy/sophy/config/JwtSecurityConfig.java b/src/main/java/org/sophy/sophy/config/JwtSecurityConfig.java index 63877bc..00471f6 100644 --- a/src/main/java/org/sophy/sophy/config/JwtSecurityConfig.java +++ b/src/main/java/org/sophy/sophy/config/JwtSecurityConfig.java @@ -1,6 +1,7 @@ package org.sophy.sophy.config; import lombok.RequiredArgsConstructor; +import org.sophy.sophy.jwt.JwtExceptionFilter; import org.sophy.sophy.jwt.JwtFilter; import org.sophy.sophy.jwt.TokenProvider; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; @@ -12,6 +13,7 @@ @RequiredArgsConstructor public class JwtSecurityConfig extends SecurityConfigurerAdapter { private final TokenProvider tokenProvider; + private final JwtExceptionFilter jwtExceptionFilter; //TokenProvider를 주입받아서 JwtFillter를 통해 Security 로직에 필터를 등록 //HttpSecurity의 userpassword인증필터에 filter 추가 @@ -19,6 +21,7 @@ public class JwtSecurityConfig extends SecurityConfigurerAdapter signup(@RequestBody MemberRequestDto memberRequestDto) { - return ResponseEntity.ok(authService.signup(memberRequestDto)); + @ResponseStatus(HttpStatus.CREATED) + public ApiResponseDto signup(@RequestBody MemberRequestDto memberRequestDto) { + return ApiResponseDto.success(SuccessStatus.SIGNUP_SUCCESS, authService.signup(memberRequestDto)); } @PostMapping("/login") @@ -32,4 +34,9 @@ public ApiResponseDto login(@RequestBody MemberLoginRequestDto memberL public ApiResponseDto reissue(@RequestBody TokenRequestDto tokenRequestDto) { return ApiResponseDto.success(SuccessStatus.REISSUE_SUCCESS, authService.reissue(tokenRequestDto)); } + + @GetMapping("/dupl-check") + public ApiResponseDto duplCheck(@RequestBody DuplCheckDto email) { + return ApiResponseDto.success(SuccessStatus.CHECK_DUPL_EMAIL_SUCCESS, authService.duplCheck(email)); + } } diff --git a/src/main/java/org/sophy/sophy/controller/TestController.java b/src/main/java/org/sophy/sophy/controller/TestController.java new file mode 100644 index 0000000..853906e --- /dev/null +++ b/src/main/java/org/sophy/sophy/controller/TestController.java @@ -0,0 +1,15 @@ +package org.sophy.sophy.controller; + +import org.sophy.sophy.common.dto.ApiResponseDto; +import org.sophy.sophy.exception.SuccessStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @GetMapping("/test") + public ApiResponseDto test() { + return ApiResponseDto.success(SuccessStatus.TEST_SUCCESS, SuccessStatus.TEST_SUCCESS.getMessage()); + } +} diff --git a/src/main/java/org/sophy/sophy/controller/dto/request/DuplCheckDto.java b/src/main/java/org/sophy/sophy/controller/dto/request/DuplCheckDto.java new file mode 100644 index 0000000..4fb1902 --- /dev/null +++ b/src/main/java/org/sophy/sophy/controller/dto/request/DuplCheckDto.java @@ -0,0 +1,17 @@ +package org.sophy.sophy.controller.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class DuplCheckDto { + @Email(message = "이메일 형식에 맞지 않습니다.") + @NotNull + String email; +} diff --git a/src/main/java/org/sophy/sophy/exception/ErrorStatus.java b/src/main/java/org/sophy/sophy/exception/ErrorStatus.java index caf78de..c9930a7 100644 --- a/src/main/java/org/sophy/sophy/exception/ErrorStatus.java +++ b/src/main/java/org/sophy/sophy/exception/ErrorStatus.java @@ -19,7 +19,8 @@ public enum ErrorStatus { /** * 401 UNAUTHORIZED */ - TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), + INVALID_ACCESS_TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, "유효하지 않은 액세스 토큰입니다."), + REFRESH_TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 리프레시 토큰입니다."), /** * 404 NOT FOUND */ @@ -27,6 +28,11 @@ public enum ErrorStatus { NOT_FOUND_SAVE_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 저장에 실패했습니다"), NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 이름을 찾을 수 없습니다"), + /** + * 409 CONFLICT + */ + ALREADY_EXIST_USER_EXCEPTION(HttpStatus.CONFLICT, "이미 존재하는 유저입니다"), + /** * 500 INTERNAL SERVER ERROR */ diff --git a/src/main/java/org/sophy/sophy/exception/SuccessStatus.java b/src/main/java/org/sophy/sophy/exception/SuccessStatus.java index 6b82153..8a5b50d 100644 --- a/src/main/java/org/sophy/sophy/exception/SuccessStatus.java +++ b/src/main/java/org/sophy/sophy/exception/SuccessStatus.java @@ -13,6 +13,8 @@ public enum SuccessStatus { */ LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공했습니다."), REISSUE_SUCCESS(HttpStatus.OK, "토큰 재발행에 성공했습니다."), + CHECK_DUPL_EMAIL_SUCCESS(HttpStatus.OK, "사용 가능한 이메일 주소입니다."), + TEST_SUCCESS(HttpStatus.OK, "Test :: OK"), /* * 201 created */ diff --git a/src/main/java/org/sophy/sophy/exception/model/ExistEmailException.java b/src/main/java/org/sophy/sophy/exception/model/ExistEmailException.java new file mode 100644 index 0000000..7e60df0 --- /dev/null +++ b/src/main/java/org/sophy/sophy/exception/model/ExistEmailException.java @@ -0,0 +1,10 @@ +package org.sophy.sophy.exception.model; + +import org.sophy.sophy.exception.ErrorStatus; + +public class ExistEmailException extends SophyException { + + public ExistEmailException(ErrorStatus errorStatus, String message) { + super(errorStatus, message); + } +} diff --git a/src/main/java/org/sophy/sophy/exception/model/ExpiredRefreshTokenException.java b/src/main/java/org/sophy/sophy/exception/model/ExpiredRefreshTokenException.java new file mode 100644 index 0000000..b9bf2c3 --- /dev/null +++ b/src/main/java/org/sophy/sophy/exception/model/ExpiredRefreshTokenException.java @@ -0,0 +1,10 @@ +package org.sophy.sophy.exception.model; + +import org.sophy.sophy.exception.ErrorStatus; + +public class ExpiredRefreshTokenException extends SophyException { + + public ExpiredRefreshTokenException(ErrorStatus errorStatus, String message) { + super(errorStatus, message); + } +} diff --git a/src/main/java/org/sophy/sophy/infrastructure/MemberRepository.java b/src/main/java/org/sophy/sophy/infrastructure/MemberRepository.java index e530e95..1d25594 100644 --- a/src/main/java/org/sophy/sophy/infrastructure/MemberRepository.java +++ b/src/main/java/org/sophy/sophy/infrastructure/MemberRepository.java @@ -3,10 +3,12 @@ import org.sophy.sophy.domain.Member; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Repository +@Transactional public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); diff --git a/src/main/java/org/sophy/sophy/jwt/JwtExceptionFilter.java b/src/main/java/org/sophy/sophy/jwt/JwtExceptionFilter.java new file mode 100644 index 0000000..f3e953c --- /dev/null +++ b/src/main/java/org/sophy/sophy/jwt/JwtExceptionFilter.java @@ -0,0 +1,39 @@ +package org.sophy.sophy.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.JwtException; +import org.sophy.sophy.common.dto.ApiResponseDto; +import org.sophy.sophy.exception.ErrorStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class JwtExceptionFilter extends OncePerRequestFilter { + + @Autowired + private ObjectMapper objectMapper; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (JwtException exception) { + setErrorResponse(HttpStatus.UNAUTHORIZED, response, exception); + } + } + + public void setErrorResponse(HttpStatus status, HttpServletResponse res, Throwable ex) throws IOException { + res.setStatus(status.value()); + res.setContentType("application/json; charset=UTF-8"); + + res.getWriter().write(objectMapper.writeValueAsString(ApiResponseDto.error(ErrorStatus.INVALID_ACCESS_TOKEN_EXCEPTION, ex.getMessage()))); + } +} diff --git a/src/main/java/org/sophy/sophy/jwt/JwtFilter.java b/src/main/java/org/sophy/sophy/jwt/JwtFilter.java index 6761d78..0be4931 100644 --- a/src/main/java/org/sophy/sophy/jwt/JwtFilter.java +++ b/src/main/java/org/sophy/sophy/jwt/JwtFilter.java @@ -1,5 +1,7 @@ package org.sophy.sophy.jwt; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -23,17 +25,21 @@ public class JwtFilter extends OncePerRequestFilter { // 실제 필터링 로직은 doFilterInternal 에 들어감 // JWT 토큰의 인증 정보를 현재 쓰레드의 SecurityContext 에 저장하는 역할 수행 @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException{ // 1. Request Header에서 토큰을 꺼냄 String jwt = resolveToken(request); // 2. validateToken 으로 토큰 유효성 검사 // 정상 토큰이면 해당 토큰으롤 Authentication을 가져와서 SecurityContext에 저장 - if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { - Authentication authentication = tokenProvider.getAuthentication(jwt); - //Context에 저장할 때 auth를 설정하며 저장 - SecurityContextHolder.getContext().setAuthentication(authentication); + try { + if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + Authentication authentication = tokenProvider.getAuthentication(jwt); + //Context에 저장할 때 auth를 설정하며 저장 + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (ExpiredJwtException e) { //access token이 만료되었을 때 + throw new JwtException("만료된 액세스 토큰입니다."); } filterChain.doFilter(request, response); diff --git a/src/main/java/org/sophy/sophy/jwt/TokenProvider.java b/src/main/java/org/sophy/sophy/jwt/TokenProvider.java index 1036fe3..598faa8 100644 --- a/src/main/java/org/sophy/sophy/jwt/TokenProvider.java +++ b/src/main/java/org/sophy/sophy/jwt/TokenProvider.java @@ -26,8 +26,10 @@ public class TokenProvider { private static final String AUTHORITIES_KEY = "auth"; private static final String BEARER_TYPE = "Bearer"; - private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; - private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; + private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 10; +// private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; + private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 30; +// private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; private final Key key; @@ -78,7 +80,7 @@ public Authentication getAuthentication(String accessToken) { Claims claims = parseClaims(accessToken); if (claims.get(AUTHORITIES_KEY) == null) { - throw new RuntimeException("권한 정보가 없는 토큰입니다."); + throw new JwtException("권한 정보가 없는 토큰입니다."); } //클레임에서 권한 정보 가져오기 @@ -115,13 +117,13 @@ public boolean validateToken(String token) { return true; } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { log.info("잘못된 JWT 서명입니다."); - } catch (ExpiredJwtException e) { - log.info("만료된 JWT 토큰입니다."); + throw new JwtException("잘못된 JWT 서명입니다."); } catch (UnsupportedJwtException e) { log.info("지원되지 않는 JWT 토큰입니다."); + throw new JwtException("지원되지 않는 JWT 토큰입니다."); } catch (IllegalStateException e) { log.info("JWT 토큰이 잘못되었습니다."); + throw new JwtException("JWT 토큰이 잘못되었습니다."); } - return false; } } diff --git a/src/main/java/org/sophy/sophy/service/AuthService.java b/src/main/java/org/sophy/sophy/service/AuthService.java index 65109cb..7b8131e 100644 --- a/src/main/java/org/sophy/sophy/service/AuthService.java +++ b/src/main/java/org/sophy/sophy/service/AuthService.java @@ -1,12 +1,15 @@ package org.sophy.sophy.service; import lombok.RequiredArgsConstructor; +import org.sophy.sophy.controller.dto.request.DuplCheckDto; import org.sophy.sophy.controller.dto.request.MemberLoginRequestDto; import org.sophy.sophy.controller.dto.request.MemberRequestDto; import org.sophy.sophy.controller.dto.request.TokenRequestDto; import org.sophy.sophy.controller.dto.response.MemberResponseDto; import org.sophy.sophy.controller.dto.response.TokenDto; import org.sophy.sophy.domain.Member; +import org.sophy.sophy.exception.ErrorStatus; +import org.sophy.sophy.exception.model.ExistEmailException; import org.sophy.sophy.infrastructure.MemberRepository; import org.sophy.sophy.jwt.TokenProvider; import org.springframework.data.redis.core.RedisTemplate; @@ -33,13 +36,22 @@ public class AuthService { @Transactional public MemberResponseDto signup(MemberRequestDto memberRequestDto) { if (memberRepository.existsByEmail(memberRequestDto.getEmail())) { - throw new RuntimeException("이미 가입되어 있는 유저입니다."); + throw new ExistEmailException(ErrorStatus.ALREADY_EXIST_USER_EXCEPTION, ErrorStatus.ALREADY_EXIST_USER_EXCEPTION.getMessage()); } Member member = memberRequestDto.toMember(passwordEncoder); return MemberResponseDto.of(memberRepository.save(member)); } + @Transactional + public String duplCheck(DuplCheckDto email) { + + if (memberRepository.existsByEmail(email.getEmail())) { + throw new ExistEmailException(ErrorStatus.ALREADY_EXIST_USER_EXCEPTION, ErrorStatus.ALREADY_EXIST_USER_EXCEPTION.getMessage()); + } + return "사용 가능한 이메일입니다."; + } + @Transactional public TokenDto login(MemberLoginRequestDto memberLoginRequestDto) { // 1. Login ID/PW 를 기반으로 AuthenticationToken 생성 @@ -64,11 +76,9 @@ public TokenDto login(MemberLoginRequestDto memberLoginRequestDto) { } @Transactional - public TokenDto reissue(TokenRequestDto tokenRequestDto) { + public TokenDto reissue(TokenRequestDto tokenRequestDto){ // 1. Refresh Token 검증 - if (!tokenProvider.validateToken(tokenRequestDto.getRefreshToken())) { - throw new RuntimeException("Refresh Token 이 유효하지 않습니다."); - } + tokenProvider.validateToken(tokenRequestDto.getRefreshToken()); // 2. Access Token 에서 Member ID 가져오기 Authentication authentication = tokenProvider.getAuthentication(tokenRequestDto.getAccessToken());