Skip to content

Commit

Permalink
Feat: OIDC 카카오 로그인 구현, CurrentUser 어노테이션 설정
Browse files Browse the repository at this point in the history
* Feat: 엔티티 수정(User->Member), 양방향 매핑 삭제

* Fix: JoinColumn 오타 수정

* refactor: 파일 구조 변경

* feat: application-dev.yml 파일 update

* feat: spring cloud/openfeign 라이브러리 추가

* fix: errorstatus 이름 변경

* feat: authorization code로 idToken 발급 로직 구현

* feat: yml 내용 추가

* fix: socialtype 수정

* feat: Update securityConfig

* feat: _EMPTY_MEMBER -> _EMPTY_USER, 인증관련 에러코드 추가

* feat: Member 엔티티 implements UserDetails(Spring Security), payload 함수 추가

* feat: redis(elasticache 환경 설정) 추가

* feat: update gitignore

* feat: RedisController, RedisService 구현

* feat: Kakao OIDC 공개키 캐싱 로직 구현

* feat: CurrentUser 어노테이션 구현

* feat: createdAt, updatedAt 자동 업데이트를 위한 @EnableJpaAuditing 어노테이션 추가

* feat: swagger JWT Bearer 인증버튼 설정

* feat: IdToken 검증 로직 및 로그인/회원가입 로직 구현

* fix: 기존 API CurrentUser 어노테이션으로 수정

* fix: toEntity, updateToken 함수 구현

* fix: findByEmail 구현(회원가입 여부 판별 시 사용)

* fix: OIDC표준 OauthOIDCHelper

* feat: yml update

* fix: 테스트용 코드 제거

* feat: add swagger description

* Feat: ApiResponse 적용
  • Loading branch information
yerim216 authored May 22, 2024
1 parent 64071c1 commit fbc1769
Show file tree
Hide file tree
Showing 45 changed files with 1,012 additions and 82 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
application-local.yml
# Created by https://www.toptal.com/developers/gitignore/api/intellij,java,gradle
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,java,gradle

Expand Down
47 changes: 47 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ repositories {
mavenCentral()
}

ext {
set('springCloudVersion', "2023.0.0")
}

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


dependencies {

implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -38,6 +49,42 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'

//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

//security
implementation 'org.springframework.boot:spring-boot-starter-security'

//oauth 2.0
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64'

//kakao access token을 가져오기 위한 Oauth 서버와의 통신을 위한 webclient
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'io.projectreactor.netty:reactor-netty'

//redis & cache
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'

//spring-cloud & openfeign
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'







//oauth2-client
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
//Oauth 서버와의 통신을 위한 webclient
implementation 'org.springframework.boot:spring-boot-starter-webflux'
//jwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'

}

Expand Down
2 changes: 1 addition & 1 deletion config
21 changes: 3 additions & 18 deletions src/main/java/com/example/tripy/TripyApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,22 @@
import io.swagger.v3.oas.annotations.servers.Server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@OpenAPIDefinition(servers = {
@Server(url = "/", description = "Default Server URL")
})

@EnableScheduling
@SpringBootApplication
@EnableFeignClients
@EnableJpaAuditing
public class TripyApplication {

public static void main(String[] args) {
SpringApplication.run(TripyApplication.class, args);
}


// @Bean
// public WebMvcConfigurer corsConfigurer() {
// return new WebMvcConfigurer() {
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedOrigins("*")
// .allowedMethods("GET", "POST", "PUT", "DELETE")
// .allowedHeaders("*")
// .maxAge(3000);
// }
// };
// }

}
30 changes: 30 additions & 0 deletions src/main/java/com/example/tripy/domain/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.tripy.domain.auth;


import com.example.tripy.domain.auth.dto.AuthResponseDto.LoginSimpleInfo;
import com.example.tripy.global.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/auth")
@Tag(name="Auth", description = "인증(로그인, 회원가입) 관련")
public class AuthController {

private final AuthService authService;

@PostMapping("/login")
@Operation(summary = "로그인/회원가입 api", description = "code : Authorization code / 회원가입, 로그인 구분 없이 동일한 API 사용")
public ApiResponse<LoginSimpleInfo> getIdToken(@RequestParam String code){
String idToken = authService.getOauth2Authentication(code);
return ApiResponse.onSuccess(authService.login(idToken));
}


}
100 changes: 100 additions & 0 deletions src/main/java/com/example/tripy/domain/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.example.tripy.domain.auth;

