Skip to content

Commit

Permalink
[ARV-154] OAuth2 로그인 로직 분리 (#130)
Browse files Browse the repository at this point in the history
* [ARV-154] fix: oauth2 전면 재구현

* [ARV-154] refactor: ApiTestSupport controller 변경

* [ARV-154] fix: GUEST role 삭제 및 GUEST 로그인 삭제

* [ARV-154] fix: 회원가입 GUEST 대신 ANONYMOUS 접근 가능하도록 수정

* [ARV-154] fix: jwt 토큰 검증 로직 개선

- refresh token 만료 시 access token 상관 없이 예외 발생

* [ARV-154] fix: refreshToken 쿠키 이름 스펠링 변경

* [ARV-154] refactor: 추가 및 수정 메서드 매개변수 final화

* [ARV-154] feat: 토큰 재발급 API 추가

- 카카오 로그인 검증 시 user가 아니라면 cookie 등록 안되게 수정

* [ARV-154] fix: redirect url API 제거 및 환경변수 authorization-uri 삭제

* [ARV-154] refactor: cookie 도메인 이름 환경변수화

* [ARV-154] fix: @AuthMember import 패키지 변경

* [ARV-154] refactor: kakao oauuth2 dto record화

* [ARV-154] refactor: AuthService kakaoLogin()에서 else문 제거
  • Loading branch information
sangcci authored Dec 18, 2024
1 parent 8a3eafe commit fa90ad6
Show file tree
Hide file tree
Showing 66 changed files with 645 additions and 613 deletions.
107 changes: 107 additions & 0 deletions src/main/java/com/backend/allreva/auth/application/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.backend.allreva.auth.application;

import com.backend.allreva.auth.application.dto.LoginResponse;
import com.backend.allreva.auth.application.dto.ReissueRequest;
import com.backend.allreva.auth.application.dto.ReissueResponse;
import com.backend.allreva.auth.application.dto.UserInfo;
import com.backend.allreva.auth.exception.code.InvalidJwtTokenException;
import com.backend.allreva.auth.exception.code.TokenNotFoundException;
import com.backend.allreva.common.model.Email;
import com.backend.allreva.member.command.domain.Member;
import com.backend.allreva.member.command.domain.MemberRepository;
import com.backend.allreva.member.command.domain.value.LoginProvider;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class AuthService {

private final OAuth2LoginService oAuth2LoginService;
private final JwtService jwtService;
private final MemberRepository memberRepository;

/**
* 카카오 로그인을 검증합니다.
* @param authorizationCode 인가 코드
* @return 로그인 응답
*/
public LoginResponse kakaoLogin(final String authorizationCode) {
UserInfo userInfo = oAuth2LoginService.getUserInfo(authorizationCode);

// 회원 존재 확인
Email emailVO = Email.builder()
.email(userInfo.email())
.build();
LoginProvider loginProvider = userInfo.loginProvider();
Optional<Member> memberOptional = memberRepository.findByEmailAndLoginProvider(emailVO, loginProvider);

if (memberOptional.isPresent()) {
return getMemberInfo(memberOptional.get());
}
return getTemporaryMemberInfo(userInfo);

}

private LoginResponse getTemporaryMemberInfo(final UserInfo userInfo) {
return LoginResponse.builder()
.isUser(false)
.email(userInfo.email())
.nickname(userInfo.nickname())
.profileImageUrl(userInfo.profileImageUrl())
.build();
}

private LoginResponse getMemberInfo(final Member member) {
// token 생성
Long memberId = member.getId();
String accessToken = jwtService.generateAccessToken(String.valueOf(memberId));
String refreshToken = jwtService.generateRefreshToken(String.valueOf(memberId));

// redis에 RefreshToken 저장
jwtService.updateRefreshToken(refreshToken, memberId);

return LoginResponse.builder()
.isUser(true)
.accessToken(accessToken)
.refreshToken(refreshToken)
.email(member.getEmail().getEmail())
.nickname(member.getMemberInfo().getNickname())
.profileImageUrl(member.getMemberInfo().getProfileImageUrl())
.build();
}

/**
* Access Token을 재발급합니다.
* @param reissueRequest Refresh Token
* @Return 재발급된 Access Token 및 Refresh Token
*/
public ReissueResponse reissueAccessToken(final ReissueRequest reissueRequest) {
String refreshToken = reissueRequest.refreshToken();

// refresh token 검증
boolean isRefreshTokenValid = jwtService.validateToken(refreshToken);
if (!isRefreshTokenValid) {
throw new InvalidJwtTokenException();
}
if (!jwtService.isRefreshTokenExist(refreshToken)) {
throw new TokenNotFoundException();
}

// access token 재발급
String memberId = jwtService.extractMemberId(refreshToken);
String generatedAccessToken = jwtService.generateAccessToken(memberId);

// token rotate
String generateRefreshToken = jwtService.generateRefreshToken(memberId);
jwtService.updateRefreshToken(generateRefreshToken, Long.valueOf(memberId));

return ReissueResponse.builder()
.accessToken(generatedAccessToken)
.refreshToken(generateRefreshToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.backend.allreva.auth.application;

import com.backend.allreva.common.util.CookieUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class CookieService {

@Value("${jwt.refresh.expiration}")
private int refreshTime;
@Value("${url.name}")
private String domainName;

public void addRefreshTokenCookie(
final HttpServletResponse response,
final String refreshToken
) {
CookieUtils.addCookie(
response,
domainName,
"refreshToken",
refreshToken,
refreshTime
);
}
}
31 changes: 20 additions & 11 deletions src/main/java/com/backend/allreva/auth/application/JwtService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@
public class JwtService {

private final SecretKey secretKey;
private final int ACCESS_TIME;
private final int REFRESH_TIME;
private final int accessTime;
private final int refreshTime;

private final RefreshTokenRepository refreshTokenRepository;

public JwtService(
@Value("${jwt.secret-key}") final String secretKey,
@Value("${jwt.access.expiration}") final int ACCESS_TIME,
@Value("${jwt.refresh.expiration}") final int REFRESH_TIME,
@Value("${jwt.access.expiration}") final int accessTime,
@Value("${jwt.refresh.expiration}") final int refreshTime,
final RefreshTokenRepository refreshTokenRepository
) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
this.ACCESS_TIME = ACCESS_TIME;
this.REFRESH_TIME = REFRESH_TIME;
this.accessTime = accessTime;
this.refreshTime = refreshTime;
this.refreshTokenRepository = refreshTokenRepository;
}

Expand Down Expand Up @@ -130,7 +130,7 @@ public boolean validateToken(final String token) {
*/
public String generateAccessToken(final String subject) {
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + ACCESS_TIME);
Date expireDate = new Date(currentDate.getTime() + accessTime);

return Jwts.builder()
.subject(subject)
Expand All @@ -147,7 +147,7 @@ public String generateAccessToken(final String subject) {
*/
public String generateRefreshToken(final String subject) {
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + REFRESH_TIME);
Date expireDate = new Date(currentDate.getTime() + refreshTime);

return Jwts.builder()
.subject(subject)
Expand All @@ -164,15 +164,24 @@ public String generateRefreshToken(final String subject) {
*/
public void updateRefreshToken(
final String generatedRefreshToken,
final String memberId
final Long memberId
) {
refreshTokenRepository.findRefreshTokenByMemberId(Long.valueOf(memberId))
refreshTokenRepository.findRefreshTokenByMemberId(memberId)
.ifPresent(refreshTokenRepository::delete);

RefreshToken refreshTokenEntity = RefreshToken.builder()
.token(generatedRefreshToken)
.memberId(Long.valueOf(memberId))
.memberId(memberId)
.build();
refreshTokenRepository.save(refreshTokenEntity);
}

/**
* Refresh Token이 Redis에 존재하는지 확인합니다.
* @param refreshToken Cookie에 저장되있던 Refresh Token
* @return Refresh Token이 존재하면 true, 그렇지 않으면 false
*/
public boolean isRefreshTokenExist(final String refreshToken) {
return refreshTokenRepository.existsRefreshTokenByToken(refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.backend.allreva.auth.application;

import com.backend.allreva.auth.application.dto.UserInfo;

public interface OAuth2LoginService {

UserInfo getUserInfo(String authorizationCode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.backend.allreva.auth.application.dto;

import lombok.Builder;

@Builder
public record LoginResponse(
boolean isUser,
String email,
String nickname,
String profileImageUrl,
String accessToken,
String refreshToken
) {

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.backend.allreva.auth.application.dto;

import lombok.Builder;

@Builder
public record ReissueRequest(
String refreshToken
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.backend.allreva.auth.application.dto;

import lombok.Builder;

@Builder
public record ReissueResponse(
String accessToken,
String refreshToken
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.backend.allreva.auth.application.dto;

import com.backend.allreva.member.command.domain.value.LoginProvider;
import lombok.Builder;

@Builder
public record UserInfo(
LoginProvider loginProvider,
String providerId,
String nickname,
String email,
String profileImageUrl
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findRefreshTokenByMemberId(Long memberId);
boolean existsRefreshTokenByToken(String token);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public enum JwtErrorCode implements ErrorCodeInterface {
INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "INVALID_JWT_TOKEN", "유효하지 않은 토큰입니다."),
INVALID_JWT_SIGNATURE(HttpStatus.UNAUTHORIZED.value(), "INVALID_JWT_SIGNATURE", "유효하지 않은 서명입니다."),
EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "EXPIRED_JWT_TOKEN", "토큰이 만료되었습니다."),
JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED.value(), "JWT_TOKEN_NOT_FOUND", "토큰이 존재하지 않습니다."),
JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED.value(), "TOKEN_NOT_FOUND", "토큰이 존재하지 않습니다."),
;

private final Integer status;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.backend.allreva.auth.oauth2.exception;
package com.backend.allreva.auth.exception.code;

import org.springframework.http.HttpStatus;

Expand All @@ -10,7 +10,7 @@
@RequiredArgsConstructor
public enum OAuth2ErrorCode implements ErrorCodeInterface {

UNSUPPORTED_PROVIDER(HttpStatus.BAD_REQUEST.value(), "UNSUPPORTED_PROVIDER", "지원하지 않는 provider 입니다.")
UNSUPPORTED_PROVIDER(HttpStatus.BAD_REQUEST.value(), "UNSUPPORTED_PROVIDER", "지원하지 않는 provider 입니다."),
;

private final Integer status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import com.backend.allreva.common.exception.CustomException;

public class JwtTokenNotFoundException extends CustomException {
public class TokenNotFoundException extends CustomException {

public JwtTokenNotFoundException() {
public TokenNotFoundException() {
super(JwtErrorCode.JWT_TOKEN_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.backend.allreva.auth.oauth2.exception;
package com.backend.allreva.auth.exception.code;

import com.backend.allreva.common.exception.CustomException;

Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/backend/allreva/auth/oauth2/KakaoAuthClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.backend.allreva.auth.oauth2;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "kakaoAuthClient", url = "${oauth2.kakao.auth-url}")
public interface KakaoAuthClient {

@PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
KakaoToken getToken(
@RequestParam("client_id") String clientId,
@RequestParam("redirect_uri") String redirectUri,
@RequestParam("code") String code,
@RequestParam("grant_type") String grantType,
@RequestParam("client_secret") String clientSecret
);
}
Loading

0 comments on commit fa90ad6

Please sign in to comment.