Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/backend/auth_server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@ repositories {
mavenCentral()
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.2"
}
}

dependencies {
// Spring
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

//db
//implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3'
//implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Lombok
compileOnly 'org.projectlombok:lombok'
Expand All @@ -40,8 +46,8 @@ dependencies {
//junit
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//External API
//implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.0'
// Spring Cloud OpenFeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.jootalkpia.auth_server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@EnableFeignClients
@SpringBootApplication
@ImportAutoConfiguration(FeignAutoConfiguration.class)
public class AuthServerApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.jootalkpia.auth_server.client.dto;


import com.jootalkpia.auth_server.user.domain.SocialType;

public record UserInfoResponse(
Long socialId,
SocialType socialType,
String email,
String socialNickname
) {
public static UserInfoResponse of(
final Long socialId,
final SocialType socialType,
final String email,
final String socialNickname
) {
return new UserInfoResponse(socialId, socialType, email, socialNickname);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

localhost로 되어있는 부분 경로를 환경변수처리하면 나중에 배포 환경에 적합하게 바꿀 때 편할 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 좋은 부분이에요 감사합니다! 다만 redirectUri를 request에 클라이언트가 보내주시는건데 @Schema를 통해 예시 들어드리는 부분입니당! 저희가 따로 처리하는 부분은 아닌데 어떻게할까요??

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.jootalkpia.auth_server.client.dto;

import com.jootalkpia.auth_server.user.domain.SocialType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record UserLoginRequest(
@NotNull(message = "소셜 로그인 종류가 입력되지 않았습니다.")
@Schema(description = "소셜로그인 타입", example = "KAKAO")
SocialType socialType,

@NotBlank(message = "redirectUri가 입력되지 않았습니다.")
@Schema(description = "리다이텍트 uri 값", example = "http://localhost:5173/kakao/redirection")
String redirectUri
) {
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 경우에는 게이트웨이에서 passport를 주지 않고 헤더를 직접 확인하는 방식으로 하는걸까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passport는 토큰을 기반으로 만들어지게 되는데요. 회원가입 부분은 토큰이 없는 상태로 응답해야하는 api 입니당!

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.jootalkpia.auth_server.client.kakao;


import com.jootalkpia.auth_server.client.kakao.response.KakaoUserResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com")
public interface KakaoApiClient {

@GetMapping(value = "/v2/user/me")
KakaoUserResponse getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.jootalkpia.auth_server.client.kakao;


import com.jootalkpia.auth_server.client.kakao.response.KakaoAccessTokenResponse;
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 = "kakaoAuthApiClient", url = "https://kauth.kakao.com")
public interface KakaoAuthApiClient {
@PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
KakaoAccessTokenResponse getOAuth2AccessToken(
@RequestParam("grant_type") String grantType,
@RequestParam("client_id") String clientId,
@RequestParam("redirect_uri") String redirectUri,
@RequestParam("code") String code
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.jootalkpia.auth_server.client.kakao;

import com.jootalkpia.auth_server.client.dto.UserInfoResponse;
import com.jootalkpia.auth_server.client.dto.UserLoginRequest;
import com.jootalkpia.auth_server.client.kakao.response.KakaoAccessTokenResponse;
import com.jootalkpia.auth_server.client.kakao.response.KakaoUserResponse;
import com.jootalkpia.auth_server.client.service.SocialService;
import com.jootalkpia.auth_server.exception.CustomException;
import com.jootalkpia.auth_server.response.ErrorCode;
import com.jootalkpia.auth_server.user.domain.SocialType;
import feign.FeignException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
@RequiredArgsConstructor
public class KakaoSocialService implements SocialService {

private static final String AUTH_CODE = "authorization_code";

@Value("${kakao.clientId}")
private String clientId;
private final KakaoApiClient kakaoApiClient;
private final KakaoAuthApiClient kakaoAuthApiClient;

@Transactional
@Override
public UserInfoResponse login(
final String authorizationCode,
final UserLoginRequest loginRequest
) {
String accessToken;
try {
// 인가 코드로 Access Token + Refresh Token 받아오기
accessToken = getOAuth2Authentication(authorizationCode, loginRequest.redirectUri());
} catch (FeignException e) {
throw new CustomException(ErrorCode.AUTHENTICATION_CODE_EXPIRED);
}
// Access Token으로 유저 정보 불러오기
return getLoginDto(loginRequest.socialType(), getUserInfo(accessToken));
}

private String getOAuth2Authentication(
final String authorizationCode,
final String redirectUri
) {
KakaoAccessTokenResponse response = kakaoAuthApiClient.getOAuth2AccessToken(
AUTH_CODE,
clientId,
redirectUri,
authorizationCode
);
return response.accessToken();
}

private KakaoUserResponse getUserInfo(
final String accessToken
) {
return kakaoApiClient.getUserInformation("Bearer " + accessToken);
}

private UserInfoResponse getLoginDto(
final SocialType socialType,
final KakaoUserResponse userResponse
) {
return UserInfoResponse.of(
userResponse.id(),
socialType,
userResponse.kakaoAccount().email(),
userResponse.kakaoAccount().profile().nickname()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.jootalkpia.auth_server.client.kakao.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoAccessTokenResponse(
String accessToken
) {
public static KakaoAccessTokenResponse of(
final String accessToken
) {
return new KakaoAccessTokenResponse(
accessToken
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jootalkpia.auth_server.client.kakao.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoAccount(
String email,
KakaoUserProfile profile
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.jootalkpia.auth_server.client.kakao.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoUserProfile(
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jootalkpia.auth_server.client.kakao.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoUserResponse(
Long id,
KakaoAccount kakaoAccount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.jootalkpia.auth_server.client.service;


import com.jootalkpia.auth_server.client.dto.UserInfoResponse;
import com.jootalkpia.auth_server.client.dto.UserLoginRequest;

public interface SocialService {
UserInfoResponse login(final String authorizationToken, final UserLoginRequest loginRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,17 @@ public class SecurityConfig {
"/api/v1/user/login",
"/api/v1/user/token-refresh",
"/api/v1/actuator/health",
"/api/v1/v3/api-docs/**",
"/api/v1/swagger-ui/**",
"/api/v1/swagger-resources/**"
"/v3/**",
"/swagger-ui/**"
};


@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> {
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
})
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exception ->
{
exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.jootalkpia.auth_server.jwt;

import com.jootalkpia.auth_server.redis.Token;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;

public interface TokenRepository extends CrudRepository<Token, Long> {

Optional<Token> findByRefreshToken(final String refreshToken);

Optional<Token> findById(final Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.jootalkpia.auth_server.jwt;

import com.jootalkpia.auth_server.redis.Token;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class TokenService {

private final TokenRepository tokenRepository;

@Transactional
public void saveRefreshToken(final Long userId, final String refreshToken) {
tokenRepository.save(
Token.of(userId, refreshToken)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public enum ErrorCode {
BAD_REQUEST("A40001", HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
MISSING_REQUIRED_HEADER("A40002", HttpStatus.BAD_REQUEST, "필수 헤더가 누락되었습니다."),
MISSING_REQUIRED_PARAMETER("A40003", HttpStatus.BAD_REQUEST, "필수 파라미터가 누락되었습니다."),
AUTHENTICATION_CODE_EXPIRED("A40004", HttpStatus.BAD_REQUEST, "인가 코드가 만료되었습니다."),
SOCIAL_TYPE_BAD_REQUEST("A40005", HttpStatus.BAD_REQUEST, "로그인 요청이 유효하지 않습니다."),

// 401 Unauthorized
ACCESS_TOKEN_EXPIRED("A40100", HttpStatus.UNAUTHORIZED, "액세스 토큰이 만료되었습니다."),
Expand All @@ -25,6 +27,7 @@ public enum ErrorCode {

// 404 Not Found
NOT_FOUND_END_POINT("A40400", HttpStatus.NOT_FOUND, "존재하지 않는 API입니다."),
USER_NOT_FOUND("A40401", HttpStatus.NOT_FOUND, "해당 유저는 존재하지 않습니다."),

// 405 Method Not Allowed Error
METHOD_NOT_ALLOWED("A40500", HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 메소드입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.jootalkpia.auth_server.user.controller;

import com.jootalkpia.auth_server.client.dto.UserLoginRequest;
import com.jootalkpia.auth_server.user.dto.LoginSuccessResponse;
import com.jootalkpia.auth_server.user.service.UserService;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class UserController implements UserControllerDocs {

private final UserService userService;

@Override
@PostMapping("api/v1/user/login")
public ResponseEntity<LoginSuccessResponse> login(
@RequestParam final String authorizationCode,
@RequestBody final UserLoginRequest loginRequest
) {
return ResponseEntity.ok().body(userService.create(authorizationCode, loginRequest));
}
}
Loading
Loading