์†Œ์…œ ๋กœ๊ทธ์ธ ํšŒ์› ํƒˆํ‡ด ๊ตฌํ˜„

2024. 12. 1. 16:16ยทํ”„๋กœ์ ํŠธ/NolGoat

๊ฐœ์š”

๊ธฐ์กด ํšŒ์› ํƒˆํ‡ด ๋ฐฉ์‹์€ ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํšŒ์› ์ •๋ณด๋ฅผ ์‚ญ์ œํ•˜๋Š” ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•๋งŒ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ณด๋‹ค ํ™•์‹คํ•œ ํƒˆํ‡ด ์ง„ํ–‰์„ ์œ„ํ•ด, ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž(`Google`, `Kakao`)์™€์˜ ๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ ์š”์ฒญ

Google 

Google Developers - revoke ์—์„œ ์—ฐ๊ฒฐ ํ•ด์ œ ๋ฐฉ๋ฒ•์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

โ€‹Kakao 

Kakao Developers - unlink ์—์„œ ์—ฐ๊ฒฐ ํ•ด์ œ ๋ฐฉ๋ฒ•์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Access ํ† ํฐ ๋˜๋Š” ์•ฑ ์–ด๋“œ๋ฏผ ํ‚ค(`Admin key`)๋กœ ์—ฐ๊ฒฐ ํ•ด์ œ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ ๋Š๊ธฐ ์š”์ฒญ์— ์„ฑ๊ณตํ•˜๋ฉด ํ•ด๋‹น ์‚ฌ์šฉ์ž ํšŒ์› ๋ฒˆํ˜ธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ›๊ณ , ๋กœ๊ทธ์•„์›ƒ์ด ํ•จ๊ป˜ ์ง„ํ–‰๋˜์–ด Access ํ† ํฐ๊ณผ Refresh ํ† ํฐ์ด ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

 

1๏ธโƒฃ์–ด๋“œ๋ฏผ ํ‚ค ์‚ฌ์šฉ

`Admin key`๋ฅผ ์‚ฌ์šฉํ•˜๋Š” REST API ์š”์ฒญ์€ ๋ฐ˜๋“œ์‹œ ์„œ๋ฒ„์—์„œ๋งŒ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋ฉฐ, ์†Œ์Šค ์ฝ”๋“œ์— ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, `Admin key`๋ฅผ ์‚ฌ์šฉํ•ด ์—ฐ๊ฒฐ ํ•ด์ œ ์š”์ฒญ ์‹œ, ๋Œ€์ƒ ์‚ฌ์šฉ์ž๋ฅผ ๋ช…์‹œํ•˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ•จ๊ป˜ ๋ณด๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

2๏ธโƒฃํ† ํฐ ์‚ฌ์šฉ

`Google`์—์„œ๋Š” `Admin key` ์‚ฌ์šฉ ๋ฐฉ์‹์ด ์—†์œผ๋ฏ€๋กœ, ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด, ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ API ํ˜ธ์ถœ ๋ฐฉ์‹์„ ํ†ต์ผํ–ˆ์Šต๋‹ˆ๋‹ค.

 

ํ† ํฐ ๊ฐฑ์‹  ์š”์ฒญ

Google

Google Developers - token ์—์„œ Access ํ† ํฐ ๊ฐฑ์‹  ๋ฐฉ๋ฒ•์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

โ€‹Kakao

Kakao Developers - token ์—์„œ ํ† ํฐ ๊ฐฑ์‹  ๋ฐฉ๋ฒ•์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

`Kakao`์—์„œ๋Š” `Google`๊ณผ ๋‹ฌ๋ฆฌ Refresh ํ† ํฐ์ด ์กฐ๊ฑด(์œ ํšจ ๊ธฐ๊ฐ„์ด 1๊ฐœ์›” ๋ฏธ๋งŒ) ์ถฉ์กฑ ์‹œ, Access ํ† ํฐ๊ณผ ํ•จ๊ป˜ ์žฌ๋ฐœ๊ธ‰์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ €ํฌ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ•ด๋‹น Refresh ํ† ํฐ ๊ธฐ๊ฐ„์„ 2์ฃผ๋กœ ์„ค์ •ํ–ˆ๊ธฐ์—, ์ด๋Ÿฌํ•œ ์กฐ๊ฑด์„ ๊ณ ๋ คํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค. (2์ฃผ๋กœ ์„ค์ •ํ•œ ์ด์œ ๋Š” ์•„๋ž˜์—์„œ ๋‹ค์‹œ ์–ธ๊ธ‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.)

 

