03. Oauth2 소셜 로그인 - YAPP-16th/Team_Web_3_Backend GitHub Wiki
03 Oauth2 소셜 로그인
개요
API 서버는 클라이언트에게 정보를 제공하기 위해 존재한다. 우리가 사는 집에는 열쇠가 있기 때문에 열쇠를 가진 사람만 드나들 수 있다. 마찬가지로 API 서버 또한 아무 클라이언트에게 모든 정보를 줄 수는 없다. 클라이언트가 열쇠를 제출하면 그 열쇠를 인증할 수 있는 수단이 필요하다. 이러한 수단에는 여러가지 방식이 있는데 가장 대중적이고 사실상 글로벌 표준처럼 취급되는 것이 바로 OAuth 2.0이다.
환경
- JDK 8
- IntelliJ IDEA
build.gradle
기본 Spring Boot 프로젝트 구성을 마친 후 /build.gradle 파일에 아래 내용을 추가한다.
dependencies {
compile group: 'org.springframework.boot', name: 'spring-boot-starter-web'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security'
compile group: 'org.springframework.security.oauth', name: 'spring-security-oauth2'
}
dependencies.compile 항목으로 spring-boot-starter-security, spring-security-oauth2를 추가하는 것 만으로 기존 작성된 모든 엔드포인트에 대해 Basic Auth 인증을 요구하는 필터가 작동하게 된다.
@EnableAuthorizationServer
package com.jsonobject.oauth2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("some_client_id")
.secret("some_client_secret")
.scopes("read:current_user", "read:users")
.authorizedGrantTypes("client_credentials");
}
}
-
AuthorizationServerConfigurerAdapter 클래스를 상속하고 클래스 레벨에 @EnableAuthorizationServer 어노테이션을 추가하면 구체적인 환경 설정이 가능하다.
-
configure(ClientDetailsServiceConfigurer clients) 메써드에서는 API의 요청 클라이언트 정보를 설정할 수 있다.
-
inMemory()는 클라이언트 정보를 메모리에 저장한다. 개발 환경에 적합하다. 반면 jdbc()는 데이터베이스에 저장한다. 운영 환경에 적합하다.
-
withClient()로 client_id 값을 설정한다. secret()은 client_secret 값을 설정한다.
-
scopes()는 scope 값을 설정한다.
-
authorizedGrantTypes()는 grant_type(access_token을 획득하기 위한 4가지 인증 방법) 값을 설정한다. 복수개를 저장할 수 있다. 본 예제에서는 client_id, client_secret 만으로 access_token을 요청할 수 있는 client_credentials를 설정했다.
저장소 설정하기
클라이언트와 토큰 관리는 Spring Security OAuth 모듈이 담당하지만 사용자 관리는 Spring Security의 몫이다. /oauth/authorize 요청을 처리하려면 사용자 저장소에 접근할 수 있어야 한다. 메모리에 사용자 정보를 등록하는 예
package com.jsonobject.oauth2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER")
.and()
.withUser("admin")
.password("password")
.roles("USER", "ADMIN");
}
}
auth.userDetailsService()로 임의의 UserDetailsService 인터페이스 구현체를 설정할 수 있다.
ClientDetailsService 빈 설정
client_id, client_secret 등을 저장하는 클라이언트 저장소에 대한 모든 CRUD는 ClientDetailsService 인터페이스로 구현하게 되어 있다. 기본 제공되는 구현체로는 InMemoryClientDetailService, JdbcClientDetailService 클래스가 제공된다. JDBC 기반의 빈 설정을 예
@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
@Bean
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(someDataSource);
}
}
TokenStore 빈 설정
Spring Security OAuth에서 access_token, refresh_token을 저장하는 토큰 저장소에 대한 모든 CRUD는 TokenStore 인터페이스로 구현하게 되어 있다. 기본 제공되는 구현체로는 InMemoryTokenStore, JdbcTokenStore, RedisTokenStore 클래스가 제공된다. 인터페이스만 구현하면 되므로 제3의 구현체를 작성해도 된다.
TokenStore 빈을 설정하려면 configure(AuthorizationServerEndpointsConfigurer endpoints) 오버라이드 메써드에서 endpoints.tokenStore()를 호출하면 된다. Redis 기반의 TokenStore 빈 설정을 예로 들면 아래와 같다.
dependencies {
compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis'
}
@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore());
}
@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(jedisConnectionFactory());
}
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setHostName("localhost");
factory.setPort(6379);
factory.setPassword("");
factory.setDatabase(1);
factory.setUsePool(true);
return factory;
}
}
/oauth/authorize
GET /oauth/authorize는 사용자의 브라우저(또는 앱)에 보여줄 리다이렉트 웹 페이지이다. 사용자에게 로그인과 사용할 데이터에 대한 동의를 요구한다.
만약 클라이언트가 로그인한 상태라면 권한부여에 대한 동의 페이지를, 로그인하지 않은 상태라면 로그인 페이지로 리다이렉트되어야 한다.
client_id 파라메터에는 사용자의 정보를 요구하는 클라이언트(사용자가 현재 서비스를 이용하고 있는)의 client_id를 담는다.
scope 파라메터에는 사용자에게 동의를 요청할 권한(허용되는 데이터의 범위)의 목록을 담는다.
redirect_uri 파라메터에는 사용자 동의 후 리다렉트될 클라이언트 측의 웹 페이지 주소를 담는다.
사용자가 권한부여에 동의하면 POST /oauth/authorize로 리다이렉트된다. 이 때 전달되는 파라메터는 아래와 같다. 처리 후에는 클라이언트가 요청한 redirect_uri로 결과를 전달한다. code 파라메터가 첨부된다.
_csrf=2ed1dfa5-3959-4560-a72d-c9f364696fc3
authorize=Authorize
scope.read:current_user=true
scope.read:user=true
user_oauth_approval=true
Spring Securiy는 사용자의 정보에 대한 CRUD를 수행하는 ClientDetailsService 인터페이스 구현체가 설정되어 있어야 한다.
/oauth/token
/oauth/token 엔드포인트는 클라이언트 인증 후 access_token을 발급한다. client_id, client_secret(Basic Auth로 요청), grant_type이 필수 파라메터로 요구된다.
$ curl -X POST "http://localhost:8080/oauth/token" \
-H 'authorization: Basic c2VydmljZS1hY2NvdW50LTE6c2VydmljZS1hY2NvdW50LTEtc2VjcmV0' \
-d "grant_type=client_credentials"
아래와 같이 응답 바디로 access_token이 도착한 것을 확인할 수 있다.
{
"access_token": "bcf570e5-0f2f-481b-a2ae-6d57fa7f711c",
"token_type": "bearer",
"expires_in": 38010,
"scope": "read:current_user read:users"
}
앞서 GET /oauth/authorize의 결과로 획득한 code 파라메터로 access_token을 발급 받을 수 있다. client_id, client_secret, grant_type, code, redirect_uri 파라메터가 필수로 요구된다. code 파라메터는 굉장히 짧은 시간 동안만 유효하며 1회 요청이 발생하면 소멸된다. /oauth/check_token /oauth/check_token 엔드포인트는 요청 파라메터의 access_token의 유효 여부와 유효시 해당 클라이언트 정보를 응답한다. 요청 예는 아래와 같다. 발급된 access_token을 token 파라메터에 첨부한다.
$ curl -X POST "http://localhost:8080/oauth/check_token" \
-d "token=067d81ec-6c68-42f3-afc6-97e4b6dc2dd6"
성공 응답은 아래와 같다.
{
"scope": [
"read:current_user",
"read:users"
],
"exp": 1499199182,
"client_id": "some_client_id"
}
실패 응답은 아래와 같다.
{
"error": "invalid_token",
"error_description": "Token was not recognised"
}
{
"error": "invalid_token",
"error_description": "Token has expired"
}