From 2be2513c3d794bb928e969fa48ef9cc712408180 Mon Sep 17 00:00:00 2001 From: sh1220 Date: Tue, 30 Jul 2024 18:35:47 +0900 Subject: [PATCH 1/4] feat: logout & refresh --- build.gradle | 7 +- .../interceptor/JwtAuthInterceptor.java | 7 +- .../JwtAuthRefreshInterceptor.java | 71 ++++++++++++ .../status/BaseExceptionResponseStatus.java | 8 +- .../itpick/backend/config/WebConfig.java | 4 +- .../backend/controller/UserController.java | 34 ++++-- .../store/itpick/backend/dto/auth/JwtDTO.java | 16 +++ .../backend/dto/auth/LoginResponse.java | 2 +- .../backend/dto/auth/LogoutRequest.java | 15 +++ .../backend/dto/auth/RefreshRequest.java | 15 +++ .../backend/dto/auth/RefreshResponse.java | 8 ++ .../store/itpick/backend/jwt/JwtProvider.java | 22 +++- .../itpick/backend/service/UserService.java | 105 +++++++++++++++--- src/main/resources/application.yml | 9 +- 14 files changed, 285 insertions(+), 38 deletions(-) create mode 100644 src/main/java/store/itpick/backend/common/interceptor/JwtAuthRefreshInterceptor.java create mode 100644 src/main/java/store/itpick/backend/dto/auth/JwtDTO.java create mode 100644 src/main/java/store/itpick/backend/dto/auth/LogoutRequest.java create mode 100644 src/main/java/store/itpick/backend/dto/auth/RefreshRequest.java create mode 100644 src/main/java/store/itpick/backend/dto/auth/RefreshResponse.java diff --git a/build.gradle b/build.gradle index da4857d..1188dd5 100644 --- a/build.gradle +++ b/build.gradle @@ -51,8 +51,11 @@ dependencies { implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - runtimeOnly 'com.mysql:mysql-connector-java' - runtimeOnly 'com.h2database:h2'} + runtimeOnly 'com.h2database:h2' + + //redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' +} tasks.named('test') { useJUnitPlatform() diff --git a/src/main/java/store/itpick/backend/common/interceptor/JwtAuthInterceptor.java b/src/main/java/store/itpick/backend/common/interceptor/JwtAuthInterceptor.java index aee2ee5..fea855b 100644 --- a/src/main/java/store/itpick/backend/common/interceptor/JwtAuthInterceptor.java +++ b/src/main/java/store/itpick/backend/common/interceptor/JwtAuthInterceptor.java @@ -13,6 +13,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; +import store.itpick.backend.service.UserService; import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; @@ -22,8 +23,8 @@ public class JwtAuthInterceptor implements HandlerInterceptor { private static final String JWT_TOKEN_PREFIX = "Bearer "; - private final JwtProvider jwtProvider; + private final UserService userService; // 컨트롤러 호출전에 JWT 검증 @Override @@ -35,8 +36,8 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons String email = jwtProvider.getPrincipal(accessToken); validatePayload(email); - // long userId = authService.getUserIdByEmail(email); - // request.setAttribute("userId", userId); + long userId = userService.getUserIdByEmail(email); + request.setAttribute("userId", userId); return true; } diff --git a/src/main/java/store/itpick/backend/common/interceptor/JwtAuthRefreshInterceptor.java b/src/main/java/store/itpick/backend/common/interceptor/JwtAuthRefreshInterceptor.java new file mode 100644 index 0000000..8be33dc --- /dev/null +++ b/src/main/java/store/itpick/backend/common/interceptor/JwtAuthRefreshInterceptor.java @@ -0,0 +1,71 @@ +package store.itpick.backend.common.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import store.itpick.backend.common.exception.jwt.bad_request.JwtNoTokenException; +import store.itpick.backend.common.exception.jwt.bad_request.JwtUnsupportedTokenException; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtExpiredTokenException; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtInvalidTokenException; +import store.itpick.backend.jwt.JwtProvider; +import store.itpick.backend.service.UserService; + +import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthRefreshInterceptor implements HandlerInterceptor { + + private static final String JWT_TOKEN_PREFIX = "Bearer "; + private final JwtProvider jwtProvider; + private final UserService userService; + + // 컨트롤러 호출전에 JWT 검증 + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + + String refreshToken = resolveRefreshToken(request); + validateRefreshToken(refreshToken); + + String email = jwtProvider.getPrincipal(refreshToken); + validatePayload(email); + + long userId = userService.getUserIdByEmail(email); + request.setAttribute("userId", userId); + return true; + + } + + private String resolveRefreshToken(HttpServletRequest request) { + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + validateToken(token); + return token.substring(JWT_TOKEN_PREFIX.length()); + } + + private void validateToken(String token) { + if (token == null) { + throw new JwtNoTokenException(TOKEN_NOT_FOUND); + } + if (!token.startsWith(JWT_TOKEN_PREFIX)) { + throw new JwtUnsupportedTokenException(UNSUPPORTED_TOKEN_TYPE); + } + } + + private void validateRefreshToken(String accessToken) { + if (jwtProvider.isExpiredToken(accessToken)) { + throw new JwtExpiredTokenException(EXPIRED_TOKEN); + } + } + + private void validatePayload(String email) { + if (email == null) { + throw new JwtInvalidTokenException(INVALID_TOKEN); + } + } + +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java index 5c1d09c..0720a93 100644 --- a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java +++ b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java @@ -34,7 +34,9 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { INVALID_TOKEN(4003, HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), MALFORMED_TOKEN(4004, HttpStatus.UNAUTHORIZED.value(), "토큰이 올바르게 구성되지 않았습니다."), EXPIRED_TOKEN(4005, HttpStatus.UNAUTHORIZED.value(), "만료된 토큰입니다."), - TOKEN_MISMATCH(4006, HttpStatus.UNAUTHORIZED.value(), "로그인 정보가 토큰 정보와 일치하지 않습니다."), + TOKEN_MISMATCH(4006, HttpStatus.UNAUTHORIZED.value(), "회원 정보가 토큰 정보와 일치하지 않습니다."), + EXPIRED_REFRESH_TOKEN(4007, HttpStatus.UNAUTHORIZED.value(), "다시 로그인 해주세요."), + /** * 5000: User 오류 @@ -46,7 +48,9 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { PASSWORD_NO_MATCH(5004, HttpStatus.BAD_REQUEST.value(), "비밀번호가 일치하지 않습니다."), INVALID_USER_STATUS(5005, HttpStatus.BAD_REQUEST.value(), "잘못된 회원 status 값입니다."), EMAIL_NOT_FOUND(5006, HttpStatus.BAD_REQUEST.value(), "존재하지 않는 이메일입니다."), - INVALID_PASSWORD(5007, HttpStatus.BAD_REQUEST.value(), "유효하지 않는 password입니다."); + INVALID_PASSWORD(5007, HttpStatus.BAD_REQUEST.value(), "유효하지 않는 password입니다."), + INVALID_REFRESHTOKEN(5008, HttpStatus.BAD_REQUEST.value(), "유효하지 않는 토큰입니다."); + /** diff --git a/src/main/java/store/itpick/backend/config/WebConfig.java b/src/main/java/store/itpick/backend/config/WebConfig.java index bfe29f6..cf56ebc 100644 --- a/src/main/java/store/itpick/backend/config/WebConfig.java +++ b/src/main/java/store/itpick/backend/config/WebConfig.java @@ -21,8 +21,8 @@ public class WebConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtAuthenticationInterceptor) .order(1) - .addPathPatterns("/auth/test","/users/**") - .excludePathPatterns("/users"); + .addPathPatterns("/**") + .excludePathPatterns("/auth/login", "/auth/singup", "/auth/refresh"); //인터셉터 적용 범위 수정 } diff --git a/src/main/java/store/itpick/backend/controller/UserController.java b/src/main/java/store/itpick/backend/controller/UserController.java index f1c81d0..8e051e4 100644 --- a/src/main/java/store/itpick/backend/controller/UserController.java +++ b/src/main/java/store/itpick/backend/controller/UserController.java @@ -8,12 +8,15 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import store.itpick.backend.common.argument_resolver.PreAuthorize; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtExpiredTokenException; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtInvalidTokenException; import store.itpick.backend.common.response.BaseResponse; -import store.itpick.backend.dto.auth.LoginRequest; -import store.itpick.backend.dto.auth.LoginResponse; +import store.itpick.backend.dto.auth.*; import store.itpick.backend.dto.user.user.PostUserRequest; import store.itpick.backend.dto.user.user.PostUserResponse; import store.itpick.backend.jwt.JwtProvider; @@ -26,19 +29,23 @@ import java.util.Map; import java.util.Optional; -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_USER_VALUE; +import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; +import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_TOKEN; import static store.itpick.backend.util.BindingResultUtils.getErrorMessages; @Slf4j @RestController @RequiredArgsConstructor +@RequestMapping("/auth") public class UserController { @Autowired private final UserService userService; - @Autowired - private JwtProvider jwtProvider; + @PostMapping("/refresh") + public BaseResponse refresh(@Validated @RequestBody RefreshRequest refreshRequest) { + return new BaseResponse<>(userService.refresh(refreshRequest.getRefreshToken())); + } /** @@ -53,8 +60,13 @@ public BaseResponse login(@Validated @RequestBody LoginRequest au return new BaseResponse<>(userService.login(authRequest)); } - @PostMapping("/logout") - public BaseResponse logoutUser() { + /** + * 로그아웃 : db의 refresh 토큰을 null로 설정 + */ + @PatchMapping("/{userId}/logout") + public BaseResponse logout(@PathVariable Long userId, @PreAuthorize long header_userId) { + userService.validationUserId(userId, header_userId); + userService.logout(userId); return new BaseResponse<>(null); } @@ -67,8 +79,14 @@ public BaseResponse signUp(@RequestBody PostUserRequest postUs return new BaseResponse<>(userService.signUp(postUserRequest)); } + + + + @PatchMapping("/{userId}/deleted") - public BaseResponse modifyUserStatus_deleted(@PathVariable Long userId) { + public BaseResponse modifyUserStatus_deleted(@PathVariable Long userId, @PreAuthorize long header_userId) { + // userId validation 추가 + userService.validationUserId(userId, header_userId); userService.modifyUserStatus_deleted(userId); return new BaseResponse<>(null); } diff --git a/src/main/java/store/itpick/backend/dto/auth/JwtDTO.java b/src/main/java/store/itpick/backend/dto/auth/JwtDTO.java new file mode 100644 index 0000000..cdbb129 --- /dev/null +++ b/src/main/java/store/itpick/backend/dto/auth/JwtDTO.java @@ -0,0 +1,16 @@ +package store.itpick.backend.dto.auth; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class JwtDTO { + private String accessToken; + private String refreshToken; + + public JwtDTO(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/dto/auth/LoginResponse.java b/src/main/java/store/itpick/backend/dto/auth/LoginResponse.java index 8f262d3..04f5d57 100644 --- a/src/main/java/store/itpick/backend/dto/auth/LoginResponse.java +++ b/src/main/java/store/itpick/backend/dto/auth/LoginResponse.java @@ -8,6 +8,6 @@ public class LoginResponse { private long userId; - private String jwt; + private JwtDTO jwt; } diff --git a/src/main/java/store/itpick/backend/dto/auth/LogoutRequest.java b/src/main/java/store/itpick/backend/dto/auth/LogoutRequest.java new file mode 100644 index 0000000..07793a3 --- /dev/null +++ b/src/main/java/store/itpick/backend/dto/auth/LogoutRequest.java @@ -0,0 +1,15 @@ +package store.itpick.backend.dto.auth; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class LogoutRequest { + + @NotBlank(message = "refreshToken: {NotBlank}") + private String refreshToken; +} diff --git a/src/main/java/store/itpick/backend/dto/auth/RefreshRequest.java b/src/main/java/store/itpick/backend/dto/auth/RefreshRequest.java new file mode 100644 index 0000000..ad17b9d --- /dev/null +++ b/src/main/java/store/itpick/backend/dto/auth/RefreshRequest.java @@ -0,0 +1,15 @@ +package store.itpick.backend.dto.auth; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class RefreshRequest { + + @NotBlank(message = "refreshToken: {NotBlank}") + private String refreshToken; +} diff --git a/src/main/java/store/itpick/backend/dto/auth/RefreshResponse.java b/src/main/java/store/itpick/backend/dto/auth/RefreshResponse.java new file mode 100644 index 0000000..652fbcc --- /dev/null +++ b/src/main/java/store/itpick/backend/dto/auth/RefreshResponse.java @@ -0,0 +1,8 @@ +package store.itpick.backend.dto.auth; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class RefreshResponse { + private String refreshToken; +} diff --git a/src/main/java/store/itpick/backend/jwt/JwtProvider.java b/src/main/java/store/itpick/backend/jwt/JwtProvider.java index 6ca28ac..c137338 100644 --- a/src/main/java/store/itpick/backend/jwt/JwtProvider.java +++ b/src/main/java/store/itpick/backend/jwt/JwtProvider.java @@ -22,6 +22,9 @@ public class JwtProvider { @Value("${secret.jwt-expired-in}") private long JWT_EXPIRED_IN; + @Value("${secret.jwt-refresh-expired-in}") + private long JWT_REFRESH_EXPIRED_IN; + public String createToken(String principal, long userId) { log.info("JWT key={}", JWT_SECRET_KEY); @@ -38,16 +41,31 @@ public String createToken(String principal, long userId) { .compact(); } + public String createRefreshToken(String principal, long userId) { + log.info("JWT key={}", JWT_SECRET_KEY); + + Claims claims = Jwts.claims().setSubject(principal); + Date now = new Date(); + Date validity = new Date(now.getTime() + JWT_REFRESH_EXPIRED_IN); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .claim("userId", userId) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET_KEY) + .compact(); + } + public boolean isExpiredToken(String token) throws JwtInvalidTokenException { try { Jws claims = Jwts.parserBuilder() .setSigningKey(JWT_SECRET_KEY).build() - .parseClaimsJws(token); + .parseClaimsJws(token); // 유효성 확인 return claims.getBody().getExpiration().before(new Date()); } catch (ExpiredJwtException e) { return true; - } catch (UnsupportedJwtException e) { throw new JwtUnsupportedTokenException(UNSUPPORTED_TOKEN_TYPE); } catch (MalformedJwtException e) { diff --git a/src/main/java/store/itpick/backend/service/UserService.java b/src/main/java/store/itpick/backend/service/UserService.java index 089f579..532a2ac 100644 --- a/src/main/java/store/itpick/backend/service/UserService.java +++ b/src/main/java/store/itpick/backend/service/UserService.java @@ -1,15 +1,20 @@ package store.itpick.backend.service; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import store.itpick.backend.common.exception.UserException; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtExpiredTokenException; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtInvalidTokenException; import store.itpick.backend.common.response.status.BaseExceptionResponseStatus; +import store.itpick.backend.dto.auth.JwtDTO; import store.itpick.backend.dto.auth.LoginRequest; import store.itpick.backend.dto.auth.LoginResponse; +import store.itpick.backend.dto.auth.RefreshResponse; import store.itpick.backend.dto.user.user.PostUserRequest; import store.itpick.backend.dto.user.user.PostUserResponse; import store.itpick.backend.jwt.JwtProvider; @@ -19,9 +24,10 @@ import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.EMAIL_NOT_FOUND; -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.PASSWORD_NO_MATCH; +import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; +import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_TOKEN; @Service @@ -30,27 +36,41 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - private final JwtProvider jwtTokenProvider; + private final JwtProvider jwtProvider; + + + @Value("${secret.jwt-refresh-expired-in}") + private long JWT_REFRESH_EXPIRED_IN; + + + + + public LoginResponse login(LoginRequest authRequest) { String email = authRequest.getEmail(); // TODO: 1. 이메일 유효성 확인 - long userId; + User user; try { - userId = userRepository.getUserByEmail(email).get().getUserId(); + user = userRepository.getUserByEmail(email).get(); } catch (IncorrectResultSizeDataAccessException e) { throw new UserException(EMAIL_NOT_FOUND); } + long userId = user.getUserId(); // TODO: 2. 비밀번호 일치 확인 validatePassword(authRequest.getPassword(), userId); // TODO: 3. JWT 갱신 - String updatedJwt = jwtTokenProvider.createToken(email, userId); + String updatedAccessToken = jwtProvider.createToken(email, userId); + String updatedRefreshToken = jwtProvider.createRefreshToken(email, userId); + user.setRefreshToken(updatedRefreshToken); + userRepository.save(user); + JwtDTO jwtDTO = new JwtDTO(updatedAccessToken, updatedRefreshToken); - return new LoginResponse(userId, updatedJwt); + return new LoginResponse(userId, jwtDTO); } private void validatePassword(String password, long userId) { @@ -78,24 +98,52 @@ public PostUserResponse signUp(PostUserRequest postUserRequest) { user = userRepository.save(user); + // 회원가입에서 jwt발급 x -> 회원가입 후 로그인 로직으로 가는게 좋을듯 (일단 토의하기) + // 이런식이면 return null; + // 회원가입에서 jwt 생성 시 여기에 로직 포함시키기 String jwt = ""; return new PostUserResponse(user.getUserId(), jwt); } - private void validateEmail(String email) { - if (userRepository.existsByEmailAndStatusIn(email, List.of("active", "dormant"))) { - throw new UserException(BaseExceptionResponseStatus.DUPLICATE_EMAIL); + public RefreshResponse refresh(String refreshToken){ + // 만료 & 유효성 확인, 로그아웃 확인 + if(jwtProvider.isExpiredToken(refreshToken) || refreshToken == null || refreshToken.isEmpty()){ + throw new JwtExpiredTokenException(EXPIRED_REFRESH_TOKEN); + } + String email = jwtProvider.getPrincipal(refreshToken); + if (email == null) { + throw new JwtInvalidTokenException(INVALID_TOKEN); } + + // 이메일 유효성 확인 + User user; + try { + user = userRepository.getUserByEmail(email).get(); + } catch (IncorrectResultSizeDataAccessException e) { + throw new UserException(EMAIL_NOT_FOUND); + } + long userId = user.getUserId(); + + // 엑세스 토큰 재발급 + return new RefreshResponse(jwtProvider.createToken(email, userId)); } - private void validateNickname(String nickname) { - if (userRepository.existsByNicknameAndStatusIn(nickname, List.of("active", "dormant"))) { - throw new UserException(BaseExceptionResponseStatus.DUPLICATE_NICKNAME); + // 로그아웃 + public void logout(long userId) { + + User user; + try { + user = userRepository.getUserByUserId(userId).get(); + } catch (IncorrectResultSizeDataAccessException e) { + throw new UserException(USER_NOT_FOUND); } + user.setRefreshToken(null); + userRepository.save(user); } + public void modifyUserStatus_deleted(long userId) { Optional optionalUser = userRepository.findById(userId); if (optionalUser.isPresent()) { @@ -107,4 +155,33 @@ public void modifyUserStatus_deleted(long userId) { } } + + + + + + + private void validateEmail(String email) { + if (userRepository.existsByEmailAndStatusIn(email, List.of("active", "dormant"))) { + throw new UserException(BaseExceptionResponseStatus.DUPLICATE_EMAIL); + } + } + + private void validateNickname(String nickname) { + if (userRepository.existsByNicknameAndStatusIn(nickname, List.of("active", "dormant"))) { + throw new UserException(BaseExceptionResponseStatus.DUPLICATE_NICKNAME); + } + } + + public long getUserIdByEmail(String email) { + return userRepository.getUserByEmail(email).get().getUserId(); + } + + public void validationUserId(long userId, long header_userId) { + if (userId == header_userId) { + throw new UserException(TOKEN_MISMATCH); + } + } + + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2d40a7b..a8c7820 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,7 +12,9 @@ spring: activate: on-profile: "localDB" + datasource: + driver-class-name: org.h2.Driver # url: ${DATASOURCE_URL} # username: ${DATASOURCE_USERNAME} # password: ${DATASOURCE_PASSWORD} @@ -22,18 +24,16 @@ spring: # driver-class-name: com.mysql.cj.jdbc.Driver dbcp2: validation-query: select 1 - hikari: - driver-class-name: org.h2.Driver sql: init: - platform: mysql + platform: h2 jpa: show-sql: true hibernate: ddl-auto: none properties: hibernate: - dialect: org.hibernate.dialect.MySQLDialect + dialect: org.hibernate.dialect.MySQL5Dialect h2: console: enabled: true @@ -118,6 +118,7 @@ spring: secret: jwt-secret-key: ${JWT_SECRET_KEY} jwt-expired-in: ${JWT_EXPIRED_IN} + jwt-refresh-expired-in: ${JWT_REFRESH_EXPIRED_IN} --- From 775cf4a668a62cc09ee16487b61e045de6493f11 Mon Sep 17 00:00:00 2001 From: sh1220 Date: Tue, 30 Jul 2024 18:37:18 +0900 Subject: [PATCH 2/4] test: build --- src/main/resources/application.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a8c7820..4f2da8b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,12 +15,12 @@ spring: datasource: driver-class-name: org.h2.Driver -# url: ${DATASOURCE_URL} -# username: ${DATASOURCE_USERNAME} -# password: ${DATASOURCE_PASSWORD} - url: jdbc:h2:tcp://localhost/~/itpick - username: sa - password: + url: ${DATASOURCE_URL} + username: ${DATASOURCE_USERNAME} + password: ${DATASOURCE_PASSWORD} +# url: jdbc:h2:tcp://localhost/~/itpick +# username: sa +# password: # driver-class-name: com.mysql.cj.jdbc.Driver dbcp2: validation-query: select 1 From de04eb580b1ca12532e5dc4571443e80b57e3e28 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 31 Jul 2024 21:27:42 +0900 Subject: [PATCH 3/4] fix: RefreshResponse --- .../common/response/status/BaseExceptionResponseStatus.java | 2 +- .../java/store/itpick/backend/controller/UserController.java | 5 ++--- .../java/store/itpick/backend/dto/auth/RefreshResponse.java | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java index 0720a93..f944d79 100644 --- a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java +++ b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java @@ -41,7 +41,7 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { /** * 5000: User 오류 */ - INVALID_USER_VALUE(5000, HttpStatus.BAD_REQUEST.value(), "회원가입 요청에서 잘못된 값이 존재합니다."), + INVALID_USER_VALUE(5000, HttpStatus.BAD_REQUEST.value(), "회원가입/로그인 요청에서 잘못된 값이 존재합니다."), DUPLICATE_EMAIL(5001, HttpStatus.BAD_REQUEST.value(), "이미 존재하는 이메일입니다."), DUPLICATE_NICKNAME(5002, HttpStatus.BAD_REQUEST.value(), "이미 존재하는 닉네임입니다."), USER_NOT_FOUND(5003, HttpStatus.BAD_REQUEST.value(), "존재하지 않는 회원입니다."), diff --git a/src/main/java/store/itpick/backend/controller/UserController.java b/src/main/java/store/itpick/backend/controller/UserController.java index 8e051e4..d694442 100644 --- a/src/main/java/store/itpick/backend/controller/UserController.java +++ b/src/main/java/store/itpick/backend/controller/UserController.java @@ -63,9 +63,8 @@ public BaseResponse login(@Validated @RequestBody LoginRequest au /** * 로그아웃 : db의 refresh 토큰을 null로 설정 */ - @PatchMapping("/{userId}/logout") - public BaseResponse logout(@PathVariable Long userId, @PreAuthorize long header_userId) { - userService.validationUserId(userId, header_userId); + @PatchMapping("/logout") + public BaseResponse logout(@PreAuthorize long userId) { userService.logout(userId); return new BaseResponse<>(null); } diff --git a/src/main/java/store/itpick/backend/dto/auth/RefreshResponse.java b/src/main/java/store/itpick/backend/dto/auth/RefreshResponse.java index 652fbcc..11024de 100644 --- a/src/main/java/store/itpick/backend/dto/auth/RefreshResponse.java +++ b/src/main/java/store/itpick/backend/dto/auth/RefreshResponse.java @@ -4,5 +4,5 @@ @AllArgsConstructor public class RefreshResponse { - private String refreshToken; + private String accessToken; } From 9512a85351f93d641bf603c3a6500bd40be8be90 Mon Sep 17 00:00:00 2001 From: sh1220 Date: Thu, 1 Aug 2024 15:34:36 +0900 Subject: [PATCH 4/4] Refactor: merge & conflict resolve --- build.gradle | 5 --- .../status/BaseExceptionResponseStatus.java | 7 +-- .../backend/controller/UserController.java | 11 ++--- .../itpick/backend/service/UserService.java | 16 +------ src/main/resources/application.yml | 43 +++++++++++++++---- 5 files changed, 40 insertions(+), 42 deletions(-) diff --git a/build.gradle b/build.gradle index 5cea5ba..a3a3e2b 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,6 @@ dependencies { implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - runtimeOnly 'com.mysql:mysql-connector-java' runtimeOnly 'com.h2database:h2' //mail @@ -60,11 +59,7 @@ dependencies { //redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' } - runtimeOnly 'com.h2database:h2' - //redis - implementation 'org.springframework.boot:spring-boot-starter-data-redis' -} tasks.named('test') { useJUnitPlatform() diff --git a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java index 0059f74..058b199 100644 --- a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java +++ b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java @@ -49,11 +49,8 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { INVALID_USER_STATUS(5005, HttpStatus.BAD_REQUEST.value(), "잘못된 회원 status 값입니다."), EMAIL_NOT_FOUND(5006, HttpStatus.BAD_REQUEST.value(), "존재하지 않는 이메일입니다."), INVALID_PASSWORD(5007, HttpStatus.BAD_REQUEST.value(), "유효하지 않는 password입니다."), - INVALID_REFRESHTOKEN(5008, HttpStatus.BAD_REQUEST.value(), "유효하지 않는 토큰입니다."); - - - INVALID_PASSWORD(5007, HttpStatus.BAD_REQUEST.value(), "유효하지 않는 password입니다."), - UNABLE_TO_SEND_EMAIL(5008,HttpStatus.BAD_REQUEST.value(),"메일을 전송할 수 없습니다."), + INVALID_REFRESHTOKEN(5008, HttpStatus.BAD_REQUEST.value(), "유효하지 않는 토큰입니다."), + UNABLE_TO_SEND_EMAIL(5012,HttpStatus.BAD_REQUEST.value(),"메일을 전송할 수 없습니다."), NO_SUCH_ALGORITHM(5009, HttpStatus.BAD_REQUEST.value(), "인증 번호 생성을 위한 알고리즘을 찾을 수 없습니다."), AUTH_CODE_IS_NOT_SAME(5010, HttpStatus.BAD_REQUEST.value(), "인증 번호가 일치하지 않습니다."), MEMBER_EXISTS(5011,HttpStatus.BAD_REQUEST.value(), "이미 존재하는 회원입니다."); diff --git a/src/main/java/store/itpick/backend/controller/UserController.java b/src/main/java/store/itpick/backend/controller/UserController.java index 8f36cac..5ed0bd7 100644 --- a/src/main/java/store/itpick/backend/controller/UserController.java +++ b/src/main/java/store/itpick/backend/controller/UserController.java @@ -19,19 +19,14 @@ import store.itpick.backend.common.response.BaseResponse; import store.itpick.backend.dto.auth.LoginRequest; import store.itpick.backend.dto.auth.LoginResponse; +import store.itpick.backend.dto.auth.RefreshRequest; +import store.itpick.backend.dto.auth.RefreshResponse; import store.itpick.backend.dto.user.user.PostUserRequest; import store.itpick.backend.dto.user.user.PostUserResponse; import store.itpick.backend.service.UserService; import store.itpick.backend.common.exception.UserException; import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_USER_VALUE; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_TOKEN; import static store.itpick.backend.util.BindingResultUtils.getErrorMessages; @Slf4j @@ -44,7 +39,7 @@ public class UserController { private final UserService userService; @PostMapping("/refresh") - public BaseResponse refresh(@Validated @RequestBody RefreshRequest refreshRequest) { + public BaseResponse refresh(@Validated @RequestBody RefreshRequest refreshRequest) { return new BaseResponse<>(userService.refresh(refreshRequest.getRefreshToken())); } diff --git a/src/main/java/store/itpick/backend/service/UserService.java b/src/main/java/store/itpick/backend/service/UserService.java index a63e853..dbece12 100644 --- a/src/main/java/store/itpick/backend/service/UserService.java +++ b/src/main/java/store/itpick/backend/service/UserService.java @@ -55,12 +55,6 @@ public class UserService { private final JwtProvider jwtProvider; - @Value("${secret.jwt-refresh-expired-in}") - private long JWT_REFRESH_EXPIRED_IN; - - - - public LoginResponse login(LoginRequest authRequest) { @@ -117,9 +111,7 @@ public PostUserResponse signUp(PostUserRequest postUserRequest) { return new PostUserResponse(user.getUserId()); } - private void validateEmail(String email) { - if (userRepository.existsByEmailAndStatusIn(email, List.of("active", "dormant"))) { - throw new UserException(DUPLICATE_EMAIL); + public RefreshResponse refresh(String refreshToken){ // 만료 & 유효성 확인, 로그아웃 확인 if(jwtProvider.isExpiredToken(refreshToken) || refreshToken == null || refreshToken.isEmpty()){ @@ -236,11 +228,5 @@ public long getUserIdByEmail(String email) { return userRepository.getUserByEmail(email).get().getUserId(); } - public void validationUserId(long userId, long header_userId) { - if (userId == header_userId) { - throw new UserException(TOKEN_MISMATCH); - } - } - } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2d40a7b..98a5adb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,17 +13,17 @@ spring: on-profile: "localDB" datasource: -# url: ${DATASOURCE_URL} -# username: ${DATASOURCE_USERNAME} -# password: ${DATASOURCE_PASSWORD} - url: jdbc:h2:tcp://localhost/~/itpick - username: sa - password: -# driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DATASOURCE_URL} + username: ${DATASOURCE_USERNAME} + password: ${DATASOURCE_PASSWORD} +# url: jdbc:h2:tcp://localhost/~/itpick +# username: sa +## password: + driver-class-name: com.mysql.cj.jdbc.Driver dbcp2: validation-query: select 1 - hikari: - driver-class-name: org.h2.Driver +# hikari: +# driver-class-name: org.h2.Driver sql: init: platform: mysql @@ -37,6 +37,26 @@ spring: h2: console: enabled: true + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + connectiontimeout: 5000 + timeout: 5000 + writetimeout: 5000 + auth-code-expiration-millis: 1800000 + data: + redis: + host: localhost + port: 6379 --- @@ -87,6 +107,10 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect + h2: + console: + enabled: true + --- @@ -118,6 +142,7 @@ spring: secret: jwt-secret-key: ${JWT_SECRET_KEY} jwt-expired-in: ${JWT_EXPIRED_IN} + jwt-refresh-expired-in: ${JWT_REFRESH_EXPIRED_IN} ---