Skip to content
This repository was archived by the owner on Mar 2, 2023. It is now read-only.

5. OAuth2

강교일(Mambo) edited this page Mar 24, 2020 · 4 revisions

스프링 시큐리티는 OAuth2 기반의 시스템을 구성하기 위한 모듈을 지원합니다. OAuth2 모듈을 통해 OAuth2 로그인을 적용하거나 OAuth2 인증 서버를 쉽게 구성할 수 있습니다.

OAuth2 Login

dependenceis {
    implementation 'org.springframework.security:spring-security-oauth2-client'
}

spring-security-oauth2-client 의존성을 추가하면 Authorization Code Grant를 사용하여 OAuth 2.0 프로바이더의 계정을 통해 현재 애플리케이션으로 로그인하는 기능을 적용할 수 있습니다. 일반적으로 "구글 계정으로 로그인" 또는 "깃허브 계정으로 로그인"과 같은 기능을 말합니다.

초기 설정

로그인을 위해 카카오 OAuth 2.0 인증 시스템을 사용하려면 카카오 개발자 센터에서 애플리케이션(OAuth 2.0 Credential)을 구성해야 합니다. 카카오 개발자 센터로 로그인하여 앱 만들기를 통해 나의 애플리케이션을 추가합니다.

리다이렉트 URI 설정

나의 애플리케이션 > 설정 > 사용자 관리 메뉴에서 로그인 Redirect URI를 다음과 같이 설정합니다.

http://localhost:8080/login/oauth2/code/kakao

기본 리다이렉트 URI 템플릿은 {baseUrl}/login/oauth2/code/{registrationId} 입니다.

Application.yml

스프링 부트 애플리케이션 프로퍼티 정보에 카카오 OAuth 2.0 프로바이더에 대한 정보를 추가합니다.

spring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id:
            client-secert:
            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
            authorization-grant-type: authorization_code
        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-info-authentication-method: POST

그리고 나의 애플리케이션의 REST API 키를 클라이언트 아이디로 설정합니다.

클라이언트 시크릿은 옵션입니다.

spring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: d65110d**************d018b
            client-secert: 3pk3UZ*************PdeXO

카카오 사용자 유형 추가

카카오 OAuth 2.0 프로바이더에 대한 사용자 유형을 추가합니다.

@Data
public class KakaoOAuth2User implements OAuth2User {
    public static final String PROVIDER = "kakao";

    private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
    private String id;
    @JsonProperty("kakao_account")
    private Map<String, Object> kakaoAccount;
    private Map<String, Object> properties;

    @Override
    public Map<String, Object> getAttributes() {
        Map<String, Object> attributes = new HashMap<>();
        attributes.putAll(kakaoAccount);
        attributes.putAll(properties);
        return attributes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getName() {
        return this.id;
    }
}


@Override
protected void configure(HttpSecurity http) throws Exception {
    http.oauth2Login(oauth2Login -> oauth2Login
             .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
                   .customUserType(KakaoOAuth2User.class, KakaoOAuth2User.PROVIDER)));
}

카카오 OAuth 2.0 인증

카카오 계정으로 로그인 버튼을 구성하여 카카오 OAuth 2.0 인증을 요청합니다.

http://localhost:8080/oauth2/authorization/kakao로 이동시키면 카카오 OAuth 2.0 프로바이더에 인증을 요청할 수 있습니다.

OAuth2 Authorization & Resource Server

스프링 시큐리티는 OAuth 2.0 표준 스펙에 따른 권한 및 리소스 서버에 대한 구성을 지원합니다.

Authorization Server

권한 서버는 OAuth Grant Types에 따라 클라이언트가 보호된 리소스에 대한 자격 증명인 액세스 토큰을 부여하는 과정을 담당 합니다.

일반적으로 인증에는 다음과 같은 유형이 있습니다.

  • Authorization Code
  • Client Credentials
  • Implicit
  • Resource Owner Password Credentials

스프링 시큐리티에서는 AuthorizationEndpoint가 Authorization Code 유형으로 자격 증명을 얻기 위한 인증 코드를 발급하는 것을 담당합니다. 이 엔드포인트는 아주 간단하게 @EnableAuthorizationServer로 OAuth 2.0 인증 서버 매커니즘을 적용하면 추가할 수 있습니다

@EnableAuthorizationServer
@Configuration
public static class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 토큰 엔드포인트에 대한 보안 제한 조건을 정의합니다.
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 클라이언트 정보를 초기화하거나 저장하는 클라이언트 디테일 서비스를 구성
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // 권한 부여 그리고 토큰 엔드포인트와 토큰 서비스를 정의합니다.
    }
}

ClientDetailsServiceConfigurer

OAuth2 인증 과정에서 필요한 클라이언트 정보를 수립하는 것을 담당합니다. 예를 들어, 카카오 애플리케이션에서는 앱키에 해당하는 정보를 만들어내는 과정입니다.

클라이언트 정보 구성

