From d08ce340f1b2aa982cccee960ac475364b360dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A0=95=ED=9B=84?= Date: Fri, 19 Jul 2024 15:56:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20cors=20=ED=95=B4=EA=B2=B0=20&=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=91=EB=8B=B5=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 2 +- .../user/application/LoginUseCase.java | 11 ++++++++ .../user/presentation/UserController.java | 28 +++++++++++++++---- .../src/main/resources/application-prod.yml | 2 +- .../security/config/CorsConfig.java | 5 ++++ .../security/util/AuthConsts.java | 13 +++++++++ 6 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 common-module/src/main/java/com/likelion/commonmodule/security/util/AuthConsts.java diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 3987bdc..0e55d1b 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,7 +2,7 @@ name: Backend CD # actions 이름 on: push: - branches: [ feat/market-info ] + branches: [ feat/login-update ] jobs: deploy: diff --git a/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java b/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java index 7ac198b..0bd43d1 100644 --- a/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java +++ b/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java @@ -37,4 +37,15 @@ private User createNewKakaoUser(final OidcDecodePayload oidcDecodePayload) { final User newUser = User.createSocialUser(oidcDecodePayload.sub(), oidcDecodePayload.nickname(), oidcDecodePayload.picture(), oidcDecodePayload.email()); return userRepository.save(newUser); } + +// @Transactional +// public LoginResponse reissueToken(String refreshToken) { +// +// final String token = TokenExtractUtils.extractToken(refreshToken); +// String reIssueAccessToken = attachAuthenticationType(jwtProvider::reIssueAccessToken, token); +// String reIssueRefreshToken = attachAuthenticationType(jwtProvider::reIssueRefreshToken, token); +// tokenDeleteService.deleteTokenByTokenValue(refreshToken); +// +// return new LoginResponse(reIssueAccessToken, reIssueRefreshToken); +// } } diff --git a/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java b/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java index 3f5dc6d..db776a4 100644 --- a/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java +++ b/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java @@ -2,6 +2,8 @@ import com.likelion.apimodule.user.application.LoginUseCase; import com.likelion.apimodule.user.dto.KakaoLoginRequest; +import com.likelion.commonmodule.exception.common.ApplicationResponse; +import com.likelion.commonmodule.security.util.AuthConsts; import com.likelion.coremodule.user.dto.LoginResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -10,10 +12,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @@ -35,7 +34,24 @@ public class UserController { } ) @Operation(summary = "카카오 로그인 API", description = "카카오 로그인 API입니다.") - public LoginResponse kakaoLogin(@Valid @RequestBody KakaoLoginRequest kakaoLoginRequest) { - return loginUseCase.kakaoLogin(kakaoLoginRequest); + public ApplicationResponse kakaoLogin(@Valid @RequestBody KakaoLoginRequest kakaoLoginRequest) { + LoginResponse response = loginUseCase.kakaoLogin(kakaoLoginRequest); + return ApplicationResponse.ok(response); + } + + @GetMapping("/reissue") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "토큰 재발급 성공", + useReturnTypeSchema = true + ) + } + ) + @Operation(summary = "토큰 재발급 API", description = "토큰 재발급 API입니다.") + public ApplicationResponse reissue(@RequestHeader(AuthConsts.REFRESH_TOKEN_HEADER) String refreshToken) { +// LoginResponse response = loginUseCase.reissueToken(refreshToken); + return ApplicationResponse.ok("미완"); } } diff --git a/api-module/src/main/resources/application-prod.yml b/api-module/src/main/resources/application-prod.yml index 8104b04..e229a55 100644 --- a/api-module/src/main/resources/application-prod.yml +++ b/api-module/src/main/resources/application-prod.yml @@ -23,7 +23,7 @@ security: secret: aGFuZXVtLWZvb2Rnby1qd3Qtc2VjcmV0LWtleQo= token: access-expiration-time: 86400 - refresh-expiration-time: 86400 + refresh-expiration-time: 604800 logging: level: diff --git a/common-module/src/main/java/com/likelion/commonmodule/security/config/CorsConfig.java b/common-module/src/main/java/com/likelion/commonmodule/security/config/CorsConfig.java index cc8ed29..1cc3357 100644 --- a/common-module/src/main/java/com/likelion/commonmodule/security/config/CorsConfig.java +++ b/common-module/src/main/java/com/likelion/commonmodule/security/config/CorsConfig.java @@ -20,10 +20,15 @@ public static CorsConfigurationSource apiConfigurationSource() { ArrayList allowedOriginPatterns = new ArrayList<>(); allowedOriginPatterns.add("http://localhost:8080"); allowedOriginPatterns.add("https://localhost:8080"); + allowedOriginPatterns.add("http://localhost:3000"); allowedOriginPatterns.add("https://localhost:3000"); + allowedOriginPatterns.add("https://syluv.link"); + allowedOriginPatterns.add("https://www.syluv.store"); + allowedOriginPatterns.add("https://syluv.store"); + configuration.setAllowedOrigins(allowedOriginPatterns); configuration.setAllowedMethods(List.of("HEAD", "POST", "GET", "DELETE", "PUT", "OPTIONS", "PATCH")); configuration.setAllowedHeaders(List.of("*")); diff --git a/common-module/src/main/java/com/likelion/commonmodule/security/util/AuthConsts.java b/common-module/src/main/java/com/likelion/commonmodule/security/util/AuthConsts.java new file mode 100644 index 0000000..a4dd65a --- /dev/null +++ b/common-module/src/main/java/com/likelion/commonmodule/security/util/AuthConsts.java @@ -0,0 +1,13 @@ +package com.likelion.commonmodule.security.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class AuthConsts { + public static final String AUTHENTICATION_TYPE= "Bearer"; + public static final String AUTHORIZATION = "Authorization"; + public static final String EMPTY_HEADER = null; + public static final String REFRESH_TOKEN_HEADER = "RefreshToken"; + public static final String AUTHENTICATION_TYPE_PREFIX = AUTHENTICATION_TYPE+" "; +} From b30ab279c8882e52ca14ae896c052060433332ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A0=95=ED=9B=84?= Date: Sun, 21 Jul 2024 21:26:19 +0900 Subject: [PATCH 2/3] feat: reissue token --- .../apimodule/security/util/JwtUtil.java | 48 +++++++++++++++---- .../user/application/LoginUseCase.java | 19 +++----- .../likelion/apimodule/user/dto/UserInfo.java | 5 ++ .../user/presentation/UserController.java | 2 +- .../user/exception/UserErrorCode.java | 4 +- 5 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 api-module/src/main/java/com/likelion/apimodule/user/dto/UserInfo.java diff --git a/api-module/src/main/java/com/likelion/apimodule/security/util/JwtUtil.java b/api-module/src/main/java/com/likelion/apimodule/security/util/JwtUtil.java index beb8820..e133524 100644 --- a/api-module/src/main/java/com/likelion/apimodule/security/util/JwtUtil.java +++ b/api-module/src/main/java/com/likelion/apimodule/security/util/JwtUtil.java @@ -2,10 +2,8 @@ import com.likelion.commonmodule.exception.jwt.SecurityCustomException; import com.likelion.commonmodule.redis.util.RedisUtil; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; +import com.likelion.coremodule.user.dto.LoginResponse; +import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; @@ -19,6 +17,7 @@ import java.util.concurrent.TimeUnit; import static com.likelion.commonmodule.exception.jwt.SecurityErrorCode.INVALID_TOKEN; +import static com.likelion.commonmodule.exception.jwt.SecurityErrorCode.TOKEN_EXPIRED; @Component @Slf4j @@ -43,7 +42,7 @@ public JwtUtil( redisUtil = redis; } - public String createJwtAccessToken(String nickname, String subId) { + public String createJwtAccessToken(String email, String subId) { Instant issuedAt = Instant.now(); Instant expiration = issuedAt.plusMillis(accessExpMs); @@ -51,14 +50,14 @@ public String createJwtAccessToken(String nickname, String subId) { .setHeaderParam("alg", "HS256") .setHeaderParam("typ", "JWT") .setSubject(subId) - .claim("nickname", nickname) + .claim("email", email) .setIssuedAt(Date.from(issuedAt)) .setExpiration(Date.from(expiration)) .signWith(secretKey) .compact(); } - public String createJwtRefreshToken(String nickname, String subId) { + public String createJwtRefreshToken(String email, String subId) { Instant issuedAt = Instant.now(); Instant expiration = issuedAt.plusMillis(refreshExpMs); @@ -66,14 +65,14 @@ public String createJwtRefreshToken(String nickname, String subId) { .setHeaderParam("alg", "HS256") .setHeaderParam("typ", "JWT") .setSubject(subId) - .claim("nickname", nickname) + .claim("email", email) .setIssuedAt(Date.from(issuedAt)) .setExpiration(Date.from(expiration)) .signWith(secretKey) .compact(); redisUtil.saveAsValue( - subId + "_refresh_token", + email + "_refresh_token", refreshToken, refreshExpMs, TimeUnit.MILLISECONDS @@ -94,6 +93,28 @@ public String resolveAccessToken(HttpServletRequest request) { return authorization.split(" ")[1]; } + public LoginResponse reissueToken(String refreshToken) { + try { + validateRefreshToken(refreshToken); + log.info("[*] Valid RefreshToken"); + + // 삭제 로직 + String email = getEmail(refreshToken); + redisUtil.delete(email + "_refresh_token"); + + String subId = getSubjectFromToken(refreshToken); + + return new LoginResponse( + createJwtAccessToken(email, subId), + createJwtRefreshToken(email, subId) + ); + } catch (IllegalArgumentException iae) { + throw new SecurityCustomException(INVALID_TOKEN, iae); + } catch (ExpiredJwtException eje) { + throw new SecurityCustomException(TOKEN_EXPIRED, eje); + } + } + public void validateRefreshToken(String refreshToken) { // refreshToken 유효성 검증 String email = getEmail(refreshToken); @@ -105,6 +126,15 @@ public void validateRefreshToken(String refreshToken) { } } + public String getSubjectFromToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.getSubject(); + } + public Long getId(String token) { return Long.parseLong(getClaims(token).getSubject()); } diff --git a/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java b/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java index 0bd43d1..41d502e 100644 --- a/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java +++ b/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java @@ -28,8 +28,8 @@ public LoginResponse kakaoLogin(final KakaoLoginRequest kakaoLoginRequest) { final User user = userRepository.findBySubId(oidcDecodePayload.sub()) .orElseGet(() -> createNewKakaoUser(oidcDecodePayload)); - return new LoginResponse(jwtUtil.createJwtAccessToken(oidcDecodePayload.nickname(), oidcDecodePayload.sub()), - jwtUtil.createJwtRefreshToken(oidcDecodePayload.nickname(), oidcDecodePayload.sub())); + return new LoginResponse(jwtUtil.createJwtAccessToken(oidcDecodePayload.email(), oidcDecodePayload.sub()), + jwtUtil.createJwtRefreshToken(oidcDecodePayload.email(), oidcDecodePayload.sub())); } private User createNewKakaoUser(final OidcDecodePayload oidcDecodePayload) { @@ -38,14 +38,9 @@ private User createNewKakaoUser(final OidcDecodePayload oidcDecodePayload) { return userRepository.save(newUser); } -// @Transactional -// public LoginResponse reissueToken(String refreshToken) { -// -// final String token = TokenExtractUtils.extractToken(refreshToken); -// String reIssueAccessToken = attachAuthenticationType(jwtProvider::reIssueAccessToken, token); -// String reIssueRefreshToken = attachAuthenticationType(jwtProvider::reIssueRefreshToken, token); -// tokenDeleteService.deleteTokenByTokenValue(refreshToken); -// -// return new LoginResponse(reIssueAccessToken, reIssueRefreshToken); -// } + @Transactional + public LoginResponse reissueToken(String refreshToken) { + + return jwtUtil.reissueToken(refreshToken); + } } diff --git a/api-module/src/main/java/com/likelion/apimodule/user/dto/UserInfo.java b/api-module/src/main/java/com/likelion/apimodule/user/dto/UserInfo.java new file mode 100644 index 0000000..0dc9f9c --- /dev/null +++ b/api-module/src/main/java/com/likelion/apimodule/user/dto/UserInfo.java @@ -0,0 +1,5 @@ +package com.likelion.apimodule.user.dto; + +public record UserInfo(String email, + String subId) { +} diff --git a/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java b/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java index db776a4..465a2de 100644 --- a/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java +++ b/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java @@ -51,7 +51,7 @@ public ApplicationResponse kakaoLogin(@Valid @RequestBody KakaoLo ) @Operation(summary = "토큰 재발급 API", description = "토큰 재발급 API입니다.") public ApplicationResponse reissue(@RequestHeader(AuthConsts.REFRESH_TOKEN_HEADER) String refreshToken) { -// LoginResponse response = loginUseCase.reissueToken(refreshToken); + LoginResponse response = loginUseCase.reissueToken(refreshToken); return ApplicationResponse.ok("미완"); } } diff --git a/core-module/src/main/java/com/likelion/coremodule/user/exception/UserErrorCode.java b/core-module/src/main/java/com/likelion/coremodule/user/exception/UserErrorCode.java index 69360a5..8b7b0a3 100644 --- a/core-module/src/main/java/com/likelion/coremodule/user/exception/UserErrorCode.java +++ b/core-module/src/main/java/com/likelion/coremodule/user/exception/UserErrorCode.java @@ -10,7 +10,9 @@ @AllArgsConstructor public enum UserErrorCode implements BaseErrorCode { - No_USER_INFO(HttpStatus.BAD_REQUEST, "2000", "사용자 정보가 존재하지 않습니다."); + No_USER_INFO(HttpStatus.BAD_REQUEST, "2000", "사용자 정보가 존재하지 않습니다."), + EMPTY_AUTHORIZATION_HEADER(HttpStatus.BAD_REQUEST, "2000", "토큰 정보가 비어 있습니다."), + INVALID_AUTHORIZATION_TYPE(HttpStatus.BAD_REQUEST, "2000", "타입이 유효하지 않습니다."); private final HttpStatus httpStatus; private final String code; From ce7a695e58edffa3674703a2ccf92ac64cbcc5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A0=95=ED=9B=84?= Date: Sun, 21 Jul 2024 21:45:46 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20logout=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=A7=88=EB=AC=B4=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apimodule/security/util/JwtUtil.java | 7 ++++++ .../user/application/LoginUseCase.java | 7 ++++++ .../user/presentation/UserController.java | 22 +++++++++++++++++-- .../user/repository/UserRepository.java | 4 ++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/api-module/src/main/java/com/likelion/apimodule/security/util/JwtUtil.java b/api-module/src/main/java/com/likelion/apimodule/security/util/JwtUtil.java index e133524..27a4202 100644 --- a/api-module/src/main/java/com/likelion/apimodule/security/util/JwtUtil.java +++ b/api-module/src/main/java/com/likelion/apimodule/security/util/JwtUtil.java @@ -115,6 +115,13 @@ public LoginResponse reissueToken(String refreshToken) { } } + public void deleteToken(String refreshToken) { + + // 삭제 로직 + String email = getEmail(refreshToken); + redisUtil.delete(email + "_refresh_token"); + } + public void validateRefreshToken(String refreshToken) { // refreshToken 유효성 검증 String email = getEmail(refreshToken); diff --git a/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java b/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java index 41d502e..fd67d3f 100644 --- a/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java +++ b/api-module/src/main/java/com/likelion/apimodule/user/application/LoginUseCase.java @@ -43,4 +43,11 @@ public LoginResponse reissueToken(String refreshToken) { return jwtUtil.reissueToken(refreshToken); } + + @Transactional + public void logout(String refreshToken, String name) { + + jwtUtil.deleteToken(refreshToken); + if (userRepository.findByName(name).isPresent()) userRepository.deleteByName(name); + } } diff --git a/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java b/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java index 465a2de..4bda35b 100644 --- a/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java +++ b/api-module/src/main/java/com/likelion/apimodule/user/presentation/UserController.java @@ -50,8 +50,26 @@ public ApplicationResponse kakaoLogin(@Valid @RequestBody KakaoLo } ) @Operation(summary = "토큰 재발급 API", description = "토큰 재발급 API입니다.") - public ApplicationResponse reissue(@RequestHeader(AuthConsts.REFRESH_TOKEN_HEADER) String refreshToken) { + public ApplicationResponse reissue(@RequestHeader(AuthConsts.REFRESH_TOKEN_HEADER) String refreshToken) { LoginResponse response = loginUseCase.reissueToken(refreshToken); - return ApplicationResponse.ok("미완"); + return ApplicationResponse.ok(response); + } + + @DeleteMapping("/logout") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "로그아웃 성공", + useReturnTypeSchema = true + ) + } + ) + @Operation(summary = "로그아웃 API", description = "로그아웃 API입니다.") + public ApplicationResponse logout(@RequestHeader(AuthConsts.REFRESH_TOKEN_HEADER) String refreshToken, + @RequestParam String name) { + + loginUseCase.logout(refreshToken, name); + return ApplicationResponse.ok("로그아웃 되었습니다."); } } diff --git a/core-module/src/main/java/com/likelion/coremodule/user/repository/UserRepository.java b/core-module/src/main/java/com/likelion/coremodule/user/repository/UserRepository.java index 14d731b..8ce0d76 100644 --- a/core-module/src/main/java/com/likelion/coremodule/user/repository/UserRepository.java +++ b/core-module/src/main/java/com/likelion/coremodule/user/repository/UserRepository.java @@ -10,4 +10,8 @@ public interface UserRepository extends JpaRepository { Optional findBySubId(String subId); Optional findByEmail(String email); + + Optional findByName(String name); + + void deleteByName(String name); }