Spring Cloud OpenFeign

์•ž์„œ ์–ธ๊ธ‰ํ•œ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž(`Kakao`, `Google`)์˜ API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด `Spring Cloud OpenFeign`์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. OpenFeign์€ ์„ ์–ธํ˜• HTTP ํด๋ผ์ด์–ธํŠธ๋กœ, REST API ํ˜ธ์ถœ ์ฝ”๋“œ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ ๊ฐ€๋Šฅํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.

 

๊ธฐ๋ณธ ์„ค์ •

  • ์˜์กด์„ฑ ์ถ”๊ฐ€
dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.1"
    }
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

 

  • ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”
@EnableFeignClients
@SpringBootApplication
public class Application {

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

}

 

API ํ˜ธ์ถœ ์ฝ”๋“œ

๐ŸŒ Google

@FeignClient(name = "googleClient", url = "https://oauth2.googleapis.com")
public interface GoogleClient {

    // Refresh ํ† ํฐ์„ ์‚ฌ์šฉํ•œ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์š”์ฒญ
    @PostMapping("/token")
    TokenResponse reissueToken(
            @RequestParam("grant_type") String grantType, // "refresh_token"์œผ๋กœ ๊ณ ์ •
            @RequestParam("client_id") String clientId,
            @RequestParam("refresh_token") String refreshToken,
            @RequestParam("client_secret") String clientSecret
    );

    // ์—ฐ๊ฒฐ ํ•ด์ œ ์š”์ฒญ
    // Refresh, Access ํ† ํฐ ๋‘˜ ๋‹ค ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    @PostMapping("/revoke")
    void unlink(@RequestParam("token") String accessToken);
}

 

๐ŸŒ Kakao

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

    // Access ํ† ํฐ์„ ์‚ฌ์šฉํ•œ ์—ฐ๊ฒฐ ํ•ด์ œ ์š”์ฒญ
    @PostMapping("/v1/user/unlink")
    UnlinkResponse unlink(@RequestHeader("Authorization") String accessToken);
}
@FeignClient(name = "kakaoAuthClient", url = "https://kauth.kakao.com")
public interface KakaoAuthClient {

    // Refresh ํ† ํฐ์„ ์‚ฌ์šฉํ•œ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์š”์ฒญ
    @PostMapping("/oauth/token")
    TokenResponse reissueToken(
            @RequestParam("grant_type") String grantType, // "refresh_token"์œผ๋กœ ๊ณ ์ •
            @RequestParam("client_id") String clientId,
            @RequestParam("refresh_token") String refreshToken,
            @RequestParam("client_secret") String clientSecret
    );
}

 

Service๋กœ ํ†ตํ•ฉ

`Google`๊ณผ `Kakao`์˜ API ํ˜ธ์ถœ ๋กœ์ง์„ ํ•˜๋‚˜์˜ Service๋กœ ํ†ตํ•ฉํ•˜์—ฌ, ๊ฐ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž์— ๋Œ€ํ•œ `์—ฐ๊ฒฐ ํ•ด์ œ`, `ํ† ํฐ ๊ฐฑ์‹ `์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

@Slf4j
@RequiredArgsConstructor
@Service
public class SocialClientService {

    ...

    // Google ํ† ํฐ ๊ฐฑ์‹  ์š”์ฒญ
    public TokenResponse reissueGoogleToken(String refreshToken) {
        return googleClient.reissueToken(
                GRANT_TYPE_REFRESH_TOKEN,
                googleClientId,
                refreshToken,
                googleClientSecret
        );
    }

    // Google ์—ฐ๊ฒฐ ํ•ด์ œ ์š”์ฒญ
    public void unlinkGoogle(String accessToken) {
        googleClient.unlink(accessToken);
        log.info("Google Unlink");
    }

    // Kakao ํ† ํฐ ๊ฐฑ์‹  ์š”์ฒญ
    public TokenResponse reissueKakaoToken(String refreshToken) {
        return kakaoAuthClient.reissueToken(
                GRANT_TYPE_REFRESH_TOKEN,
                kakaoClientId,
                refreshToken,
                kakaoClientSecret
        );
    }

