OAuth2란?
- OAuth2는 사용자가 하나의 서비스(ex: 웹사이트, Application)에 로그인할 때 다른 서비스의 자격증명을 사용하여 인증할 수 있게 해주는 인증 프로토콜입니다.
ex)
새로운 웹사이트에 가입하려고 할 때, google, naver 로그인을 보셨을 겁니다.
이 경우, 해당 웹사이트는 사용자의 google 또는 naver 계정을 통해 사용자를 인증합니다.
사용자는 별도의 새로운 계정을 만들 필요 없이 기존의 소셜 미디어 계정을 사용하여 쉽게 로그인 할 수 있습니다.
이 과정에서 OAuth2 프로토콜이 사용되며, 사용자의 로그인 정보는 안전하게 보호됩니다.
OAuth2 구조
OAuth2 의 플로우는 위 아래 그림과 같습니다.
기본적으로는
- Client -> OAuth Authentication Server 인증코드 받기
- OAuth Authentication Server 에서 받은 인증코드를 기반으로 Client에서 OAuth Authentication Server로 token 발급
- OAuth Authentication Server 에서 받은 token 기반으로 OAuth Resource Server에서 Resource 할당
OAuth2 말하는 역할 및 주요 용어는 어떤게 있나요?
1) 역할
이름 | 설명 |
---|---|
Resource Owner | 리소스(자원) 소유자입니다. 본인의 정보에 접근할 수 있는 자격을 승인하는 주체입니다. 예를 들어보겠습니다. 서비스를 이용하기 위해 해당 서비스에서 제공하는 Google 간편 로그인으로 로그인을 한 경우, 해당 Google 아이디를 가지고 있는 본인이 Resource Owner가 되는 겁니다. |
Client | Resource Owner의 리소스(자원)를 사용하고자 접근 요청을 하는 서비스(어플리케이션) 입니다. |
Resource Server |
Resource Owner의 정보가 저장되어 있는 서버입니다. 위 예시에서 Google 로그인을 하는 본인이 Resource Owner라고 했었습니다. 이 때, Google이 Resource Server가 되는 겁니다. |
Autehorization Server |
Resource Server에서 리소스를 가져오기 위한 token(권한)을 할당해주는 권한 서버입니다. 인증 / 인가를 수행하는 서버로써 Client의 접근 자격을 확인하고 Access Token을 발급하여 권한을 부여하는 역할을 수행합니다. |
2) 주요 용어
이름 | 설명 |
---|---|
Authentication(인증) | 인증, 접근 자격이 있는지 검증하는 단계입니다. |
Authorization(인가) | 자원에 접근할 권한을 부여하고 리소스 접근 권한이 담긴 Access Token을 제공합니다. |
Access Token | Resource Server 에서 Resource Owner 의 리소스(자원)를 획득 할 때 사용되는 만료기간이 있는 Token 입니다. |
Refresh Token | Access Token 만료시 이를 재발급 받기위한 용도로 사용하는 Token 입니다. |
토큰(Token) 발급 방식
- 아래에서 사용하는 URL은 기본 oauth2.0 코드 발급 URL 입니다.
- code (code를 발급 받고 code로 access token 발급)
1) Client -> Authentication Server 로 code를 발행 합니다.
- URL: http://localhost:8080/oauth2/authorize
- Parameter: response_type=code, redirect_uri=코드 전달받을 URL, scope=read
2) 1번에서 발급받은 code를 기반으로 Client -> Authentication Server로 access token을 발급 받습니다.
- 발급 받은 code를 가지고 token을 발행 합니다.
- URL: http://localhost:8080/oauth2/token
- Parameter: grant_type=code, authorization_code=발급받은 코드, redirect_uri=코드 전달받은 URL
- password(id, pw로 access token 발급)
- id, pw로 token을 바로 발급 받을 수 있습니다.
- URL: http://localhost:8080/oauth2/token
- Parameter: grant_type=password, username=아이디, password=비밀번호, scope=read
- client_credentials(바로 access token 발행)
- header의 authorization에 clientId, secret key만 등록된 정보를 기반으로 access token을 발급해 주며, 별다른 인증이 필요 없는 신뢰도가 높은 Client에게만 해당 방식을 허용하도록 합니다.
- 해당 방식은 별다른 인증을 요하지 않기에 refresh_token은 따로 발행해주지 않습니다.
- URL: http://localhost:8080/oauth2/token
- Parameter: grant_type=client_credentials, scope=read
- refresh_token
- refresh token으로 access token 발행합니다.
- 1, 2번 방식으로 access_token 발행과 동시에 refresh_token 도 같이 발행해줍니다.
- URL: http://localhost:8080/oauth2/token
- Parameter: grant_type=refresh_token, refresh_token=이전에 발급받은 refresh token, scope=read
OAuth2 예시 코드
- Springboot 2.7.10, java 11, gradle 8.4 환경에서 구현하였습니다.
- 저는 OAuth2의 Resource Server와 Authentication Server를 합쳐서 구현하였습니다.
- 단순 FrontEnd와 BackEnd 간의 통신에서 사용하기 위하여 구현하였기에 client_credentials 기반으로 구현하였습니다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.14'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com.oauth2.resource.server'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '11'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.security:spring-security-oauth2-resource-server:5.7.11'
implementation 'org.springframework.security:spring-security-oauth2-jose:5.7.11'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.3.1'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.7.14'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
security 설정
package com.cafe.be.security.config;
import com.cafe.be.security.handler.CustomAccessDeniedHandler;
import com.cafe.be.security.handler.CustomAuthenticationEntryPoint;
import com.cafe.be.oauth.handler.JwtAuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@RequiredArgsConstructor
public class ResourceServerConfig{
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.headers().frameOptions().disable()
.and()
.csrf().ignoringAntMatchers("/h2-console/**").ignoringAntMatchers("/oauth2/token")
.and()
.authorizeRequests()
.mvcMatchers("/h2-console/**").permitAll()
.mvcMatchers("/oauth2/token").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
.and()
.oauth2ResourceServer()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.jwt();
return http.build();
}
}
OAuth2 설정
package com.cafe.be.oauth.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class) // OAuth2 인증 서버에 필요한 기본 설정을 가져옵니다.
public class AuthorizationServerConfig {
@Value("${oauth.issuer.url}")
private String issuerUrl;
@Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(10)) // 액세스 토큰 유효 기간을 10분으로 설정
.build();
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception{
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.build();
}
@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return ((jwkSelector, context) -> jwkSelector.select(jwkSet));
}
private RSAKey generateRsa() throws NoSuchAlgorithmException {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private static KeyPair generateRsaKey() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}
@Bean
public ProviderSettings providerSettings(){
return ProviderSettings.builder()
.issuer(issuerUrl)
.build();
}
}
OAuth2 관련 Entity
package com.cafe.be.oauth.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "oauth_client")
@Getter
@AllArgsConstructor
@Builder
public class OAuthClient {
@Id
private String clientId;
private String clientSecret;
private String scopes;
private String grantType;
protected OAuthClient() {}
}
OAuth2 관련 Enums
package com.cafe.be.oauth.enums;
public enum OAuthScopes {
READ("cafe.read");
private String scopeName;
OAuthScopes(String scopeName) {
this.scopeName = scopeName;
}
}
OAuth2 관련 Repository
package com.cafe.be.oauth.repo;
import com.cafe.be.oauth.entity.OAuthClient;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.util.UUID;
@Repository
@RequiredArgsConstructor
public class CustomRegisteredClientRepository implements RegisteredClientRepository {
private final OAuthClientRepository oauthClientRepository;
@Override
public void save(RegisteredClient registeredClient) {
// 클라이언트 정보를 데이터베이스에 저장하는 로직을 구현합니다.
OAuthClient oauthClient = OAuthClient.builder()
.clientId(registeredClient.getClientId())
.clientSecret(registeredClient.getClientSecret())
.scopes(String.valueOf(registeredClient.getScopes()))
.grantType(registeredClient.getAuthorizationGrantTypes().toString())
.build();
oauthClientRepository.save(oauthClient);
}
@Override
public RegisteredClient findById(String id) {
return oauthClientRepository.findById(id)
.map(this::toRegisteredClient)
.orElse(null);
}
@Override
public RegisteredClient findByClientId(String clientId) {
return oauthClientRepository.findByClientId(clientId)
.map(this::toRegisteredClient)
.orElse(null);
}
private RegisteredClient toRegisteredClient(OAuthClient oauthClient) {
// OAuthClient 엔티티를 RegisteredClient 객체로 변환하는 로직
// String grantType = oauthClient.getGrantType();
//
// Set<AuthorizationGrantType> grantTypeSet = new HashSet<AuthorizationGrantType>();
// if(grantType != null && grantType.equals("")){
// String[] grantTypeSplit = grantType.split(",");
//
// for (String type : grantTypeSplit) {
// grantTypeSet.add(new AuthorizationGrantType(type));
// }
// }
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(oauthClient.getClientId())
.clientSecret(oauthClient.getClientSecret())
.scope(oauthClient.getScopes())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(new AuthorizationGrantType(oauthClient.getGrantType()))
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(1)).build())
// .authorizationGrantTypes((Consumer<Set<AuthorizationGrantType>>) grantTypeSet)
.build();
return registeredClient;
}
}
package com.cafe.be.oauth.repo;
import com.cafe.be.oauth.entity.OAuthClient;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface OAuthClientRepository extends JpaRepository<OAuthClient, String> {
Optional<OAuthClient> findByClientId(String clientId);
}
반응형
'개발 > Spring' 카테고리의 다른 글
AOP란? AOP 내용, AOP 예제 (0) | 2024.01.11 |
---|---|
응집도와 결합도란? (1) | 2023.12.26 |
[Querydsl] DTO 조회 방법 (0) | 2023.12.17 |
DTO <-> Entity 변환 이유! (0) | 2023.12.14 |