import com.example.tripy.domain.auth.dto.AuthResponseDto.KakaoAccessTokenResponse;
import com.example.tripy.domain.auth.dto.AuthResponseDto.LoginSimpleInfo;
import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCDecodePayload;
import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse;
import com.example.tripy.domain.member.Member;
import com.example.tripy.domain.member.MemberRepository;
import com.example.tripy.domain.member.enums.SocialType;
import com.example.tripy.global.security.JwtTokenProvider;
import java.util.Optional;
import lombok.RequiredArgsConstructor;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@RequiredArgsConstructor
@Service
public class AuthService {
private final KakaoAuthApiClient kakaoAuthApiClient;
private final RedisTemplate<String, Object> redisTemplate;
private final KakaoOauthHelper kakaoOauthHelper;
private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;


@Value("${security.oauth2.client.kakao.authorization-grant-type}")
private String grantType;
@Value("${security.oauth2.client.kakao.client-id}")
private String clientId;
@Value("${security.oauth2.client.kakao.redirect-uri}")
private String redirectUri;




public String getOauth2Authentication(final String authorizationCode){
KakaoAccessTokenResponse tokenInfo = kakaoAuthApiClient.getOAuth2AccessToken(
grantType,
clientId,
redirectUri,
authorizationCode
);
return tokenInfo.idToken();
}

@Transactional
public LoginSimpleInfo login(String idToken){
OIDCDecodePayload oidcDecodePayload = kakaoOauthHelper.getOIDCDecodePayload(idToken);
String email = oidcDecodePayload.email();
String nickName = oidcDecodePayload.nickName();
String picture = oidcDecodePayload.picture();
Optional<Member> optionalMember = memberRepository.findByEmail(email);

String accessToken = null;
String refreshToken = null;
Member member = null;

//회원가입
if(optionalMember.isEmpty()){
member = Member.toEntity(email, nickName, picture, SocialType.KAKAO);
memberRepository.save(member);
accessToken = jwtTokenProvider.createAccessToken(member.getPayload());
refreshToken = jwtTokenProvider.createRefreshToken(member.getId());
}
if(optionalMember.isPresent()){
accessToken = jwtTokenProvider.createAccessToken(optionalMember.get().getPayload());
refreshToken = jwtTokenProvider.createRefreshToken(optionalMember.get().getId());
member = optionalMember.get();

}

member.updateToken(accessToken, refreshToken);

return LoginSimpleInfo.toDTO(accessToken, refreshToken);


}

@Cacheable(cacheNames = "KakaoOIDC", cacheManager = "oidcCacheManager")
public OIDCPublicKeysResponse getKakaoOIDCOpenKeys() {
return kakaoAuthApiClient.getKakaoOIDCOpenKeys();
}

public void updateOpenKeyTestRedis() {
OIDCPublicKeysResponse oidcPublicKeysResponse = getKakaoOIDCOpenKeys();
saveOIDCPublicKeysResponse(oidcPublicKeysResponse);
}
public void saveOIDCPublicKeysResponse(OIDCPublicKeysResponse response) {
String key = "oidc:public_keys";

redisTemplate.opsForValue().set(key, response);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.tripy.domain.auth;

import com.example.tripy.domain.auth.dto.AuthResponseDto.KakaoAccessTokenResponse;
import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;


@FeignClient(name= "kakaoApiClient", 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 //인가코드
);


//@Cacheable(cacheNames = "KakaoOIDC", cacheManager = "oidcCacheManager")
@GetMapping("/.well-known/jwks.json")
OIDCPublicKeysResponse getKakaoOIDCOpenKeys();





}


30 changes: 30 additions & 0 deletions src/main/java/com/example/tripy/domain/auth/KakaoOauthHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.tripy.domain.auth;


import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCDecodePayload;
import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class KakaoOauthHelper {
private final KakaoAuthApiClient kakaoAuthApiClient;
private final OauthOIDCHelper oauthOIDCHelper;
@Value("${security.oauth2.client.kakao.client-id}")
private String appId;

@Value("${security.oauth2.client.kakao.iss}")
private String iss;

public OIDCDecodePayload getOIDCDecodePayload(String token){
OIDCPublicKeysResponse oidcPublicKeysResponse = kakaoAuthApiClient.getKakaoOIDCOpenKeys();
return oauthOIDCHelper.getPayloadFromIdToken(
token, iss, appId, oidcPublicKeysResponse
);
}



}
35 changes: 35 additions & 0 deletions src/main/java/com/example/tripy/domain/auth/OauthOIDCHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.tripy.domain.auth;

import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCDecodePayload;
import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKey;
import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse;
import com.example.tripy.global.security.JwtOIDCProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;


@RequiredArgsConstructor
@Component
public class OauthOIDCHelper {

private final JwtOIDCProvider jwtOIDCProvider;

//토큰에서 kid 가져온다 -> 가져온 kid는 공개키 결정에 사용
private String getKidFromUnsignedIdToken(String token, String iss, String aud){
return jwtOIDCProvider.getKidFromUnsignedTokenHeader(token, iss, aud);
}

public OIDCDecodePayload getPayloadFromIdToken(String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse){
String kid = getKidFromUnsignedIdToken(token, iss, aud);

//같은 Kid인 공개키 불러와서 토큰 검증에 사용
OIDCPublicKey oidcPublicKey = oidcPublicKeysResponse.keys().stream()
.filter(o-> o.kid().equals(kid))
.findFirst()
.orElseThrow();

//검증 된 토큰에서 바디를 꺼내온다
return jwtOIDCProvider.getOIDCTokenBody(token, oidcPublicKey.n(), oidcPublicKey.e());
}

}
Loading

0 comments on commit fbc1769

Please sign in to comment.