OAuth 클라이언트 정보는 다음과 같이 구성됩니다.

  • ClientId: (필수) 클라이언트 아이디
  • secret: (신뢰된 클라이언트인 경우) 클라이언트 시크릿
  • scope: 클라이언트의 제한된 스코프 범위
  • authorizedGrantTypes: 클라이언트가 사용할 수 있는 인증 유형
  • authorities: 클라이언트에 부여된 권한

이 클라이언트 정보는 ClientDetailsManager 인터페이스를 통해 생성하거나 JDBC와 같은 저장소를 활용할 수 있습니다.

다음은 사용자 정의된 ClientDetailsService를 적용하는 예시입니다.

@Service
public class ClientService implements ClientDetailsService {

    private List<Client> clients;

    public ClientService() throws IOException {
        loadClients();
    }

    private void loadClients() throws IOException {
        Gson gson = new Gson();
        ClassPathResource classPathResource = new ClassPathResource("db/client.json");
        String clientsJson = StreamUtils.copyToString(classPathResource.getInputStream(), StandardCharsets.UTF_8);
        this.clients = gson.fromJson(clientsJson, new TypeToken<List<Client>>(){}.getType());
    }

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {

        if(clients != null) {
            for (Client client : clients) {
                if (client.getClientId().equals(clientId)) {
                    return client;
                }
            }
        }

        return null;
    }
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.withClientDetails(clientService);
}

db/client.json

다음의 JSON은 두 개의 클라이언트 정보를 구성하고 있습니다.

[
  {
    "name": "client_1",
    "clientId": "e0113ca5-486f-43e9-9615-674b2232caed",
    "clientSecret": "$2a$12$ry/T4SyQyiNpaWbadf9sne3Cko..q92Oh2klkCMv4XB1qG6cy8iaG",
    "authorizedGrantTypes": "authorization_code, client_credentials, refresh_token",
    "registeredRedirectUri": "http://localhost:8080",
    "accessTokenValidity": 86400,
    "refreshTokenValidity": 604800
  },
  {
    "name": "client_2",
    "clientId": "429c4db4-821c-414e-bccd-2f4826ea08c4",
    "clientSecret": "$2a$12$ry/T4SyQyiNpaWbadf9sne3Cko..q92Oh2klkCMv4XB1qG6cy8iaG",
    "authorizedGrantTypes": "authorization_code, implicit, client_credentials",
    "registeredRedirectUri": "http://localhost:8080",
    "accessTokenValidity": 86400
  }
]

그리고 이 클라이언트 정보를 표현하는 ClientDetails를 구현합니다.

@Data
public class Client implements ClientDetails {

    private String name;

    private String clientId;
    private String clientSecret;
    private String authorities;
    private String scope;
    private String authorizedGrantTypes;
    private String resourceIds;
    private Integer accessTokenValidity;
    private Integer refreshTokenValidity;
    private String autoApprove;
    private String additionalInformation;
    private String registeredRedirectUri;

    @Override
    public String getClientId() {
        return this.clientId;
    }

    @Override
    public Set<String> getResourceIds() {
        if(this.resourceIds == null) {
            return Collections.emptySet();
        }
        return new HashSet<>(Arrays.asList(resourceIds.split(",")));
    }

    @Override
    public boolean isSecretRequired() {
        return this.clientSecret != null;
    }

    @Override
    public String getClientSecret() {
        return this.clientSecret;
    }

    @Override
    public boolean isScoped() {
        return this.scope != null;
    }

    @Override
    public Set<String> getScope() {
        Set<String> scopes = new HashSet<>();
        if(this.scope == null) {
            scopes.add("all");
        } else {
            scopes =  Arrays.stream(scope.split(",")).map(String::trim).collect(Collectors.toSet());
        }
        return scopes;
    }

    @Override
    public Set<String> getAuthorizedGrantTypes() {
        if(this.authorizedGrantTypes == null) {
            return Collections.emptySet();
        }
        return Arrays.stream(authorizedGrantTypes.split(",")).map(String::trim).collect(Collectors.toSet());
    }

    @Override
    public Set<String> getRegisteredRedirectUri() {
        if(registeredRedirectUri == null) {
            return Collections.emptySet();
        }
        return Arrays.stream(registeredRedirectUri.split(",")).map(String::trim).collect(Collectors.toSet());
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        if(this.authorities == null) {
            return Collections.emptyList();
        }
        return Arrays.stream(this.authorities.split(",")).map(authority -> (GrantedAuthority) authority::trim).collect(Collectors.toSet());
    }

    @Override
    public Integer getAccessTokenValiditySeconds() {
        return this.accessTokenValidity;
    }

    @Override
    public Integer getRefreshTokenValiditySeconds() {
        return this.refreshTokenValidity;
    }

    @Override
    public boolean isAutoApprove(String scope) {
        return false;
    }

    @Override
    public Map<String, Object> getAdditionalInformation() {
        return null;
    }
}

참고

Clone this wiki locally