Skip to content

Commit

Permalink
Merge pull request #32 from ki-met-hoon/feature/#29-login
Browse files Browse the repository at this point in the history
카카오 로그인 서비스 구현
  • Loading branch information
ki-met-hoon authored Mar 17, 2024
2 parents f32b965 + f0db47a commit 8a3b031
Show file tree
Hide file tree
Showing 20 changed files with 486 additions and 122 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@EnableFeignClients
@EnableCaching
@SpringBootApplication
public class PnuUnivMiryangCampusApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.pnuunivmiryangcampus.auth;

import lombok.Getter;

@Getter
public class Exception500 extends RuntimeException {
public Exception500(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.example.pnuunivmiryangcampus.auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class JwtOIDCProvider {

public String getKidFromTokenHeader(String token) {

String KID = "kid";
String[] splitToken = token.split("\\.");
String header = splitToken[0];

byte[] decodeJson = Base64.getDecoder().decode(header);
String decodeHeader = new String(decodeJson);

try {
JSONObject jsonObject = new JSONObject(decodeHeader);
return jsonObject.get(KID).toString();
} catch (JSONException e) {
return e.toString();
}
}

public Jws<Claims> getOIDCTokenJws(String token, String modulus, String exponent, String iss, String aud) {

try {
return Jwts.parser()
.verifyWith(getRSAPublicKey(modulus, exponent))
.requireAudience(aud)
.requireIssuer(iss)
.build()
.parseSignedClaims(token);
} catch (ExpiredJwtException e) {
throw new Exception500(e.getMessage());
} catch (Exception e) {
log.error(e.toString());
throw new Exception500(e.getMessage());
}
}

public OIDCDecodePayload getOIDCTokenBody(String token, String modulus, String exponent, String iss, String aud) {

Claims payload = getOIDCTokenJws(token, modulus, exponent, iss, aud).getPayload();

return new OIDCDecodePayload(
payload.getIssuer(),
payload.getAudience().toString(),
payload.getSubject(),
payload.get("email", String.class));
}

private PublicKey getRSAPublicKey(String modulus, String exponent) throws NoSuchAlgorithmException, InvalidKeySpecException {

KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodeN = Base64.getUrlDecoder().decode(modulus);
byte[] decodeE = Base64.getUrlDecoder().decode(exponent);
BigInteger n = new BigInteger(1, decodeN);
BigInteger e = new BigInteger(1, decodeE);

RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
return keyFactory.generatePublic(keySpec);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.pnuunivmiryangcampus.auth;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient(
name = "KakaoInfoClient",
url = "${feign.client.kakao.oicd-base-url}")
public interface KakaoInfoClient {

@GetMapping("${feign.client.kakao.oicd-userinfo-uri}")
KakaoInformationResponse kakaoUserInfo(@RequestHeader("Authorization") String accessToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.pnuunivmiryangcampus.auth;

public record KakaoInformationResponse(
String sub,
String nickname,
String email,
boolean emailVerified
) {

public static KakaoInformationResponse of(String sub, String nickname, String email, boolean emailVerified) {
return new KakaoInformationResponse(sub, nickname, email, emailVerified);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package com.example.pnuunivmiryangcampus.auth;

import com.example.pnuunivmiryangcampus.dto.KakaoTokenDto;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "${feign.client.kakao.name}", url = "${feign.client.kakao.base-url}")
public interface KakaoApiCaller {
@FeignClient(
name = "KakaoOauthClient",
url = "${feign.client.kakao.base-url}")
public interface KakaoOauthClient {

@PostMapping("/${feign.client.kakao.token-uri}")
@PostMapping("${feign.client.kakao.token-uri}")
KakaoTokenDto getKakaoToken(@RequestParam("client_id") String restApiKey,
@RequestParam("redirect_uri") String redirectUrl,
@RequestParam("code") String code,
@RequestParam("grant_type") String grantType);

@Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager")
@GetMapping("${feign.client.kakao.oicd-open-key-uri}")
OIDCPublicKeysResponse getKakaoOIDCOpenKeys();
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class KakaoProperties {
private String baseUrl;
private String authUrl;
private String tokenUri;
private String oicdOpenKeyUri;
private String restApiKey;
private String redirectUri;
private String grantType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.pnuunivmiryangcampus.auth;

public record OIDCDecodePayload(
/* issuer ex https://kauth.kakao.com */
String iss,
/* client id */
String aud,
/* oauth provider account unique id */
String sub,
String email
) {

public static OIDCDecodePayload of(String iss, String aud, String sub, String email) {
return new OIDCDecodePayload(iss, aud, sub, email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.pnuunivmiryangcampus.auth;

public record OIDCPublicKeyDto(
String kid,
String alg,
String use,
String n,
String e
) {

public static OIDCPublicKeyDto of(String kid, String alg, String use, String n, String e) {
return new OIDCPublicKeyDto(kid, alg, use, n, e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.pnuunivmiryangcampus.auth;

import java.util.List;

public record OIDCPublicKeysResponse(
List<OIDCPublicKeyDto> keys
) {

public static OIDCPublicKeysResponse of(List<OIDCPublicKeyDto> keys) {
return new OIDCPublicKeysResponse(keys);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.pnuunivmiryangcampus.auth;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OauthOIDCHelper {

private final JwtOIDCProvider jwtOIDCProvider;
private final KakaoOauthClient kakaoOauthClient;
private final KakaoProperties kakaoProperties;

private OIDCDecodePayload getPayloadFromIdToken(String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse) {
String kid = jwtOIDCProvider.getKidFromTokenHeader(token);

OIDCPublicKeyDto oidcPublicKeyDto = oidcPublicKeysResponse.keys().stream()
.filter(o -> o.kid().equals(kid))
.findFirst()
.orElseThrow();

return jwtOIDCProvider.getOIDCTokenBody(token, oidcPublicKeyDto.n(), oidcPublicKeyDto.e(), iss, aud);
}

public OIDCDecodePayload getKakaoOIDCDecodePayload(String token) {

OIDCPublicKeysResponse oidcPublicKeysResponse = kakaoOauthClient.getKakaoOIDCOpenKeys();
return getPayloadFromIdToken(
token,
kakaoProperties.getBaseUrl(),
kakaoProperties.getRestApiKey(),
oidcPublicKeysResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.pnuunivmiryangcampus.auth;

import java.time.Duration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching
@Configuration
public class RedisCacheConfig {
@Bean
public CacheManager oidcCacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration redisCacheConfiguration =
RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofDays(7L));

return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public String kakaoLogin() {

@GetMapping("/kakao/callback")
public ResponseEntity<KakaoTokenDto> getKakaoToken(@RequestParam String code) {
return ResponseEntity.ok(loginService.getKakaoToken(code));

KakaoTokenDto kakaoTokenDto = loginService.getKakaoToken(code);
loginService.isUserRegistered(kakaoTokenDto);

return ResponseEntity.ok(kakaoTokenDto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,45 @@

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import lombok.Getter;
import lombok.ToString;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@Getter
@ToString(callSuper = true)
@EntityListeners(AuditingEntityListener.class)
@Entity
public class UserAccount extends AuditingFields{
public class UserAccount extends AuditingFields {

@Column(nullable = false)
private String profileNickname;

@Column(nullable = false, length = 100)
private String accountEmail;

@Column(nullable = false)
private String sub;

@Column
private String memo;

@Column(nullable = false, length = 100, updatable = false)
private String createdBy;

protected UserAccount() {
}

private UserAccount(String profileNickname, String accountEmail, String memo) {
private UserAccount(String profileNickname, String accountEmail, String sub, String memo, String createdBy) {
this.profileNickname = profileNickname;
this.accountEmail = accountEmail;
this.sub = sub;
this.memo = memo;
this.createdBy = createdBy;
}

public static UserAccount of(String profileNickname, String accountEmail, String memo) {
return new UserAccount(profileNickname, accountEmail, memo);
public static UserAccount of(String profileNickname, String accountEmail, String sub, String memo,
String createdBy) {
return new UserAccount(profileNickname, accountEmail, sub, memo, createdBy);
}
}
Loading

0 comments on commit 8a3b031

Please sign in to comment.