Skip to content

Commit

Permalink
Merge pull request #33 from onetime-with-members/feature/#32/social-l…
Browse files Browse the repository at this point in the history
…ogin

[feat] : 소셜 로그인 기능을 구현한다
  • Loading branch information
bbbang105 authored Sep 18, 2024
2 parents dc05399 + e613f8f commit 67fd96a
Show file tree
Hide file tree
Showing 27 changed files with 990 additions and 2 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,20 @@ dependencies {
// DB
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// OAuth 2.0
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.2'
}

tasks.named('test') {
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/side/onetime/auth/constant/Provider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package side.onetime.auth.constant;

public enum Provider {
GOOGLE_PROVIDER("google"),
KAKAO_PROVIDER("kakao"),
NAVER_PROVIDER("naver");

private final String provider;

Provider(String provider) {
this.provider = provider;
}

public String getProvider() {
return provider;
}
}
27 changes: 27 additions & 0 deletions src/main/java/side/onetime/auth/dto/GoogleUserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package side.onetime.auth.dto;

import lombok.AllArgsConstructor;
import side.onetime.auth.constant.Provider;

import java.util.Map;

@AllArgsConstructor
public class GoogleUserInfo implements OAuth2UserInfo {

private Map<String, Object> attributes;

@Override
public String getProviderId() {
return (String) attributes.get("sub");
}

@Override
public String getProvider() {
return Provider.GOOGLE_PROVIDER.getProvider();
}

@Override
public String getName() {
return (String) attributes.get("name");
}
}
29 changes: 29 additions & 0 deletions src/main/java/side/onetime/auth/dto/KakaoUserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package side.onetime.auth.dto;

import lombok.AllArgsConstructor;
import side.onetime.auth.constant.Provider;

import java.util.Map;

@AllArgsConstructor
public class KakaoUserInfo implements OAuth2UserInfo {

private Map<String, Object> attributes;

@Override
public String getProviderId() {
// Long 타입이기 때문에 toString으로 변환
return attributes.get("id").toString();
}

@Override
public String getProvider() {
return Provider.KAKAO_PROVIDER.getProvider();
}

@Override
public String getName() {
// kakao_account라는 Map에서 추출
return (String) ((Map) attributes.get("properties")).get("nickname");
}
}
27 changes: 27 additions & 0 deletions src/main/java/side/onetime/auth/dto/NaverUserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package side.onetime.auth.dto;

import lombok.AllArgsConstructor;
import side.onetime.auth.constant.Provider;

import java.util.Map;

@AllArgsConstructor
public class NaverUserInfo implements OAuth2UserInfo {

private Map<String, Object> attributes;

@Override
public String getProviderId() {
return (String) attributes.get("id");
}

@Override
public String getProvider() {
return Provider.NAVER_PROVIDER.getProvider();
}

@Override
public String getName() {
return (String) attributes.get("name");
}
}
7 changes: 7 additions & 0 deletions src/main/java/side/onetime/auth/dto/OAuth2UserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package side.onetime.auth.dto;

public interface OAuth2UserInfo {
String getProviderId();
String getProvider();
String getName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package side.onetime.auth.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuthLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.error("LOGIN FAILED : {}", exception.getMessage());
super.onAuthenticationFailure(request, response, exception);

// 추후 구현
}
}
120 changes: 120 additions & 0 deletions src/main/java/side/onetime/auth/handler/OAuthLoginSuccessHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package side.onetime.auth.handler;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import side.onetime.auth.dto.GoogleUserInfo;
import side.onetime.auth.dto.KakaoUserInfo;
import side.onetime.auth.dto.NaverUserInfo;
import side.onetime.auth.dto.OAuth2UserInfo;
import side.onetime.domain.RefreshToken;
import side.onetime.domain.User;
import side.onetime.repository.RefreshTokenRepository;
import side.onetime.repository.UserRepository;
import side.onetime.util.JwtUtil;

import java.io.IOException;
import java.util.Map;

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuthLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

@Value("${jwt.redirect.access}")
private String ACCESS_TOKEN_REDIRECT_URI;

@Value("${jwt.redirect.register}")
private String REGISTER_TOKEN_REDIRECT_URI;

@Value("${jwt.access-token.expiration-time}")
private long ACCESS_TOKEN_EXPIRATION_TIME;

@Value("${jwt.refresh-token.expiration-time}")
private long REFRESH_TOKEN_EXPIRATION_TIME;

private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
String provider = token.getAuthorizedClientRegistrationId(); // provider 추출

OAuth2UserInfo oAuth2UserInfo = extractOAuth2UserInfo(token, provider);
handleAuthentication(request, response, oAuth2UserInfo, provider);
}