    // Kakao ์—ฐ๊ฒฐ ํ•ด์ œ ์š”์ฒญ
    public void unlinkKakao(String accessToken) {
        UnlinkResponse response = kakaoApiClient.unlink(accessToken);
        log.info("Kakao Unlink ํšŒ์›๋ฒˆํ˜ธ : {}", response.getId());
    }
}

 

OAuth2 ์ธ์ฆ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

OAuth2AuthorizedClientService ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

`OAuth2AuthorizedClientService`์˜ ๊ธฐ๋ณธ ๊ตฌํ˜„์ฒด๋Š” `InMemoryOAuth2AuthorizedClientService`๋กœ, ์ธ์ฆ ์„œ๋ฒ„์—์„œ ๋ฐœ๊ธ‰๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š”๋ฐ, ์ด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  • ํ•œ์ •๋œ ๋ฉ”๋ชจ๋ฆฌ ๋ฆฌ์†Œ์Šค
    • ์„œ๋ฒ„์˜ ๋ฉ”๋ชจ๋ฆฌ ๊ณต๊ฐ„์€ ํ•œ์ •์ ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ˆ˜๊ฐ€ ์ฆ๊ฐ€ํ•˜๋ฉด, ๋ฉ”๋ชจ๋ฆฌ ๋ถ€ํ•˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 
  • ์„œ๋ฒ„ ํ™•์žฅ์„ฑ ๋ฌธ์ œ
    • ์„œ๋ฒ„๋ฅผ ํ™•์žฅํ•  ๊ฒฝ์šฐ, ๊ฐ ์„œ๋ฒ„๋Š” ๋…๋ฆฝ์ ์ธ ๋ฉ”๋ชจ๋ฆฌ ๊ณต๊ฐ„์„ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์„œ๋ฒ„ ๊ฐ„ ์ธ์ฆ ์ •๋ณด์˜ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋‹จ๊ณ„์—์„œ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋“ค์ด ์ง์ ‘์ ์ธ ๊ณ ๋ ค ๋Œ€์ƒ์€ ์•„๋‹ˆ์ง€๋งŒ, ํ–ฅํ›„ ์‹œ์Šคํ…œ์˜ ํ™•์žฅ์„ฑ๊ณผ `TTL(Time To Live)` ์„ค์ •์„ ํ™œ์šฉํ•œ ํšจ์œจ์ ์ธ ํ† ํฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด `Redis`๋ฅผ ํ†ตํ•œ ํ† ํฐ ์ €์žฅ ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

@Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
    String loginId = principal.getName();
    OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken();
    OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

    // Refresh ํ† ํฐ ์ €์žฅ
    String key = RedisTokenService.OAUTH2_REFRESH_TOKEN_KEY_PREFIX + loginId;
    redisTokenService.saveToken(
            key,
            refreshToken.getTokenValue(),
            Date.from(
                    LocalDateTime.now()
                            .plusWeeks(2)
                            .atZone(ZoneId.systemDefault())
                            .toInstant()
            )
    );

    // Access ํ† ํฐ ์ €์žฅ
    key = RedisTokenService.OAUTH2_ACCESS_TOKEN_KEY_PREFIX + loginId;
    redisTokenService.saveToken(
            key,
            accessToken.getTokenValue(),
            Date.from(accessToken.getExpiresAt())
    );
}

 

๐Ÿ’ก Refresh ํ† ํฐ ์œ ํšจ ๊ธฐ๊ฐ„์„ 2์ฃผ๋กœ ์„ค์ •ํ•œ ์ด์œ 

์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž(`Google`, `Kakao`)๊ฐ€ ์ œ๊ณตํ•˜๋Š” Refresh ํ† ํฐ์˜ ์œ ํšจ ๊ธฐ๊ฐ„์€ ์ตœ์†Œ 2๊ฐœ์›” ์ด์ƒ์ด์ง€๋งŒ, ์žฌ๋กœ๊ทธ์ธ ์‹œ ์ œ๊ณต์ž๊ฐ€ ์ƒˆ๋กœ์šด Refresh ํ† ํฐ์„ ๋‹ค์‹œ ๋ฐœ๊ธ‰ํ•ด ์ค๋‹ˆ๋‹ค. ์ด๋ฅผ ๊ณ ๋ คํ•ด, ์ €ํฌ ์„œ๋ฒ„์—์„œ ๋ฐœ๊ธ‰ํ•˜๋Š” Refresh ํ† ํฐ(`JWT`)๊ณผ ๋™์ผํ•œ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž์˜ Refresh ํ† ํฐ ์œ ํšจ ๊ธฐ๊ฐ„๋„ ๋™์ผํ•˜๊ฒŒ 2์ฃผ๋กœ ์„ค์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

