Project/์‹นํ‹”์›€

[์‹นํ‹”์›€] 10/31 ๊ฐœ๋ฐœ์ผ์ง€ ์‹นํ‹”์›€ ํ”„๋กœ์ ํŠธ Oauth2.0 ์ ์šฉ ( Kakao, Google, NAVER )

ํ•œ33 2024. 11. 2. 17:19

๐Ÿ“š ๋ฐฐ๊ฒฝ

 

๊ธฐ์กด์— ์œ„์ฒ˜๋Ÿผ application.yml ํŒŒ์ผ์— kakao ์†Œ์…œ ๋กœ๊ทธ์ธ ์„ค์ •์„ ํ•ด๋†จ์—ˆ๋Š”๋ฐ,

 

google ์†Œ์…œ๋กœ๊ทธ์ธ๋„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ 

 

//oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

 

build.gradle ์— ์œ„์™€ ๊ฐ™์ด ์ถ”๊ฐ€๋ฅผ ํ•ด์ค€ ํ›„์— ๋‹ค์‹œ ๋Œ๋ฆฌ๋‹ˆ๊นŒ ๊ฐ ์ข… ์—๋Ÿฌ๋“ค์ด ๋ฐœ์ƒํ–ˆ๋‹ค.

 

Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration'

Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration'

Error creating bean with name 'OAuth2AuthorizedClientManager'

Failed to instantiate [org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager]: Factory method 'getAuthorizedClientManager' threw exception with message

Provider ID must be specified for client registration 'kakao'

๐ŸŒฑ ๊ฐœ์š”

1. ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ: ์ง์ ‘ ๊ตฌํ˜„

  • ์ง์ ‘ API ํ˜ธ์ถœ: RestTemplate์ด๋‚˜ WebClient๋กœ Kakao์˜ ๋กœ๊ทธ์ธ API์— ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ , ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ access token์„ ์‚ฌ์šฉํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ์‹.
  • ์ˆ˜๋™์œผ๋กœ ์ฒ˜๋ฆฌ: ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์ง์ ‘ ๋‹ค๋ฃจ๊ณ , ๋กœ๊ทธ์ธ ํ›„ ์‚ฌ์šฉ์ž ์ •๋ณด์™€ ์ธ์ฆ ์ฒ˜๋ฆฌ๋„ ๋ชจ๋‘ ์ˆ˜๋™์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ํผ.

๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ํ๋ฆ„์„ ์Šค์Šค๋กœ ์ปจํŠธ๋กคํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•  ๋•Œ ๊ฐ™์€ ๊ณผ์ •์„ ๋ฐ˜๋ณตํ•ด์•ผ ํ•ด์„œ ๋ฒˆ๊ฑฐ๋กœ์šธ ์ˆ˜ ์žˆ์Œ.

2. ์‚ฌ์šฉํ–ˆ์„ ๋•Œ์˜ ์ด์ : ์ž๋™ํ™”์™€ ๊ฐ„ํŽธํ•จ

  • ์ž๋™ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ: ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด Spring์ด ์ž๋™์œผ๋กœ Kakao์˜ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ํ›„ ๋‹ค์‹œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋Œ์•„์˜ค๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Œ์š”.
  • ํ† ํฐ ๋ฐ ์‚ฌ์šฉ์ž ์ •๋ณด ์ž๋™ ๊ด€๋ฆฌ: Spring์ด Kakao์˜ access token๊ณผ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ณ  ๊ด€๋ฆฌํ•ด ์ฃผ๊ธฐ ๋•Œ๋ฌธ์—, ๋กœ๊ทธ์ธ ํ›„ ์ถ”๊ฐ€์ ์ธ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฑฐ์˜ ํ•„์š” ์—†์Œ.
  • ๋‹ค๋ฅธ ์†Œ์…œ ๋กœ๊ทธ์ธ๊ณผ ํ™•์žฅ์„ฑ: Kakao ์™ธ์— ๋‹ค๋ฅธ ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•  ๋•Œ๋„ ์„ค์ •๋งŒ ํ•˜๋ฉด ๋˜๊ณ , ์ฝ”๋“œ๋ฅผ ๊ฑฐ์˜ ์ˆ˜์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Œ.

