Skip to content

Commit

Permalink
Merge branch 'dev' into choer(#35)-redis-setting
Browse files Browse the repository at this point in the history
  • Loading branch information
jusung-c authored Jan 20, 2024
2 parents dd95873 + 199c59c commit 9e1024e
Show file tree
Hide file tree
Showing 29 changed files with 1,355 additions and 35 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/CI_dev_be_pull_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Spring Boot & Gradle CI Jobs (With. dev branches pull_request)

on:
pull_request:
branches: [ dev ]

jobs:
build:
# 실행 환경 (Git Runners 개인 서버)
runs-on: self-hosted

steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'

# application.yml 파일 설정
- name: resources 폴더 생성
run: |
mkdir -p ./backend/src/main/resources
- name: yml 파일 생성
run: |
echo "${{ secrets.APPLICATION_DEFAULT }}" > ./backend/src/main/resources/application.yml
echo "${{ secrets.APPLICATION_LOCAL }}" > ./backend/src/main/resources/application-local.yml
echo "${{ secrets.APPLICATION_TEST }}" > ./backend/src/main/resources/application-test.yml
# gradlew를 실행시키기 위해 권한 부여
- name: Gradlew에게 실행권한 부여
run: chmod +x ./backend/gradlew

# 멀티모듈 빌드하기
- name: 멀티모듈 전체 빌드
run: |
cd ./backend
./gradlew clean build -x test
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.backend.auth.api.controller.auth;

import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse;
import com.example.backend.auth.api.controller.auth.response.AuthLoginResponse;
import com.example.backend.auth.api.service.auth.AuthService;
import com.example.backend.auth.api.service.oauth.OAuthService;
import com.example.backend.common.response.JsonResult;
Expand Down Expand Up @@ -33,21 +34,17 @@ public JsonResult<List<AuthLoginPageResponse>> loginPage() {
}

@GetMapping("/{platformType}/login")
public JsonResult<String> login(
public JsonResult<AuthLoginResponse> login(
@PathVariable("platformType") UserPlatformType platformType,
@RequestParam("code") String code,
@RequestParam("state") String loginState) {
/*
TODO : state 값이 유효한지 검증하는 로직이 필요합니다.
*/

/*
TODO : 로그인 API는 최종적으로 JWT 토큰이 담긴 AuthLoginResponse DTO를 반환해줘야 합니다.
*/

authService.login(platformType, code, loginState);
AuthLoginResponse loginResponse = authService.login(platformType, code, loginState);

return JsonResult.successOf("로그인에 성공하였습니다.");
return JsonResult.successOf(loginResponse);
}

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,104 @@
package com.example.backend.auth.api.service.auth;

import com.example.backend.auth.api.controller.auth.response.AuthLoginResponse;
import com.example.backend.auth.api.service.jwt.JwtService;
import com.example.backend.auth.api.service.jwt.JwtToken;
import com.example.backend.auth.api.service.oauth.OAuthService;
import com.example.backend.auth.api.service.oauth.response.OAuthResponse;
import com.example.backend.domain.define.user.User;
import com.example.backend.domain.define.user.constant.UserPlatformType;
import com.example.backend.domain.define.user.constant.UserRole;
import com.example.backend.domain.define.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import java.util.HashMap;
import java.util.List;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AuthService {
private static final String ROLE_CLAIM = "role";
private static final String NAME_CLAIM = "name";
private static final String PROFILE_IMAGE_CLAIM = "profileImageUrl";

private final UserRepository userRepository;
private final OAuthService oAuthService;
private final JwtService jwtService;

@Transactional
public AuthLoginResponse login(UserPlatformType platformType, String code, String state) {
OAuthResponse loginResponse = oAuthService.login(platformType, code, state);
log.info(">>>> {}님이 로그인하셨습니다.", loginResponse.getName());
String name = loginResponse.getName();
String profileImageUrl = loginResponse.getProfileImageUrl();
String email = loginResponse.getEmail();

log.info(">>>> [ {}님이 로그인하셨습니다 ] <<<<", name);

/*
TODO : OAuth 로그인 인증을 마쳤으니 우리 애플리케이션의 DB에도 존재하는 사용자인지 확인해야 합니다.
* 회원이 아닐 경우, 즉 회원가입이 필요한 신규 사용자의 경우 OAuthResponse를 바탕으로 DB에 등록해줍니다.
* OAuth 로그인 인증을 마쳤으니 우리 애플리케이션의 DB에도 존재하는 사용자인지 확인한다.
* 회원이 아닐 경우, 즉 회원가입이 필요한 신규 사용자의 경우 OAuthResponse를 바탕으로 DB에 등록해준다.
*/
User findUser = userRepository.findByEmail(email)
.orElseGet(() -> {
User saveUser = User.builder()
.platformId(loginResponse.getPlatformId())
.platformType(loginResponse.getPlatformType())
.role(UserRole.UNAUTH)
.name(name)
.email(email)
.profileImageUrl(profileImageUrl)
.build();

log.info(">>>> [ UNAUTH 권한으로 사용자를 DB에 등록합니다. 이후 회원가입이 필요합니다 ] <<<<");
return userRepository.save(saveUser);
});

// 기존 사용자의 경우 OAuth 사용자 정보(이름, 사진)가 변경되었으면 업데이트해준다.
findUser.updateProfile(name, profileImageUrl);

/*
TODO : 기존 사용자의 경우 OAuth 사용자 정보가 변경되었을 수 있으므로 변경 사항을 업데이트해주는 로직이 필요합니다.
DB에 저장된 사용자 정보를 기반으로 JWT 토큰을 발급
* JWT 토큰을 요청시에 담아 보내면 JWT 토큰 인증 필터에서 Security Context에 인증된 사용자로 등록
TODO : JWT 재발급을 위한 Refresh 토큰은 Redis에서 관리할 예정입니다.
*/
JwtToken jwtToken = generateJwtToken(findUser);

// JWT 토큰과 권한 정보를 담아 반환
return AuthLoginResponse.builder()
.accessToken(jwtToken.getAccessToken())
.refreshToken(jwtToken.getRefreshToken())
.role(findUser.getRole())
.build();
}

private JwtToken generateJwtToken(User user) {
// JWT 토큰 생성을 위한 claims 생성
HashMap<String, String> claims = new HashMap<>();
claims.put(ROLE_CLAIM, user.getRole().name());
claims.put(NAME_CLAIM, user.getName());
claims.put(PROFILE_IMAGE_CLAIM, user.getProfileImageUrl());

// Access Token 생성
final String jwtAccessToken = jwtService.generateAccessToken(claims, user);
// 임시로 Refresh Token 생성
final String jwtRefreshToken = "jwt-refresh-token";
log.info(">>>> [ 사용자 {}님의 JWT 토큰이 발급되었습니다 ] <<<<", user.getName());

/*
TODO : DB에 저장된 사용자 정보를 기반으로 JWT 토큰을 발급해주는 로직이 필요합니다.
* JWT 토큰을 요청시에 담아 인증된 사용자임을 알립니다.
* JWT 토큰 인증 필터에서 Security에 인증된 사용자로 등록될 것입니다.
* JWT 재발급을 위한 Refresh 토큰은 Redis에서 관리할 예정입니다.
TODO : Refresh Token 생성, 저장 로직이 필요합니다.
* Redis DB를 연동해 Refresh Token을 저장, 관리할 예정입니다.
*/

// TODO : JWT 토큰을 담아 최종적으로 AuthLoginResponse를 반환해줍니다.
return null;
return JwtToken.builder()
.accessToken(jwtAccessToken)
.refreshToken(jwtRefreshToken)
.build();
}

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.backend.auth.api.service.jwt;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class JwtToken {
private String accessToken;
private String refreshToken;

@Builder
public JwtToken(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse;
import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter;
import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter;

import com.example.backend.auth.api.service.oauth.adapter.kakao.KakaoAdapter;
import com.example.backend.auth.api.service.oauth.builder.OAuthURLBuilder;
import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder;
import com.example.backend.auth.api.service.oauth.builder.kakao.KakaoURLBuilder;

import com.example.backend.auth.api.service.oauth.adapter.google.GoogleAdapter;
import com.example.backend.auth.api.service.oauth.builder.google.GoogleURLBuilder;

import com.example.backend.auth.api.service.oauth.response.OAuthResponse;
import com.example.backend.domain.define.user.constant.UserPlatformType;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -16,6 +23,8 @@
import java.util.stream.Collectors;

import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB;
import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO;
import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE;

@Slf4j
@Service
Expand All @@ -24,16 +33,30 @@ public class OAuthService {
private Map<UserPlatformType, OAuthFactory> adapterMap;

// 플랫폼별 Adapter, URLBuilder 등록
public OAuthService(GithubAdapter githubAdapter, GithubURLBuilder githubURLBuilder) {


public OAuthService(GithubAdapter githubAdapter, GithubURLBuilder githubURLBuilder, GoogleAdapter googleAdapter, GoogleURLBuilder googleURLBuilder , KakaoAdapter kakaoAdapter, KakaoURLBuilder kakaoURLBuilder) {
this.adapterMap = new HashMap<>() {{
// 깃허브 플랫폼 추가
put(GITHUB, OAuthFactory.builder()
.oAuthAdapter(githubAdapter)
.oAuthURLBuilder(githubURLBuilder)
.build());

// 카카오 플랫폼 추가
put(KAKAO, OAuthFactory.builder()
.oAuthAdapter(kakaoAdapter)
.oAuthURLBuilder(kakaoURLBuilder)
.build());

// 구글 플랫폼 추가
put(GOOGLE, OAuthFactory.builder()
.oAuthAdapter(googleAdapter)
.oAuthURLBuilder(googleURLBuilder)
.build());

}};
}

// OAuth 2.0 로그인 페이지 생성
public List<AuthLoginPageResponse> loginPage(String state) {
// 지원하는 모든 플랫폼의 로그인 페이지를 생성해 반환한다.
Expand Down Expand Up @@ -62,17 +85,16 @@ public OAuthResponse login(UserPlatformType platformType, String code, String st

OAuthURLBuilder urlBuilder = factory.getOAuthURLBuilder();
OAuthAdapter adapter = factory.getOAuthAdapter();
log.info(">>>> {} Login Start", platformType);
log.info(">>>> [ {} Login Start ] <<<<", platformType);

// code, state를 이용해 Access Token 요청 URL 생성
String tokenUrl = urlBuilder.token(code, state);

// Access Token 획득
String accessToken = adapter.getToken(tokenUrl);

// 사용자 프로필 조회
OAuthResponse userInfo = adapter.getProfile(accessToken);
log.info(">>>> {} Login Success", platformType);
log.info(">>>> [ {} Login Success ] <<<<", platformType);

return userInfo;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public String getToken(String tokenURL) {

return token.getAccess_token();
} catch (RuntimeException e) {
log.error(">>>> [ Github Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText());
log.error(">>>> [ Github Oauth 인증 에러 발생: {} ] <<<<", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText());
throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL);
}
}
Expand All @@ -53,7 +53,7 @@ public OAuthResponse getProfile(String accessToken) {
.profileImageUrl(profile.getAvatar_url())
.build();
} catch (RuntimeException e) {
log.error(">>>> [ Github Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText());
log.error(">>>> [ Github Oauth 인증 에러 발생: {} ] <<<<", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText());
throw new OAuthException(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.backend.auth.api.service.oauth.adapter.google;

import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter;
import com.example.backend.auth.api.service.oauth.response.OAuthResponse;
import com.example.backend.common.exception.ExceptionMessage;
import com.example.backend.common.exception.oauth.OAuthException;
import com.example.backend.external.clients.oauth.google.GoogleProfileClients;
import com.example.backend.external.clients.oauth.google.GoogleTokenClients;
import com.example.backend.external.clients.oauth.google.response.GoogleProfileResponse;
import com.example.backend.external.clients.oauth.google.response.GoogleTokenResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.net.URI;
import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE;

@Slf4j
@Component
@RequiredArgsConstructor
public class GoogleAdapter implements OAuthAdapter {

private final GoogleTokenClients googleTokenClients;
private final GoogleProfileClients googleProfileClients;

@Override
public String getToken(String tokenURL) {
try {
GoogleTokenResponse token = googleTokenClients.getToken(URI.create(tokenURL));
// URL로 액세스 토큰을 요청

// 만약 token이 null일 경우 예외처리
if (token.getAccess_token() == null) {
throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL);
}
return token.getAccess_token();
} catch (RuntimeException e) {
log.error(">>>> [ Google Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText());
throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL);
}
}

@Override
public OAuthResponse getProfile(String accessToken) {
try {
GoogleProfileResponse profile = googleProfileClients.getProfile("Bearer " + accessToken);

// 액세스 토큰을 사용하여 프로필 정보 요청
return OAuthResponse.builder()
.platformId(profile.getSub())
.platformType(GOOGLE)
.name(profile.getName())
.profileImageUrl(profile.getPicture())
.build();
} catch (RuntimeException e) {
log.error(">>>> [ Google Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText());
throw new OAuthException(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN);
}
}
}
Loading

0 comments on commit 9e1024e

Please sign in to comment.