OAuth2AuthorizationRequestResolver ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

`Google`์—์„œ๋Š” ๋กœ๊ทธ์ธ์„ ํ†ตํ•ด Refresh ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์œผ๋ ค๋ฉด ์ถ”๊ฐ€์ ์ธ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  Refresh ํ† ํฐ ์—†์ด๋„ ์—ฐ๊ฒฐ ํ•ด์ œ ์š”์ฒญ์€ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, Access ํ† ํฐ์˜ ์œ ํšจ๊ธฐ๊ฐ„์ด ์งง๊ธฐ ๋•Œ๋ฌธ์—, ์—ฐ๊ฒฐ ํ•ด์ œ ์š”์ฒญ ์‹œ, ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์•ผ ํ•˜๋Š” ๋ถˆํŽธํ•จ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, `OAuth2AuthorizationRequestResolver`๋ฅผ ์ปค์Šค๋งˆ์ด์ง•ํ•˜์—ฌ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž๊ฐ€ `Google`์ธ ๊ฒฝ์šฐ ํ•„์š”ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋™์ ์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด Refresh ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์•„ ์ธ์ฆ ์ƒํƒœ๋ฅผ ์ง€์†์ ์œผ๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

ํ•„์š”ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •

  • access_type=offline
  • prompt=consent
    • ํ•ญ์ƒ ๋™์˜ ํ™”๋ฉด์„ ํ‘œ์‹œํ•˜๊ณ , ๋ช…์‹œ์ ์ธ ๋™์˜๋ฅผ ๊ตฌํ•ด์•ผ Refresh ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
    return customizeAuthorizationRequest(defaultResolver.resolve(request));
}

@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
    return customizeAuthorizationRequest(defaultResolver.resolve(request, clientRegistrationId));
}

private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {
    if (authorizationRequest == null) {
        return null;
    }

    Object registrationId = authorizationRequest.getAttribute("registration_id");

    // Google์ผ ๋•Œ๋งŒ Refresh ํ† ํฐ์„ ๋ฐ›๊ธฐ ์œ„ํ•ด, ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
    if (registrationId.equals("google")) {
        return OAuth2AuthorizationRequest.from(authorizationRequest)
                .additionalParameters(params -> {
                    params.put("prompt", "consent");
                    params.put("access_type", "offline");
                })
                .build();
    }

    // ๋‹ค๋ฅธ ์ œ๊ณต์ž๋Š” ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
    return authorizationRequest;
}

 

ํšŒ์› ํƒˆํ‡ด ์ฒ˜๋ฆฌ