์ฆ‰, spring-boot-starter-oauth2-client๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๋‹จ์ˆœํ•ด์ง€๊ณ  ๋ฐ˜๋ณต๋˜๋Š” ๋ถ€๋ถ„์„ ์ค„์ผ ์ˆ˜ ์žˆ์–ด์„œ ํ™•์žฅ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด์—์„œ๋„ ๋” ํŽธ๋ฆฌํ•จ.


๊ฒฐ๋ก 

์œ„์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ƒ ์•ˆ ํ•˜๋ƒ๋Š” ๋‚ด๊ฐ€ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด๊ณ , ๊ธฐ์กด์— kakao ์†Œ์…œ ๋กœ๊ทธ์ธ ํ•˜๋‚˜๋งŒ ๊ตฌํ˜„๋˜์–ด์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘ ํ™˜๊ฒฝ์„ ๊ตฌํ˜„ํ–ˆ๋˜ ๊ฒƒ์ด๋‹ค. 

 

ํ•˜์ง€๋งŒ ๊ตฌ๊ธ€๊ณผ ๋„ค์ด๋ฒ„๊นŒ์ง€ ์†Œ์…œ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•˜๋ ค๋Š” ์ง€๊ธˆ์€ ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹์•„๋ณด์˜€๋‹ค.


๐ŸŒฑ ์‚ฌ์šฉ๋ฒ•

  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: ${KAKAO_CLIENT_ID}
            redirect-uri: "http://localhost:8080/ssaktium/signin/kakao"
            authorization-grant-type: authorization_code
            scope: profile_nickname, account_email, birthyear
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            redirect-uri: "http://localhost:8080/ssaktium/signin/google"
            authorization-grant-type: authorization_code
            scope: email, profile, https://www.googleapis.com/auth/user.birthday.read
          naver:
            client-id: ${NAVER_CLIENT_ID}
            client-secret: ${NAVER_CLIENT_SECRET}
            redirect-uri: "http://localhost:8080/ssaktium/signin-naver"
            authorization-grant-type: authorization_code
            scope: name, email, birthyear

        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/auth
            token-uri: https://oauth2.googleapis.com/token
            user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
            user-name-attribute: sub
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response.id

 

ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์œ„์ฒ˜๋Ÿผ ์–‘์‹์„ ๋งž์ถฐ์„œ ์ž‘์„ฑํ•ด์•ผํ•œ๋‹ค.

 

Authorization Code Grant Type (์Šน์ธ ์ฝ”๋“œ ๋ฐฉ์‹)

 

OAuth2์—๋Š” ์—ฌ๋Ÿฌ ์Šน์ธ ๋ฐฉ์‹์ด ์žˆ๋Š”๋ฐ, Authorization Code ๋ฐฉ์‹์€ ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ๋ฐฉ์‹์œผ๋กœ, ๋ณด์•ˆ์ด ๊ฐ•ํ™”๋œ ์ธ์ฆ ํ”Œ๋กœ์šฐ๋‹ค. ์ด ๋ฐฉ์‹์€ ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜(์˜ˆ: ๋‹น์‹ ์˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜)์ด ์™ธ๋ถ€ ์ธ์ฆ ์ œ๊ณต์ž(์˜ˆ: Kakao, Google)๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์Šน์ธ ์ฝ”๋“œ(Authorization Code)๋ฅผ ๋ฐ›์•„, ์ด ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์•ก์„ธ์Šค ํ† ํฐ์„ ์–ป์–ด์˜ค๋Š” ๊ตฌ์กฐ.

 


CustomOauthController

// ์†Œ์…œ๋กœ๊ทธ์ธ
@GetMapping("/ssaktium/signin/{provider}")
public String socialLogin(
        @PathVariable("provider") String provider,
        @RequestParam("code") String code, HttpServletResponse response) throws JsonProcessingException {

    String token = customOauthService.socialLogin(provider, code, response);
    Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7));
    cookie.setPath("/");

 