// OAuth2UserInfo 추출
private OAuth2UserInfo extractOAuth2UserInfo(OAuth2AuthenticationToken token, String provider) {
switch (provider) {
case "google":
log.info("구글 로그인 요청");
return new GoogleUserInfo(token.getPrincipal().getAttributes());
case "kakao":
log.info("카카오 로그인 요청");
return new KakaoUserInfo(token.getPrincipal().getAttributes());
case "naver":
log.info("네이버 로그인 요청");
return new NaverUserInfo((Map<String, Object>) token.getPrincipal().getAttributes().get("response"));
default:
throw new IllegalArgumentException("지원하지 않는 OAuth2 제공자입니다.");
}
}

// 인증 처리
private void handleAuthentication(HttpServletRequest request, HttpServletResponse response, OAuth2UserInfo oAuth2UserInfo, String provider) throws IOException {
String providerId = oAuth2UserInfo.getProviderId();
String name = oAuth2UserInfo.getName();

User existUser = userRepository.findByProviderId(providerId);

if (existUser == null) {
// 신규 유저 처리
handleNewUser(request, response, provider, providerId, name);
} else {
// 기존 유저 처리
handleExistingUser(request, response, existUser);
}

log.info("유저 이름 : {}", name);
log.info("PROVIDER : {}", provider);
log.info("PROVIDER_ID : {}", providerId);
}

// 신규 유저 처리
private void handleNewUser(HttpServletRequest request, HttpServletResponse response, String provider, String providerId, String name) throws IOException {
log.info("신규 유저입니다.");
String registerToken = jwtUtil.generateRegisterToken(provider, providerId, name, ACCESS_TOKEN_EXPIRATION_TIME);
String redirectUri = String.format(REGISTER_TOKEN_REDIRECT_URI, registerToken);
getRedirectStrategy().sendRedirect(request, response, redirectUri);
}

// 기존 유저 처리
private void handleExistingUser(HttpServletRequest request, HttpServletResponse response, User user) throws IOException {
log.info("기존 유저입니다.");
Long userId = user.getId();

// 액세스 & 리프레쉬 토큰 발급 및 저장
String accessToken = jwtUtil.generateAccessToken(userId, ACCESS_TOKEN_EXPIRATION_TIME);
String refreshToken = jwtUtil.generateRefreshToken(userId, REFRESH_TOKEN_EXPIRATION_TIME);
saveRefreshToken(userId, refreshToken);

// 리다이렉트 처리
String redirectUri = String.format(ACCESS_TOKEN_REDIRECT_URI, accessToken, refreshToken);
getRedirectStrategy().sendRedirect(request, response, redirectUri);
}

// Refresh Token 저장
private void saveRefreshToken(Long userId, String refreshToken) {
RefreshToken newRefreshToken = new RefreshToken(userId, refreshToken);
refreshTokenRepository.save(newRefreshToken);
}
}
28 changes: 28 additions & 0 deletions src/main/java/side/onetime/controller/TokenController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package side.onetime.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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 side.onetime.dto.TokenDto;
import side.onetime.global.common.ApiResponse;
import side.onetime.global.common.constant.SuccessStatus;
import side.onetime.service.TokenService;

@RestController
@RequestMapping("/api/v1/tokens")
@RequiredArgsConstructor
public class TokenController {
private final TokenService tokenService;

// 액세스 토큰 재발행 API
@PostMapping("/action-reissue")
public ResponseEntity<ApiResponse<TokenDto.ReissueTokenResponse>> reissueToken(
@RequestBody TokenDto.ReissueTokenRequest reissueAccessTokenRequest) {

TokenDto.ReissueTokenResponse reissueTokenResponse = tokenService.reissueToken(reissueAccessTokenRequest);
return ApiResponse.onSuccess(SuccessStatus._REISSUE_TOKENS, reissueTokenResponse);
}
}
28 changes: 28 additions & 0 deletions src/main/java/side/onetime/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package side.onetime.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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 side.onetime.dto.UserDto;
import side.onetime.global.common.ApiResponse;
import side.onetime.global.common.constant.SuccessStatus;
import side.onetime.service.UserService;

@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;

// 유저 온보딩 API
@PostMapping("/onboarding")
public ResponseEntity<ApiResponse<UserDto.OnboardUserResponse>> onboardUser(
@RequestBody UserDto.OnboardUserRequest onboardUserRequest) {

UserDto.OnboardUserResponse onboardUserResponse = userService.onboardUser(onboardUserRequest);
return ApiResponse.onSuccess(SuccessStatus._ONBOARD_USER, onboardUserResponse);
}
}
16 changes: 16 additions & 0 deletions src/main/java/side/onetime/domain/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package side.onetime.domain;

import jakarta.persistence.Id;
import lombok.Getter;

@Getter
public class RefreshToken {
@Id
private Long userId;
private String refreshToken;

public RefreshToken(Long userId, String refreshToken) {
this.userId = userId;
this.refreshToken = refreshToken;
}
}
Loading

0 comments on commit 67fd96a

Please sign in to comment.