@Transactional
public void deleteUserByLoginId(String loginId) {
    String refreshKey = RedisTokenService.OAUTH2_REFRESH_TOKEN_KEY_PREFIX + loginId;
    String accessKey = RedisTokenService.OAUTH2_ACCESS_TOKEN_KEY_PREFIX + loginId;
    String accessToken = redisTokenService.getToken(accessKey);

    String registrationId = loginId.split(PROVIDER_ID_DELIMITER)[0];
    if (registrationId.equals(KAKAO)) {
        // Access ํ† ํฐ์ด null์ด๋ฉด Refresh ํ† ํฐ์„ ์ด์šฉํ•ด ์žฌ๋ฐœ๊ธ‰
        if (accessToken == null) {
            TokenResponse tokenResponse = socialClientService.reissueKakaoToken(redisTokenService.getToken(refreshKey));

            // ํšŒ์› ํƒˆํ‡ด๋ฅผ ์œ„ํ•œ ์žฌ๋ฐœ๊ธ‰์ด๊ธฐ ๋•Œ๋ฌธ์—, Redis์— ์ €์žฅํ•˜์ง€ ์•Š์Œ
            accessToken = tokenResponse.getAccess_token();
        }
        socialClientService.unlinkKakao(BEARER_PREFIX + accessToken);
    }
    if (registrationId.equals(GOOGLE)) {
        if (accessToken == null) {
            TokenResponse tokenResponse = socialClientService.reissueGoogleToken(redisTokenService.getToken(refreshKey));
            accessToken = tokenResponse.getAccess_token();
        }
        socialClientService.unlinkGoogle(accessToken);
    }

    // Refresh ํ† ํฐ์€ ๋‚จ์•„์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์‚ญ์ œ
    redisTokenService.deleteToken(refreshKey);

    // ์œ ์ €์˜ isDeleted๋ฅผ true๋กœ ๋ณ€๊ฒฝ
    User user = userRepository.findByLoginId(loginId)
            .orElseThrow(() -> new ApiException(USER_NOT_FOUND));
    user.delete();
}
  1. ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž๋ณ„ ํ† ํฐ ์ฒ˜๋ฆฌ
    • ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž(`Google`, `Kakao`)์— ๋”ฐ๋ผ Redis์—์„œ Access ํ† ํฐ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค
    • Access ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์–ด Redis์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, Refresh ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒˆ๋กœ์šด Access ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์Šต๋‹ˆ๋‹ค.
  2. ์†Œ์…œ ๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ
    • ๋ฐœ๊ธ‰๋ฐ›์€ Access ํ† ํฐ์„ ์‚ฌ์šฉํ•ด ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž์—๊ฒŒ ๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ๋ฅผ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.
  3. ํ† ํฐ ์ •๋ฆฌ
    • Redis์— ๋‚จ์•„์žˆ๋Š” Refresh ํ† ํฐ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
  4. ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ 
    • MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ `isDeleted` ํ•„๋“œ๋ฅผ `true`๋กœ ๋ฐ”๊พธ์–ด, ์‚ญ์ œ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. 

 

ํšŒ์› ํƒˆํ‡ด ํ›„, ๋‹ค์‹œ ๊ฐ€์ž…

ํšŒ์› ํƒˆํ‡ด ํ›„ ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•  ๊ฒฝ์šฐ, ๋™์ผํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ `๊ณ ์œ  ID`๋Š” ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๊ณ„์ • ์—ฐ๊ฒฐ์„ ํ•ด์ œํ•˜๊ณ  ๋‹ค์‹œ ์—ฐ๊ฒฐํ•ด๋„ `๊ณ ์œ  ID`๋Š” ๋ฐ”๋€Œ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํšŒ์› ํƒˆํ‡ด ํ›„ ์žฌ ๋กœ๊ทธ์ธ ์‹œ, ๊ธฐ์กด ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋ฉฐ `nickname`๊ณผ `profileImage` ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ณ , `isDeleted` ์ƒํƒœ๋ฅผ `false`๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.
(ํšŒ์› ํƒˆํ‡ด ์‹œ, ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ–ˆ๋˜ ๋ฆฌ๋ทฐ, ๋ถ๋งˆํฌ ๋“ฑ๊ณผ ๊ฐ™์€ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•๊ณผ ๋‹ค์‹œ ๊ฐ€์ž…ํ•  ๊ฒฝ์šฐ์˜ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์€ ์ถ”ํ›„์— ์ถ”๊ฐ€๋กœ ๋…ผ์˜ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.)

@Transactional
public OAuth2User processOAuth2User(OAuth2Response oAuth2Response) {
    String uniqueProviderId = oAuth2Response.getProvider() + PROVIDER_ID_DELIMITER + oAuth2Response.getProviderId();
    String nickname = oAuth2Response.getNickname();
    String profileImage = oAuth2Response.getProfileImage();

    if (!userRepository.existsByLoginId(uniqueProviderId)) {
        userRepository.save(
                new User(
                        uniqueProviderId,
                        null,
                        nickname,
                        profileImage,
                        null,
                        null
                )
        );
        OAuth2UserDto oAuth2UserDto = new OAuth2UserDto(uniqueProviderId);
        return new OAuth2UserImpl(oAuth2UserDto);
    }

    User user = userRepository.findByLoginId(uniqueProviderId)
            .orElseThrow(() -> new ApiException(USER_NOT_FOUND));
    if (user.isDeleted()) {
        user.reactivate();
    }
    user.update(
            null,
            nickname,
            profileImage
    );

    OAuth2UserDto oAuth2UserDto = new OAuth2UserDto(uniqueProviderId);

    return new OAuth2UserImpl(oAuth2UserDto);
}

 