provider Param ์„ ๋ฐ›์•„์„œ ์นด์นด์˜ค, ๋„ค์ด๋ฒ„, ๊ตฌ๊ธ€ ๋กœ ์†Œ์…œ๋กœ๊ทธ์ธ์„ ์ง„ํ–‰ ํ–ˆ์„ ๋•Œ ํ•˜๋‚˜์˜ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ง„ํ–‰๋  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ–ˆ๋‹ค.


CustomOauthInfoDto

@Getter
@NoArgsConstructor
public class CustomOauthInfoDto {
    private String socialId;
    private String userName;
    private String email;
    private String birthYear;


    public CustomOauthInfoDto(String socialId, String userName, String email) {
        this.socialId = socialId;
        this.userName = userName;
        this.email = email;
    }

    public CustomOauthInfoDto(String socialId, String userName, String email, String birthYear) {
        this.socialId = socialId;
        this.userName = userName;
        this.email = email;
        this.birthYear = birthYear;
    }

    public static CustomOauthInfoDto addGoogleId(String socialId, String userName, String email) {
        return new CustomOauthInfoDto(socialId, userName, email);
    }
}

 

์†Œ์…œ๋กœ๊ทธ์ธ ์‹œ ์ œ๊ณต๋ฐ›๋Š” ํšŒ์›์ •๋ณด ๋ฐ์ดํ„ฐ๋ฅผ socialId, userName, email, birthYear ์ด ํฌํ•จ๋œ Dto ๋กœ ๋ฐ›์•„์„œ Service ์—์„œ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด Dto ๋ฅผ ๋งŒ๋“ค์–ด์คฌ๋‹ค.


CustomOauthService

// ์†Œ์…œ ๋กœ๊ทธ์ธ ์„œ๋น„์Šค๋ฅผ ํ†ตํ•ด ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ
public String socialLogin(String provider, String code, HttpServletResponse response) throws JsonProcessingException {

    // 1. "์ธ๊ฐ€ ์ฝ”๋“œ"๋กœ "์•ก์„ธ์Šค ํ† ํฐ" ์š”์ฒญ
    String accessToken = getAccessToken(provider, code);

    // 2. ํ† ํฐ์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ
    CustomOauthInfoDto UserInfo = fetchUserInfoFromProvider(accessToken, provider);

    // 3. ์œ ์ € ์ƒ์„ฑ
    User user = registerUserIfNeeded(UserInfo, response, provider);

    // 4. ํ† ํฐ ๋ฐœ๊ธ‰
    String createToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole());

    log.info(createToken);

    // 5. ํ† ํฐ ๋ฐ˜ํ™˜
    return createToken;
}

 

 


CustomOauthService

// AccessToken์„ ๋ฐœ๊ธ‰๋ฐ›๊ธฐ ์œ„ํ•œ ๊ณตํ†ต ๋ฉ”์„œ๋“œ
private String getAccessToken(String provider, String code) {
    String redirectUri = "http://localhost:8080/ssaktium/signin/" + provider;
    String url;
    String clientId;
    String clientSecret = null;  // ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ํ• ๋‹น

    // provider ์— ๋”ฐ๋ฅธ URL, clientId, clientSecret ์„ค์ •
    switch (provider) {
        case "kakao":
            url = "https://kauth.kakao.com/oauth/token";
            clientId = kakaoClientId;
            break;
        case "google":
            url = "https://oauth2.googleapis.com/token";
            clientId = googleClientId;
            clientSecret = googleClientSecret;
            break;
        case "naver":
            url = "https://nid.naver.com/oauth2.0/token";
            clientId = naverClientId;
            clientSecret = naverClientSecret;
            break;
        default:
            throw new NotFoundUserException();
    }

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    // ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("grant_type", "authorization_code");
    params.add("client_id", clientId);
    params.add("redirect_uri", redirectUri);
    params.add("code", code);

    // clientSecret ์ด ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ถ”๊ฐ€
    if (clientSecret != null) {
        params.add("client_secret", clientSecret);
    }

    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
    Map responseBody = restTemplate.postForObject(url, request, Map.class);

    if (responseBody == null || !responseBody.containsKey("access_token")) {
        log.error("Failed to fetch access token from {} provider. Response: {}", provider, responseBody);
        throw new NotFoundUserException();
    }

    return (String) responseBody.get("access_token");
}

 

