일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 파이썬
- 티스토리챌린지
- githubactions
- 토이프로젝트
- 재갱신
- 프로그래머스
- Spring
- 도커
- 소셜로그인
- 트랜잭션
- 액세스토큰
- java
- JIRA
- 스프링부트
- 백준
- 메시지
- springdataredis
- oauth2
- docker
- 국제화
- springsecurity
- 데이터베이스
- 오블완
- 리프레시토큰
- 스프링시큐리티
- yaml-resource-bundle
- springsecurityoauth2client
- AWS
- CI/CD
- 스프링
- Today
- Total
땃쥐네
[토이프로젝트] 게시판 시스템(board-system) 22. 스프링 시큐리티 OAuth2 Client 를 사용한 소셜 로그인 (1) 설정 준비 본문
[토이프로젝트] 게시판 시스템(board-system) 22. 스프링 시큐리티 OAuth2 Client 를 사용한 소셜 로그인 (1) 설정 준비
ttasjwi 2024. 11. 28. 15:05
간단하게 서비스의 사용자들의 편의를 위해 소셜 로그인 기능을 추가해보겠습니다.
다만 소셜 로그인 기능은 구현과정이 약간 길어지다보니 글을 2-3개 정도로 쪼개서 진행해보겠습니다.
양해 부탁드립니다.
1. OAuth2 인증 방식 흐름
예를 들어 구글 로그인을 생각해볼게요.
여러분이 여태 다른 서비스를 이용해보시면서 그 서비스 사용시 Google 로그인을 하며
id 와 pw를 직접 전달해본 적이 있나요? 보통 서비스는 그런식으로 구성되어 있지 않습니다.
해당 서비스를 믿고 id, pw 를 그대로 넘겼다가 그 서비스가 무슨 짓을 할지 모르는데
그걸 냉큼 넘겨줘선 안 됩니다.
이런 신뢰성 문제를 해결하기 위해 도입된 것이 OAuth2 입니다.
우리 서비스가 최종사용자의 승인을 얻고, 제3의 서비스로부터 최종사용자의 정보를 얻어오는 과정입니다.
사실 OAuth2 표준에는 몇 가지 인가 유형이 존재합니다만,
여기서는 가장 대표적인 승인 코드 인증 유형(authorization_code grant type) 을 기준으로 설명하겠습니다.
최종 사용자(Resource Owner),
사용자를 대신하여 자원을 얻어와 사용하는 서비스(OAuth2 Client),
리소스를 제공하는 서비스에서 자원을 보유하고 있는 리소스 서버(Resource Server),
자격증명 사용에 대한 권한, 자격증명을 관리 인가 서버(Authorization Server)
네가지 주체가 협력을 해야합니다.
아주 짧게 함축해서 요약하면
- Resource Owner가 OAuth2 Client에게 소셜로그인 요청을 합니다.
- Resource Owner가 Authorization Server에게 OAuth2 Client의 Resource Owner사용자 정보 사용을 승인한 뒤 이 승인 결과물(code) 을 OAuth2Client 에게 다시 전달합니다.
- OAuth2 Client는 위에서 가져온 승인 결과물과 여러가지 자격증명을 가지고 Authorization Server에게 요청하여, 사용자 정보를 획득 할 수 있는 액세스 토큰을 얻어옵니다.
- 이 액세스토큰을 사용자 정보를 보관한 Resource Server에게 전달하여 Resource Owner의 사용자 정보를 획득해와야합니다.
그런데 이렇게 말씀드리면 너무 많은 일들이 생략됐고,이해하기 힘든게 당연합니다.
과정을 하나씩 하나씩 쪼개가면서, 기능을 구현해나가겠습니다.
2. 바퀴를 재발명하지 말자. 스프링 시큐리티 OAuth2 Client 사용!
'바퀴를 재발명하지 마라' 라는 격언이 있습니다.
OAuth2의 모든 과정을 개발자가 직접 구현하는 것은 매우 귀찮고 실수할 여지가 많습니다.
학습 목적으로 좋을 수 있기야합니다. 그런데 실무에서도 OAuth2의 모든 기능을 자체적으로 구현하는게 맞나? 라고 생각하면 그건 아닐 것 같다는 생각이 듭니다. 이 기능의 모든 세세한걸 직접 개발한다면, 개발/테스트만으로 몇 주를 날려버릴 수 있습니다.
서비스 기업 입장에서 소셜 로그인 기능은 독창적이고 차별적인 기능이 되기보다 서비스의 접근성을 향상시키기 위한 부가적인 기능에 불과합니다. 서비스 기업의 개발자는 서비스에서 다루는 핵심 도메인 기능 구현을 우선시해야하고, 서비스 운영을 위한 다른 것들에 좀 더 관심을 가지는게 합리적입니다.
소셜로그인 기능에 대한 솔루션은 이미 오랜 세월을 거치면서 정형화되었고, 이를 라이브러리/프레임워크화 시킨 것들이 많습니다. 특히 Spring Security OAuth2 Client 는 그 중 하나입니다. Spring Security OAuth2 Client 는 OAuth2 Client 의 기능을 편리하게 수행할 수 있도록 기능을 제공해주고 있으니 이를 가져와서 약간만 커스터마이징 해줘서 빠르게 구현하면 됩니다. Spring Security OAuth2 Client 는 한번 학습해두면 다른 백엔드 개발자들과 기술 사용법 공유가 충분히 됐다면 다시 다른 스프링 프로젝트를 만들 때 기존 틀을 거의 유지하면서 재사용할 수 있는 장점도 있습니다.
그래서 저는 이번 소셜로그인 기능(이번 글에서 다루는 리다이렉트도 그렇고...)은 대부분 스프링 시큐리티에서 담당하도록 할 예정입니다. 스프링 시큐리티가 제공해주는 기능을 사용하면서, 중간과정에서 어떤 흐름으로 작업들이 이루어지는 지 흐름을 이해하고 문제가 터졌을 때 그 부분만 손봐주는 것으로 소셜로그인 기능 유지보수는 충분할 것 같습니다.
3. OAuth2 클라이언트 등록
제 서비스에서는 구글, 카카오, 네이버에 대한 로그인 기능을 지원할 계획입니다.
GitHub 과 페이스북 같은 다른 서비스들도 있기야 하겠지만
한국에서 가장 보편적으로 쓰이는 소셜 서비스를 3개 꼽으면 저는 구글, 카카오, 네이버라고 생각해요.
구글, 카카오, 네이버 로그인 기능을 사용할 수 있도록 이들 서비스에게 우리를 OAuth2 Client 로서 등록해보겠습니다.
3.1 Google Client 등록
구글 클라우드 플랫폼에 페이지에 가시면 프로젝트를 생성할 수 있습니다.
저는 board-system 이름의 프로젝트를 만들었는데(이미지에서는 설명을 위해 새 프로젝트를 만듬)
편하신 이름으로 지으시면 돼요.
생성에는 약간의 시간이 소요될 수 있습니다.
막 이렇게 프로젝트 만들어도 나중에 삭제할 수 있으니까 얼마든지 편하게 프로젝트를 만드시면 됩니다.
프로젝트 선택에 들어가셔서
새로 만든 프로젝트를 클릭합니다.
이후 좌측 상단의 탐색 메뉴를 클릭하면, API 및 서비스 > OAuth2 동의화면에 들어가시면
OAuth2 동의화면 관리를 설정할 수 있는데요.
외부 사용자들이 사용할 수 있도록 User Type 을 설정합니다.
동의화면에 띄울 앱 이름,
문제가 발생하였을 때 메일을 누구한테 보내야할 지 메일을 설정할 수 있습니다.
또, 동의화면에 띄울 로고도 설정할 수 있어요.
앱 도메인
우리 서비스 기업(사용자)이 운영하는 웹사이트 주소, 도메인을 의미합니다.
최종사용자 입장에선 주로 프론트 엔드 화면을 통해 서비스를 이용하므로 웹 사이트 주소를 기입하면 됩니다.
예를 들어 사이트 주소가 "http://jello.com" 을 넣으면 됩니다.
승인된 도메인
OAuth2 인증 과정에서 허용되는 도메인을 의미합니다.
백엔드와 프론트로 분리된 구조일 때 백엔드 주소가 만약 "http://api.jello.com" 이라 하면, 여기에 등록을 별도로 해줘야합니다.
지금 제 프로젝트는 별도의 도메인에 호스팅을 하고 있지 않기 때문에 생략하겠습니다.
범위 추가 또는 삭제를 통해 사용자에게 승인하고자 하는 데이터의 범위를 지정하게 합니다.
OAuth2 표준 용어로 이것을 "scope" 라고 합니다.
지금 프로젝트에서 저는 사용자의 기본 식별자, email 정도만 필요합니다.
추가적인 정보(닉네임, 프로필 이미지 활용)가 더 필요하다면 profile 정도를 더 거는 것도 괜찮습니다.
openid connect 프로토콜을 사용하면서 사용자 정보를 가져오기 위해 openid, email 정도만 허용하겠습니다.
테스트 사용자로는 제 이메일을 지정하겠습니다.
이렇게 하면 테스트 기간동안에는 제 이메일만 동의화면을 이용할 수 있어요.
여기까지 설정하면 이제 동의화면 기능을 사용할 수 있습니다.
API 및 서비스에 들어가면 OAuth2 Client Id를 등록할 수 있습니다.
구글 승인화면 요청 시 구글에서 어디로 리다이렉트 시킬 지 redirect_uri 파라미터를 통해 전달해야하는데요.
여기다 사용가능한 redirect_uri 를 지정해야합니다.
악의적인 사용자들이 엉뚱한 사이트로 리다이렉트 시키도록 redirect_uri 를 전달할 수도 있다보니 이를 막도록 사용가능한 redirect_uri 를 등록해야합니다.
저는 리액트(또는 Next.js) 서버를 띄운다는 가정하에, localhost 3000번 포트에 위치한 /auth/login/oauth2/code/google 페이지로 리다이렉트를 허용하기로 했어요.
만약 나중에 프론트엔드 서비스를 배포하고, 프론트엔드 페이지로 리다이렉트 시켜야한다면
여기에 승인된 redirect_uri로 프로덕션 프론트엔드 페이지를 추가해야합니다.(나중에 수정 가능해요.)
이렇게 하고나서 클라이언트 Id, 클라이언트 Secret을 발급받을 수 있습니다.
이 Client Id는 노출되어도 상관없는 값인데 Secret은 절대 노출되선 안 됩니다.
혹시 노출되었다 생각되면 바로 만료시키고 새로 Secret 을 추가해주면 돼요.
이 값들을 안전한 곳에 잘 보관해둡시다.
3.2 Kakao Client 등록
카카오에서는 개발자들을 위해 Kakao Developers 페이지 통해 여러가지 기능들을 지원하고 있습니다.
애플리케이션 추가를 통해 애플리케이션을 추가하셔야합니다.
사용자에게 보여질 앱 아이콘, 이름을 설정할 수 있습니다.
추가된 애플리케이션에 들어갑니다.
좌측의 카카오 로그인으로 가서
카카오 로그인 API 및 OpenId Connect 를 활성화합니다.
그리고 하단에 보면 허용할 Redirect URI 를 등록해둘 수 있습니다.
Google 때처럼 localhost:3000 에 별도의 프론트엔드 서버가 있다고 가정하고 리다이렉트 Uri 를 지정합니다.
승인 후에는 이 페이지로 리다이렉트 되도록 할거에요.
좌측의 동의항목에 들어가보면
동의항목 목록이 뜨는데 이메일이 기본 허용범위에 없습니다.
개인정보 동의항목 심사 신청에 들어가 심사신청을 해야합니다.
비즈앱으로 전환하라고 하네요... 근데 이 비즈앱 전환을 위해서는 앱 아이콘이 필요하다고 합니다.
앱 아이콘은 좌측의 일반에 들어가서 설정할 수 있습니다.
이제 비즈앱 신청이 가능해지는데
저는 사업자가 아닌 개인 개발자이므로 개인 개발자 비즈 앱 전환을 하겠습니다.
이메일 필수 동의를 목적으로 하므로 이메일 필수 동의를 선택합니다.
동의항목 페이지에 가보면 이제 이메일 동의 여부를 설정할 수 있어요.
이메일 획득 필수 동의를 요구하도록 합니다.
이제 이메일 동의가 활성화됐으니 openid connect 방식을 통해, id 토큰에 사용자 email 이 포함됩니다.
scope 에 위에서 설정한 account_email 이 포함되어야 합니다.
openid connect 가 어떤 것인지는 실제 이후 기능을 구현해보면서 간단하게 확인해보겠습니다.
일단 동의화면 기능을 설정했으나 OAuth2 Client 설정이 필요합니다.
client_id 획득이 필요한데 여기서 REST API 키를 client_id 로 삼으면 됩니다.
좌측의 보안에 들어가서
client Secret 을 활성화하고, 값을 어디 잘 보관해둡니다.
3.3 Naver Client 등록
이번에는 네이버 동의화면을 구성해보겠습니다.
네이버 개발자 센터에 접속합니다.
상단에서 애플리케이션 등록을 클릭합니다.
휴대폰, 이메일 등록 절차를 거쳐야 애플리케이션을 등록할 수 있습니다.
애플리케이션 이름을 적절히 짓고, 사용 API는 네이버 로그인을 설정합니다.
연락처 이메일 주소 획득 동의를 활성화 합니다.
여기서 가져오는 이메일은 사용자의 {네이버아이디}@naver.com 이 아니라 사용자가 기본으로 설정해둔 연락처이메일이 우선적으로 제공된다고 되어있습니다.
사용자가 연락처 이메일을 gmail로 설정했다면 gmail 값을 가져올 수도 있어요.
여기서 서비스 환경도 설정하는데 PC 웹을 선택합니다.
서비스 URL 은 실제 서비스의 도메인을 의미하는 것 같고
Call back URL 은 허용할 리다이렉트 URL 을 설정하는 부분입니다.
마찬가지로 로컬 PC의 3000번 포트에 프론트엔드 서버가 있다고 가정하고 URL 을 지정했습니다.
이제 네이버의 로그인 API 를 쓸 수 있습니다.
애플리케이션 정보쪽에서 client_id, client_secret 을 가져다가 쓸 수 있습니다.
4. 스프링부트 시큐리티 OAuth2 Client 의존성 등록 및 자동구성
Spring Security OAuth2 Client 기능을 사용하기 위해 의존성을 추가하겠습니다.
저는 buildSrc 에 의존성 이름들을 관리해두는데요.
// spring
SPRING_BOOT_STARTER(groupId = "org.springframework.boot", artifactId = "spring-boot-starter"),
SPRING_BOOT_WEB(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-web"),
SPRING_BOOT_DATA_JPA(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-data-jpa"),
SPRING_BOOT_DATA_REDIS(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-data-redis"),
SPRING_BOOT_SECURITY(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-security"),
SPRING_BOOT_OAUTH2_CLIENT(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-oauth2-client"),
SPRING_BOOT_MAIL(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-mail"),
SPRING_BOOT_TEST(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-test"),
SPRING_SECURITY_OAUTH2_CLIENT(groupId = "org.springframework.security", artifactId="spring-security-oauth2-client"),
Dependencies enum 쪽에 스프링 시큐리티 OAuth2 관련 라이브러리 정보들을 추가했습니다
1. SPRING_BOOT_OAUTH2_CLIENT
스프링 시큐리티 OAuth2 를 비롯한 여러가지 라이브러리, 자동구성이 포함됩니다.
이전에 사용했던 spring-security-oauth2-jose 도 여기에 포함됩니다.
2. SPRING_SECURITY_OAUTH2_CLIENT
스프링 시큐리티 OAuth2 의 기본 기능이 포함됩니다.
REDIS 쪽에서 사용할 일이 있어서 새로 넣어뒀습니다.
external-security 모듈의 build.gradle.kts 에서
dependencies {
implementation(Dependencies.SPRING_BOOT_SECURITY.fullName)
implementation(Dependencies.SPRING_BOOT_WEB.fullName)
implementation(Dependencies.SPRING_BOOT_OAUTH2_CLIENT.fullName)
implementation(Dependencies.JACKSON_DATETIME.fullName)
implementation(Dependencies.KOTLIN_JACKSON.fullName)
implementation(project(":board-system-api:api-core"))
implementation(project(":board-system-application:application-auth"))
implementation(project(":board-system-domain:domain-core"))
implementation(project(":board-system-domain:domain-member"))
implementation(project(":board-system-domain:domain-auth"))
testImplementation(testFixtures(project(":board-system-application:application-auth")))
testImplementation(testFixtures(project(":board-system-domain:domain-core")))
testImplementation(testFixtures(project(":board-system-domain:domain-member")))
testImplementation(testFixtures(project(":board-system-domain:domain-auth")))
}
스프링 부트 OAuth2 Client,
kotlin jackson 의존성, jackson 사용시 java8 이후 필요한 datetime 모듈을 추가했습니다.
스프링 부트 OAuth2 Client 의존성을 추가하면 여러가지 자동구성이 포함됩니다.
스프릥 부트 시큐리티쪽은 자동구성 한번으로 기존에 작동하던 기능들에 영향을 줄 수 있는 것들이 있다보니 주의깊게 자동구성을 확인해보셔야합니다.
크게 보면 OAuth2ClientAutoConfiguration 에 의한 자동구성, @EnableWebSecurity 쪽에서의 추가 자동구성설정 활성화 현상이 일어납니다.
전부 다 설명하기엔 길어져서 모든 것을 설명하는 것은 생략하고 중요한 부분만 보겠습니다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class OAuth2SecurityFilterChainConfiguration {
@Bean
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.oauth2Login(withDefaults());
http.oauth2Client(withDefaults());
return http.build();
}
}
OAuth2WebSecurityConfiguration 에 가보면 자동 구성에 의해서 디폴트 시큐리티 필터체인이 등록됩니다.
저는 이전글에서 커스텀 필터체인을 등록했기 때문에 이 필터체인은 등록되지 않습니다.
혹시 새로운 프로젝트에 스프링부트 Oauth2 Client 의존성등록을 했는데
뭔가 내가 의도치 않은 필터가 작동한다? 그러면 이쪽을 참고하시길 바랍니다.
@ConfigurationProperties(prefix = "spring.security.oauth2.client")
public class OAuth2ClientProperties implements InitializingBean {
/**
* OAuth provider details.
*/
private final Map<String, Provider> provider = new HashMap<>();
/**
* OAuth client registrations.
*/
private final Map<String, Registration> registration = new HashMap<>();
OAuth2ClientProperties 는 spring.security.oauth2.client 로 시작되는 스프링 설정을 사용하여 구성되는 설정 프로퍼티입니다.
여기에는 여러 개의 Provider 및 Registration 목록이 담기게 되는데요.
public static class Provider {
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private String userInfoAuthenticationMethod;
private String userNameAttribute;
private String jwkSetUri;
private String issuerUri;
Provider 는 소셜서비스 제공자에 관련된 설정을 바인딩하는 부분입니다.
예를 들어 authorizationUri 는 해당 서비스의 승인 페이지의 Uri 라 보면 되고, tokenUri 는 액세스토큰을 발급하는 Uri,
userInfoUri 는 사용자 정보를 획득하기 위한 엔드포인트 입니다.
public static class Registration {
private String provider;
private String clientId;
private String clientSecret;
private String clientAuthenticationMethod;
private String authorizationGrantType;
private String redirectUri;
private Set<String> scope;
private String clientName;
Registration 은 OAuth2 Client, 즉 우리 서비스의 OAuth2 Client 로서의 등록정보입니다.
client_id, client_secret, redirect_uri 와 같은 우리 서비스가 소셜서비스별로 설정한 필수적인 기본 정보를 이곳에 바인딩되게 해야합니다.
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
class OAuth2ClientRegistrationRepositoryConfiguration {
@Bean
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
new OAuth2ClientPropertiesMapper(properties).asClientRegistrations().values());
return new InMemoryClientRegistrationRepository(registrations);
}
}
위에서 구성된 OAuth2ClientProperties 를 사용해서
스프링 시큐리티 OAuth2 자동구성 과정에는 InMemoryClientRegistrationRepository 라는 빈이 등록됩니다.
ClientRegistrationRepository 는 여러 개의 ClientRegistration 이 포함됩니다.
public final class ClientRegistration implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private String registrationId;
private String clientId;
private String clientSecret;
private ClientAuthenticationMethod clientAuthenticationMethod;
private AuthorizationGrantType authorizationGrantType;
private String redirectUri;
private Set<String> scopes = Collections.emptySet();
private ProviderDetails providerDetails = new ProviderDetails();
private String clientName;
위에서 언급한 Registration, Provider 클래스의 정보가 융합되어 ClientRegistration 가 만들어집니다.
여기에는 소셜서비스 제공자, Provider 의 정보와 우리 서비스의 OAuth2 Client 로서의 정보가 모여있습니다.
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
class OAuth2ClientRegistrationRepositoryConfiguration {
@Bean
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
new OAuth2ClientPropertiesMapper(properties).asClientRegistrations().values());
return new InMemoryClientRegistrationRepository(registrations);
}
}
다시 ClientRegistration 을 구성하는 코드를 봅시다.
public Map<String, ClientRegistration> asClientRegistrations() {
Map<String, ClientRegistration> clientRegistrations = new HashMap<>();
this.properties.getRegistration()
.forEach((key, value) -> clientRegistrations.put(key,
getClientRegistration(key, value, this.properties.getProvider())));
return clientRegistrations;
}
asRegistrations 에서 더 파고 들어가 getClientRegistration 을 보겠습니다.
private static ClientRegistration getClientRegistration(String registrationId,
OAuth2ClientProperties.Registration properties, Map<String, Provider> providers) {
Builder builder = getBuilderFromIssuerIfPossible(registrationId, properties.getProvider(), providers);
if (builder == null) {
builder = getBuilder(registrationId, properties.getProvider(), providers);
}
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties::getClientId).to(builder::clientId);
map.from(properties::getClientSecret).to(builder::clientSecret);
map.from(properties::getClientAuthenticationMethod)
.as(ClientAuthenticationMethod::new)
.to(builder::clientAuthenticationMethod);
map.from(properties::getAuthorizationGrantType)
.as(AuthorizationGrantType::new)
.to(builder::authorizationGrantType);
map.from(properties::getRedirectUri).to(builder::redirectUri);
map.from(properties::getScope).as(StringUtils::toStringArray).to(builder::scope);
map.from(properties::getClientName).to(builder::clientName);
return builder.build();
}
이곳에서 실질적인 ClientRegistration 을 구성하는 작업이 일어나는데요.
이 과정에서 클라이언트 기준인 Registration 항목과 서비스 제공자 기준인 Provider 항목으로 구분하여 순서대로 설정합니다.
1. Provider (서비스 제공자) 설정 구성
여기서 Provider 설정을 기반으로 builder를 준비합니다.
- getBuilderFromIssuerIfPossible : provider 설정의 issuerUri 가 있다면 issuerUri를 기반으로 메타데이터를 조회해서 provider 설정이 구성됩니다.
- getBuilder : 앞에서 얻어오지 못 했다면 builder가 null 인데, 여기서 우리가 따로 설정한 provider 설정값을 기반으로 builder 를 구성합니다.
- 이 때 provider id가 google, github, okta, facebook 이면 스프링이 알아서 설정을 구성해줍니다.
- 각 작업에서, 우리가 따로 추가 작성해 둔 provider 설정이 있으면 그 값으로 덮어씌워집니다.
2. Registration 설정 구성
클라이언트 설정 추가가 그 다음에 이루어집니다.
Registration으로부터 클라이언트 설정(properties)을 가져와 builder에 추가합니다.
3. 빌더.build()
최종적으로 1,2 를 통해 만들어진 설정을 종합하여 ClientRegistration 이 구성됩니다.
private static Builder getBuilderFromIssuerIfPossible(String registrationId, String configuredProviderId,
Map<String, Provider> providers) {
String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId;
if (providers.containsKey(providerId)) {
Provider provider = providers.get(providerId);
String issuer = provider.getIssuerUri();
if (issuer != null) {
Builder builder = ClientRegistrations.fromIssuerLocation(issuer).registrationId(registrationId);
return getBuilder(builder, provider);
}
}
return null;
}
getBuilderFromIssuerIfPossible
우리가 설정값에서 issuerUri(OAuth2 인가서버 주소) 를 설정했다면
스프링은 issuer Uir 설정이 되어 있으면
다음에 해당하는 엔드포인트들과 HTTP 통신을 해서
/.well-known/openid-configuration
/.well-known/oauth-authorization-server
먼저 찾아지는 인가서버의 여러가지 메타데이터를 가져옵니다.
보통 Oidc 표준을 준수하는 서비스(Google, Kakao 는 이러한 엔드포인트가 개방되어 있습니다.
이런 메타데이터를 기반으로 Builder 를 구성하고, 만약 개발자가 커스텀하게 설정한 값이 있다면 그 값으로 우선시하여 덮어씌웁니다.
Kakao 는 "https://kauth.kakao.com" 를 issuerUri 로 설정해두면 /.well-known/openid-configuration 에 접근할 수 있는데 여기 보시면 여러가지 OAuth2, Oidc 인증에 필요한 메타데이터들이 제공됩니다.
인가 엔드포인트(승인), 토큰 엔드 포인트, 사용자 정보 엔드포인트, jwks uri(제공되는 액세스토큰, 리프레시토큰, Open id 검증을 위한 JWKS), 토큰 엔드포인트 접근을 위해 인증은 어떤 방식으로 해야하는지, PKCE 방식을 지원한다면 어떤 알고리즘을 담아야하는지... 여러가지 메타데이터들이 여기 담겨있습니다.
스프링은 이런 메타데이터를 전부 끌어와서 ClientRegistration 의 Provider 정보 구성에 사용합니다.
private static Builder getBuilder(String registrationId, String configuredProviderId,
Map<String, Provider> providers) {
String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId;
CommonOAuth2Provider provider = getCommonProvider(providerId);
if (provider == null && !providers.containsKey(providerId)) {
throw new IllegalStateException(getErrorMessage(configuredProviderId, registrationId));
}
Builder builder = (provider != null) ? provider.getBuilder(registrationId)
: ClientRegistration.withRegistrationId(registrationId);
if (providers.containsKey(providerId)) {
return getBuilder(builder, providers.get(providerId));
}
return builder;
}
public enum CommonOAuth2Provider {
GOOGLE {
@Override
public Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = getBuilder(registrationId,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL);
builder.scope("openid", "profile", "email");
builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
builder.issuerUri("https://accounts.google.com");
builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
builder.userNameAttributeName(IdTokenClaimNames.SUB);
builder.clientName("Google");
return builder;
}
getBuilder
만약 IssuerUri 가 없다면 직접 Provider 설정을 작성한 값을 기반으로 Provider 정보를 구성해야하는데요.
이때 만약 provider 의 Id 값이 google, okta, facebook, github 에 해당될 경우 스프링이 Provider 정보를 준비해뒀기 때문에 따로 설정을 우리가 작성하지 않아도 스프링이 알아서 대부분의 설정을 구성해줍니다.
이런 것에 해당되지 않는다면 빈 builder 를 만듭니다.
그 후 개발자가 작성한 provider 설정이 있으면 이를 기반으로 Provider builder 를 구성합니다.(기존값이 있으면 덮어씌우고)
5. 그래서 어떻게 설정해야하는데?
여기서 좀 더 상세한 내부 구현을 기반으로 어떻게 설정을 작성해야할 지 결론을 내려보겠습니다.
저는 Spring Security OAuth2 Client 자동 구성을 위한 설정을 해야하며,
여기서 서비스 제공자(Provider) 에 관한 설정과 OAuth2 Client 로서의 설정 두 가지를 모두 준비해야합니다.
제가 연동하고자 하는 소셜 서비스는 구글, KAKAO, NAVER 입니다. 위의 기반지식을 토대로 설정을 어떻게 구성해야할지 따져보겠습니다.
spring:
config:
activate:
on-profile: localSecret
import: security-config-localSecret.yml
---
spring:
config:
activate:
on-profile: localSecret
security:
oauth2:
client:
registration:
google:
clientId: client_id값
clientSecret: client_secret값
redirectUri: redirect_uri값
scope: openid,email
naver:
clientId: client_id값
clientSecret: client_secret값
authorizationGrantType: authorization_code
redirectUri: redirect_uri값
scope: email
kakao:
clientId: client_id값
clientSecret: client_secret값
redirectUri: redirect_uri값
scope: openid,account_email
provider:
naver:
authorizationUri: https://nid.naver.com/oauth2.0/authorize
tokenUri: https://nid.naver.com/oauth2.0/token
userInfoUri: https://openapi.naver.com/v1/nid/me
userNameAttribute: response
kakao:
issuerUri: https://kauth.kakao.com
제가 설정한 yml 파일입니다. (localSecret 프로필일 떄 설정되도록 한 값들이에요.)
Provider 설정
- GOOGLE 은 이미 스프링이 대부분의 Provider 메타데이터를 전부 준비해뒀기에 따로 설정할 필요가 없습니다.
- KAKAO는 issuer_uri 를 설정해두기만 해두면 스프링이 알아서 메타데이터 엔드포인트를 통해 여러가지 Provider 설정을 가져옵니다.
- NAVER 는 위의 어떤 것에도 속하지 않기 때문에 여러가지 Provider 설정을 개발자가 직접 적어줘야합니다.
- authorizationUri : 인가서버의 승인 엔드포인트
- tokenUri : 토큰 발급 엔드포인트
- usernameAttribute : 사용자 정보 엔드포인트로부터 응답을 받았을 때 사용자의 식별자를 얻기 위한 속성 이름인데, naver는 사용자 식별자를 response 객체 내부의 id 로 감싸서 관리하고 있습니다... 그래서 response 로 지정해도 사실 사용자 식별자를 얻을 수 없습니다. 그런데 이 값을 지정하지 않으면 예외가 발생하다보니 일단 response 로 지정합니다.
Client 설정
- 서비스에 무관하게 client_id, client_secret 은 필수입니다.
- redirect_uri : 인가 서버에게 전달할 redirect_uri 입니다. 인가서버는 사용자 승인 이후 사용자를 이곳으로 리다이렉트 시킵니다. 인가서버 측에 redirect_uri 를 등록해놔야합니다.
- scope: 사용자에게 허락을 구하고 싶은 scope 입니다. 각 서비스별로 scope 이름이 제각각 다를 수 있다보니 이건 각 서비스의 API 명세들을 살펴봐야합니다.
- GOOGLE 은 openid connect 를 지원하므로 openid 를 스코프에 포함했습니다. 추가로 email 정보를 필요로 해서 email 을 포함시켰습니다. 이렇게 하면 id token 에 email 이 포함될거에요.
- KAKAO 는 openid connect 를 지원하므로 스코프에 openid 를 넣었습니다. 카카오에서는 open id token 에 이메일을 포함시키기 위해서 account_email 스코프를 포함시켜야합니다.
- NAVER는 openid connect 를 지원하지 않습니다. email 을 스코프에 포함시켰습니다.
- authorizationGrantType : OAUth2 승인 유형을 어떻게 할 것인가를 지정하는 부분이에요.
- GOOGLE 은 스프링에 의해 authorization_code 로 이 값이 설정됩니다. 자세한 부분은 CommonOAuth2Provider 를 참고하세요.
- KAKAO 는 issuer_uri 설정을 했는데 이 결과로 메타데이터 엔드포인트를 통해 Provider 정보를 갖고오고, 클라이언트 설정인 AuthorizationGrantType authorization_code 방식으로 설정됩니다. 이 부분은 ClientRegistrations 클래스를 참고하시면 될듯합니다.
- NAVER 는 따로 설정이 안 되기 때문에 저희가 authorization_code 로 지정해야합니다.
6. 실제 애플리케이션 구동
실제로 InmemoryClientRegistration 이 등록되는 곳에 디버거를 걸어보고 애플리케이션을 디버거 구동해보겠습니다.
실제로 ClientRegistration 3개가 등록되어 있습니다.
6.1 Google
구글은 제가 provider 설정을 하나도 안 했음에도 스프링이 미리 준비해둔 설정을 상당히 채워두고 있습니다.
6.2 Kakao
카카오는 제가 provider 관련 설정은 issuerUri 설정만 했음에도 다른 여러가지 정보들이 같이 끌려온 것을 볼 수 있습니다.
스프링이 내부적으로 RestTemplate 을 통해 카카오의 메타데이터 엔드포인트쪽과 통신해서 데이터를 가져온 것이에요.
6.3 Naver
네이버는 제가 수동으로 기입한 정보들 위주로만 채워지고 다른 부가적인 설정들은 채워져 있지 않습니다.
일단 이번 글에서는 Google, KaKao, Naver 와 소셜 연동을 하기 위한 설정 구성을 간단하게 해봤습니다.
이어지는 글들에서는 소셜로그인 기능을 한 단계씩 구현해보겠습니다.