๊ฒฐ๊ณผ

ํšŒ์› ํƒˆํ‡ด ์‹œ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ์˜ ์‚ญ์ œ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž ์ธก์—์„œ๋„ ๊ณ„์ • ์—ฐ๊ฒฐ์„ ํ•ด์ œํ•˜์—ฌ ๋ณด๋‹ค ์™„์„ฑ๋„ ์žˆ๋Š” ํšŒ์› ํƒˆํ‡ด ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • Redis๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž๊ฐ€ ์ „๋‹ฌํ•ด ์ฃผ๋Š” ํ† ํฐ์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌ
  • OpenFeign์„ ํ†ตํ•œ REST API ์š”์ฒญ
  • OAuth2 ์ธ์ฆ ๊ด€๋ จ ์ธํ„ฐํŽ˜์ด์Šค ์ปค์Šคํ…€ ๊ตฌํ˜„
  • ๊ณ ์œ  ID์™€ ์†Œํ”„ํŠธ ๋”œ๋ฆฌํŠธ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํšŒ์› ํƒˆํ‡ด ํ›„ ์žฌ๊ฐ€์ž… ์‹œ, ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์œ ์ง€

๋˜ํ•œ, ์•ž์„œ ์–ธ๊ธ‰ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ, ํƒˆํ‡ด ์‹œ ์‚ฌ์šฉ์ž์™€ ์—ฐ๊ด€๋œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ๊ฐ€์ž… ์‹œ์˜ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•ด์„œ๋Š” ์ถ”๊ฐ€์ ์œผ๋กœ ๋…ผ์˜ ํ›„ ๊ตฌํ˜„ํ•  ๊ณ„ํš์ž…๋‹ˆ๋‹ค.

 

์ฐธ๊ณ 

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

https://developers.google.com/identity/protocols/oauth2/web-server?hl=ko

https://mangkyu.tistory.com/278

'ํ”„๋กœ์ ํŠธ > NolGoat' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

์นด์นด์˜ค ๋กœ๊ทธ์ธ ๋™์˜ ํ•ญ๋ชฉ ๋ฌธ์ œ ํ•ด๊ฒฐ  (1) 2024.12.11
๋ณด์•ˆ ์ทจ์•ฝ์  ํ•ด๊ฒฐ์„ ์œ„ํ•œ AWS S3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ณ€๊ฒฝ  (0) 2024.12.07
ํšจ์œจ์ ์ธ ํ† ํฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Redis ๋„์ž…  (1) 2024.11.29
์ธ์ฆ ๋ณด์•ˆ ๊ฐ•ํ™”  (0) 2024.11.29
์ธ์ฆ์ด ํ•„์š”ํ•œ URL ๊ณตํ†ต ๊ด€๋ฆฌ  (0) 2024.11.27
'ํ”„๋กœ์ ํŠธ/NolGoat' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๋™์˜ ํ•ญ๋ชฉ ๋ฌธ์ œ ํ•ด๊ฒฐ
  • ๋ณด์•ˆ ์ทจ์•ฝ์  ํ•ด๊ฒฐ์„ ์œ„ํ•œ AWS S3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ณ€๊ฒฝ
  • ํšจ์œจ์ ์ธ ํ† ํฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Redis ๋„์ž…
  • ์ธ์ฆ ๋ณด์•ˆ ๊ฐ•ํ™”
yongh๐Ÿ™‚
yongh๐Ÿ™‚
yongh-dev ๋‹˜์˜ ๋ธ”๋กœ๊ทธ ์ž…๋‹ˆ๋‹ค.
  • yongh๐Ÿ™‚
    ๊ฐœ๋ฐœ ๊ธฐ๋ก
    yongh๐Ÿ™‚
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (9)
      • ํ”„๋กœ์ ํŠธ (9)
        • NolGoat (9)
  • ์ตœ๊ทผ ๊ธ€

  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.2
yongh๐Ÿ™‚
์†Œ์…œ ๋กœ๊ทธ์ธ ํšŒ์› ํƒˆํ‡ด ๊ตฌํ˜„
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”