kakao, Google, Naver ๋กœ ๋ถ€ํ„ฐ Access Token ์„ ๋ฐœ๊ธ‰ ๋ฐ›๊ธฐ ์œ„ํ•ด switch ๋ฌธ์„ ์จ์„œ ๊ฐ ๊ฐ์˜ ์ผ€์ด์Šค์— ๋งž๊ฒŒ Token ์„ ๋ฐ›์•„์˜ค๋Š” ๋กœ์ง์„ ์งฐ๋‹ค.


CustomOauthService

// ์†Œ์…œ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด๋กœ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ
public User registerUserIfNeeded(CustomOauthInfoDto userInfo, HttpServletResponse response, String provider) {
    // ๊ฐ™์€ ์ด๋ฉ”์ผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
    String changedEmail = userInfo.getEmail() + "_" + provider;
    User existingUser = userRepository.findByEmail(changedEmail).orElse(null);

    if (existingUser == null) {
        // ๊ธฐ์กด ์‚ฌ์šฉ์ž๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์‹ ๊ทœ ์‚ฌ์šฉ์ž๋กœ ๋“ฑ๋ก
        String password = UUID.randomUUID().toString();
        String encodedPassword = passwordEncoder.encode(password);
        String email = userInfo.getEmail() + "_" + provider;
        String birthYear = userInfo.getBirthYear();
        String socialAccountId = userInfo.getSocialId();

        existingUser = User.builder()
                .email(email)
                .userName(userInfo.getUserName())
                .password(encodedPassword)
                .birthYear(birthYear)
                .userRole(UserRole.ROLE_USER)
                .socialAccountId(socialAccountId)
                .build();

        userRepository.save(existingUser);

        // JWT ์ƒ์„ฑ ๋ฐ ํ—ค๋” ์ถ”๊ฐ€
        addJwtToResponse(existingUser, response);

    }
    return existingUser;
}
 // JWT ํ† ํฐ์„ ์ƒ์„ฑํ•˜๊ณ , ์‘๋‹ต ํ—ค๋”์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฉ”์„œ๋“œ

private void addJwtToResponse(User user, HttpServletResponse response) {
    String createToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole());

    jwtUtil.addTokenToResponseHeader(createToken, response);
}

 

๊ธฐ์กด ์œ ์ €๊ฐ€ ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ๋กœ๊ทธ์ธ, ์—†๋‹ค๋ฉด ์ƒˆ๋กœ User ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ DB ์— ์ €์žฅ์‹œํ‚ค๋Š” ๋กœ์ง์ด๊ณ , ์ƒ์„ฑ๋œ Jwt ํ† ํฐ์„ ํ—ค๋”์— ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด addJwtToResponse ๋ฉ”์„œ๋“œ๋„ ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค.


CustomOauthService

// AccessToken ์„ ์‚ฌ์šฉํ•˜์—ฌ ์†Œ์…œ ์ œ๊ณต์ž์˜ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
private CustomOauthInfoDto fetchUserInfoFromProvider(String accessToken, String provider) throws JsonProcessingException {
    return switch (provider) {
        case "kakao" -> fetchKakaoUserInfo(accessToken);
        case "google" -> fetchGoogleUserInfo(accessToken);
        case "naver" -> fetchNaverUserInfo(accessToken);
        default -> throw new NotFoundUserException();
    };
}

 

๋ฐœ๊ธ‰๋œ Access Token ์„ ๊ฐ ํ”Œ๋žซํผ์— ๋งž๊ฒŒ ๋ฉ”์„œ๋“œ๋กœ ์œ ๋„์‹œํ‚จ๋‹ค.


CustomOauthService

