-
Notifications
You must be signed in to change notification settings - Fork 26
5. OAuth2
스프링 시큐리티는 OAuth2 기반의 시스템을 구성하기 위한 모듈을 지원합니다. OAuth2 모듈을 통해 OAuth2 로그인을 적용하거나 OAuth2 인증 서버를 쉽게 구성할 수 있습니다.
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)을 구성해야 합니다. 카카오 개발자 센터로 로그인하여 앱 만들기를 통해 나의 애플리케이션을 추가합니다.
나의 애플리케이션 > 설정 > 사용자 관리 메뉴에서 로그인 Redirect URI
를 다음과 같이 설정합니다.
http://localhost:8080/login/oauth2/code/kakao
기본 리다이렉트 URI 템플릿은 {baseUrl}/login/oauth2/code/{registrationId} 입니다.
스프링 부트 애플리케이션 프로퍼티 정보에 카카오 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 인증을 요청합니다.
http://localhost:8080/oauth2/authorization/kakao로 이동시키면 카카오 OAuth 2.0 프로바이더에 인증을 요청할 수 있습니다.
스프링 시큐리티는 OAuth 2.0 표준 스펙에 따른 권한 및 리소스 서버에 대한 구성을 지원합니다.
권한 서버는 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) {
// 권한 부여 그리고 토큰 엔드포인트와 토큰 서비스를 정의합니다.
}
}
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;
}
}