๋”๋ณด๊ธฐ
//์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
private CustomOauthInfoDto fetchKakaoUserInfo(String accessToken) throws JsonProcessingException {
    String url = "https://kapi.kakao.com/v2/user/me";

    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(accessToken);
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    HttpEntity<String> request = new HttpEntity<>(headers);
    String responseBody = restTemplate.exchange(url, HttpMethod.GET, request, String.class).getBody();

    if (responseBody == null) {
        log.error("Kakao API response body is null.");
        throw new NotFoundUserException();
    }

    JsonNode jsonNode = new ObjectMapper().readTree(responseBody);
    String socialId = jsonNode.get("id").asText();
    String email = jsonNode.get("kakao_account").get("email").asText();
    String birthYear = jsonNode.get("kakao_account")
            .get("birthyear").asText();
    String userName = jsonNode.get("properties").get("nickname").asText();

    log.info("์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด: " + socialId + ", " + userName + ", " + birthYear + ", " + email);
    return new CustomOauthInfoDto(socialId, userName, email, birthYear);
}

//๊ตฌ๊ธ€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
private CustomOauthInfoDto fetchGoogleUserInfo(String accessToken) throws JsonProcessingException {
    String url = "https://www.googleapis.com/oauth2/v3/userinfo";

    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(accessToken);

    HttpEntity<String> request = new HttpEntity<>(headers);
    String responseBody = restTemplate.exchange(url, HttpMethod.GET, request, String.class).getBody();

    if (responseBody == null) {
        log.error("Google API response body is null.");
        throw new NotFoundUserException();
    }

    JsonNode jsonNode = new ObjectMapper().readTree(responseBody);
    log.info(responseBody);
    String socialId = jsonNode.has("sub") ? jsonNode.get("sub").asText() : null;
    String name = jsonNode.has("name") ? jsonNode.get("name").asText() : null; // null ์ฒดํฌ
    String email = jsonNode.has("email") ? jsonNode.get("email").asText() : null; // null ์ฒดํฌ

    return CustomOauthInfoDto.addGoogleId(socialId, name, email);
}

// ๋„ค์ด๋ฒ„ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
private CustomOauthInfoDto fetchNaverUserInfo(String accessToken) throws JsonProcessingException {
    String url = "https://openapi.naver.com/v1/nid/me";

    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(accessToken);
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    HttpEntity<String> request = new HttpEntity<>(headers);
    String responseBody = restTemplate.exchange(url, HttpMethod.GET, request, String.class).getBody();

    if (responseBody == null) {
        log.error("Naver API response body is null.");
        throw new NotFoundUserException();
    }

    JsonNode jsonNode = new ObjectMapper().readTree(responseBody);
    JsonNode responseNode = jsonNode.get("response");
    String socialId = responseNode.has("id") ? responseNode.get("id").asText() : null; // null ์ฒดํฌ
    String email = responseNode.has("email") ? responseNode.get("email").asText() : null; // null ์ฒดํฌ
    String userName = responseNode.has("name") ? responseNode.get("name").asText() : null; // null ์ฒดํฌ
    String birthyear = responseNode.has("birthyear") ? responseNode.get("birthyear").asText() : null; // null ์ฒดํฌ

    log.info("๋„ค์ด๋ฒ„ ์‚ฌ์šฉ์ž ์ •๋ณด: " + ", " + userName + ", " + birthyear + ", " + email);
    return new CustomOauthInfoDto(socialId, userName, email, birthyear);
}

 

๊ฐ ํ”Œ๋žซํผ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋˜๋Š” ์–‘์‹์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ ํ”Œ๋žซํผ์— ๋งž๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ฐ ๊ฐ Dto ๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด์คฌ๋‹ค.


์ „์ฒด ์ฝ”๋“œ

๋”๋ณด๊ธฐ
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOauthService {
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;
    private final RestTemplate restTemplate;
    private final PasswordEncoder passwordEncoder;

    @Value("${spring.security.oauth2.client.registration.kakao.client-id}")
    private String kakaoClientId;

    @Value("${spring.security.oauth2.client.registration.google.client-id}")
    private String googleClientId;

    @Value("${spring.security.oauth2.client.registration.google.client-secret}")
    private String googleClientSecret;

    @Value("${spring.security.oauth2.client.registration.naver.client-id}")
    private String naverClientId;

    @Value("${spring.security.oauth2.client.registration.naver.client-secret}")
    private String naverClientSecret;


    // ์†Œ์…œ ๋กœ๊ทธ์ธ ์„œ๋น„์Šค๋ฅผ ํ†ตํ•ด ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ
    public String socialLogin(String provider, String code, HttpServletResponse response) throws JsonProcessingException {

        // 1. "์ธ๊ฐ€ ์ฝ”๋“œ"๋กœ "์•ก์„ธ์Šค ํ† ํฐ" ์š”์ฒญ
        String accessToken = getAccessToken(provider, code);

        // 2. ํ† ํฐ์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ
        CustomOauthInfoDto UserInfo = fetchUserInfoFromProvider(accessToken, provider);

        // 3. ์œ ์ € ์ƒ์„ฑ
        User user = registerUserIfNeeded(UserInfo, response, provider);

        // 4. ํ† ํฐ ๋ฐœ๊ธ‰
        String createToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole());

        log.info(createToken);

        // 5. ํ† ํฐ ๋ฐ˜ํ™˜
        return createToken;
    }


    // AccessToken์„ ๋ฐœ๊ธ‰๋ฐ›๊ธฐ ์œ„ํ•œ ๊ณตํ†ต ๋ฉ”์„œ๋“œ
    private String getAccessToken(String provider, String code) {
        String redirectUri = "http://localhost:8080/ssaktium/signin/" + provider;
        String url;
        String clientId;
        String clientSecret = null;  // ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ํ• ๋‹น

        // provider ์— ๋”ฐ๋ฅธ URL, clientId, clientSecret ์„ค์ •
        switch (provider) {
            case "kakao":
                url = "https://kauth.kakao.com/oauth/token";
                clientId = kakaoClientId;
                break;
            case "google":
                url = "https://oauth2.googleapis.com/token";
                clientId = googleClientId;
                clientSecret = googleClientSecret;
                break;
            case "naver":
                url = "https://nid.naver.com/oauth2.0/token";
                clientId = naverClientId;
                clientSecret = naverClientSecret;
                break;
            default:
                throw new NotFoundUserException();
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        // ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", clientId);
        params.add("redirect_uri", redirectUri);
        params.add("code", code);

        // clientSecret ์ด ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ถ”๊ฐ€
        if (clientSecret != null) {
            params.add("client_secret", clientSecret);
        }

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
        Map responseBody = restTemplate.postForObject(url, request, Map.class);

        if (responseBody == null || !responseBody.containsKey("access_token")) {
            log.error("Failed to fetch access token from {} provider. Response: {}", provider, responseBody);
            throw new NotFoundUserException();
        }

        return (String) responseBody.get("access_token");
    }


    // ์†Œ์…œ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด๋กœ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ
    public User registerUserIfNeeded(CustomOauthInfoDto userInfo, HttpServletResponse response, String provider) {
        // ๊ฐ™์€ ์ด๋ฉ”์ผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
        String changedEmail = userInfo.getEmail() + "_" + provider;
        User existingUser = userRepository.findByEmail(changedEmail).orElse(null);

        if (existingUser == null) {
            // ๊ธฐ์กด ์‚ฌ์šฉ์ž๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์‹ ๊ทœ ์‚ฌ์šฉ์ž๋กœ ๋“ฑ๋ก
            String password = UUID.randomUUID().toString();
            String encodedPassword = passwordEncoder.encode(password);
            String email = userInfo.getEmail() + "_" + provider;
            String birthYear = userInfo.getBirthYear();
            String socialAccountId = userInfo.getSocialId();

            existingUser = User.builder()
                    .email(email)
                    .userName(userInfo.getUserName())
                    .password(encodedPassword)
                    .birthYear(birthYear)
                    .userRole(UserRole.ROLE_USER)
                    .socialAccountId(socialAccountId)
                    .build();

            userRepository.save(existingUser);

            // JWT ์ƒ์„ฑ ๋ฐ ํ—ค๋” ์ถ”๊ฐ€
            addJwtToResponse(existingUser, response);

        }
        return existingUser;
    }


     // JWT ํ† ํฐ์„ ์ƒ์„ฑํ•˜๊ณ , ์‘๋‹ต ํ—ค๋”์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฉ”์„œ๋“œ

    private void addJwtToResponse(User user, HttpServletResponse response) {
        String createToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole());

        jwtUtil.addTokenToResponseHeader(createToken, response);
    }


    // AccessToken ์„ ์‚ฌ์šฉํ•˜์—ฌ ์†Œ์…œ ์ œ๊ณต์ž์˜ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
    private CustomOauthInfoDto fetchUserInfoFromProvider(String accessToken, String provider) throws JsonProcessingException {
        return switch (provider) {
            case "kakao" -> fetchKakaoUserInfo(accessToken);
            case "google" -> fetchGoogleUserInfo(accessToken);
            case "naver" -> fetchNaverUserInfo(accessToken);
            default -> throw new NotFoundUserException();
        };
    }


    //์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
    private CustomOauthInfoDto fetchKakaoUserInfo(String accessToken) throws JsonProcessingException {
        String url = "https://kapi.kakao.com/v2/user/me";

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        HttpEntity<String> request = new HttpEntity<>(headers);
        String responseBody = restTemplate.exchange(url, HttpMethod.GET, request, String.class).getBody();

        if (responseBody == null) {
            log.error("Kakao API response body is null.");
            throw new NotFoundUserException();
        }

        JsonNode jsonNode = new ObjectMapper().readTree(responseBody);
        String socialId = jsonNode.get("id").asText();
        String email = jsonNode.get("kakao_account").get("email").asText();
        String birthYear = jsonNode.get("kakao_account")
                .get("birthyear").asText();
        String userName = jsonNode.get("properties").get("nickname").asText();

        log.info("์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด: " + socialId + ", " + userName + ", " + birthYear + ", " + email);
        return new CustomOauthInfoDto(socialId, userName, email, birthYear);
    }

    //๊ตฌ๊ธ€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
    private CustomOauthInfoDto fetchGoogleUserInfo(String accessToken) throws JsonProcessingException {
        String url = "https://www.googleapis.com/oauth2/v3/userinfo";

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);

        HttpEntity<String> request = new HttpEntity<>(headers);
        String responseBody = restTemplate.exchange(url, HttpMethod.GET, request, String.class).getBody();

        if (responseBody == null) {
            log.error("Google API response body is null.");
            throw new NotFoundUserException();
        }

        JsonNode jsonNode = new ObjectMapper().readTree(responseBody);
        log.info(responseBody);
        String socialId = jsonNode.has("sub") ? jsonNode.get("sub").asText() : null;
        String name = jsonNode.has("name") ? jsonNode.get("name").asText() : null; // null ์ฒดํฌ
        String email = jsonNode.has("email") ? jsonNode.get("email").asText() : null; // null ์ฒดํฌ

        return CustomOauthInfoDto.addGoogleId(socialId, name, email);
    }

    // ๋„ค์ด๋ฒ„ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
    private CustomOauthInfoDto fetchNaverUserInfo(String accessToken) throws JsonProcessingException {
        String url = "https://openapi.naver.com/v1/nid/me";

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        HttpEntity<String> request = new HttpEntity<>(headers);
        String responseBody = restTemplate.exchange(url, HttpMethod.GET, request, String.class).getBody();

        if (responseBody == null) {
            log.error("Naver API response body is null.");
            throw new NotFoundUserException();
        }

        JsonNode jsonNode = new ObjectMapper().readTree(responseBody);
        JsonNode responseNode = jsonNode.get("response");
        String socialId = responseNode.has("id") ? responseNode.get("id").asText() : null; // null ์ฒดํฌ
        String email = responseNode.has("email") ? responseNode.get("email").asText() : null; // null ์ฒดํฌ
        String userName = responseNode.has("name") ? responseNode.get("name").asText() : null; // null ์ฒดํฌ
        String birthyear = responseNode.has("birthyear") ? responseNode.get("birthyear").asText() : null; // null ์ฒดํฌ

        log.info("๋„ค์ด๋ฒ„ ์‚ฌ์šฉ์ž ์ •๋ณด: " + ", " + userName + ", " + birthyear + ", " + email);
        return new CustomOauthInfoDto(socialId, userName, email, birthyear);
    }
}