앞서 다룬 내용들은 사용자의 크리덴셜(Credential)을 직접 관리하는 방식으로 로그인 인증 과정을 수행했다.
이 방법은 외부의 써드 파티 애플리케이션 API를 이용하는 기능이 추가되면 사용자의 크리덴셜을 여러개 관리해야 하고, 써드 파티 애플리케이션에서 패스워드를 변경하게 된다면 해당 애플리케이션도 추가적으로 업데이트가 이루어져야 한다는 단점이 있다.
또한, 외부의 애플리케이션에서 사용하는 크리덴셜을 직접적으로 보관하고 있다면 보안상으로 위험하다.
OAuth 2
- 소셜 로그인 인증 방식을 구현한 기술
- 사용자 정보를 보유하고 있는 신뢰할 만한 써드 파티 애플리케이션(GitHub, Google, Facebook 등)에서 사용자의 인증을 대신 처리해 주고 Resource에 대한 자격 증명용 토큰을 발급한 후, Client가 해당 토큰을 이용해 써드 파티 애플리케이션의 서비스를 사용하게 해주는 방식
- 특정 애플리케이션(Client)에서 사용자의 인증을 직접 처리하는 것이 아님
Resource Owner가 로그인을 하겠다고 버튼을 누름
-> 클라이언트가 써드 파티 애플리케이션의 로그인 페이지로 리다이렉트(Redirect) 해줌
Resource Owner가 로그인 인증 진행
로그인 성공시 Authorization Server가 Access Token을 Client에게 전송
Resource Server에게 Resource Owner 소유의 Resource를 요청
Access Token을 검증 후 자격 증명되면 Resource Owner의 Resource를 Client에게 전송
Client 애플리케이션이 Access Token을 얻기 위한 Resource Owner의 권한을 표현하는 크리덴셜(Credential)을 의미
(Client가 Access Token을 얻기 위한 수단)
Authorization Grant 타입 4가지
권한 부여 승인을 위해 자체 생성한 Authorization Code를 전달하는 방식으로, 가장 많이 쓰이고 기본이 되는 방식이다.
Refresh Token을 사용할 수 있다.
권한 부여 승인 요청 시 응답 타입(response_type
)을 code로 지정하여 요청한다.
별도의 Authorization Code 없이 바로 Access Token을 발급하는 방식
권한 부여 승인 요청 시 응답 타입(response_type)을 token으로 지정하여 요청한다.
간단하게 로그인 시 필요한 정보(username, password)로 Access Token을 발급받는 방식이다.
ex. 네이버 계정으로 네이버 웹툰 애플리케이션에 로그인, 카카오 계정으로 카카오 지도 애플리케이션에 로그인
즉, Authorization Server, Resource Server, Client가 모두 같은 시스템에 속해 있을 때만 사용이 가능
클라이언트의 자격증명만으로 Access Token을 획득하는 방식
자격 증명을 안전하게 보관할 수 있는 Client에서만 사용되어야 한다.
Refresh Token의 사용은 불가능하다.
같은 시스템내에서 사용자 요청 없이 내부로직에 의해 데이터 가져오는거라고 생각하면 됨
Resource Owner = 사용하고자 하는 Resource의 소유자
Client = Resource Owner를 대신해 보호된 Resource에 액세스하는 애플리케이션
Resource Server = Client의 요청을 수락하고 Resource Owner에 해당하는 Resource를 제공하는 서버
Authorization Server는 Client가 Resource Server에 접근할 수 있는 권한을 부여하는 서버를 의미한다.
OAuth 2 인증 프로토콜의 키포인트는 어떤 Resource를 소유하고 있는 Resource Owner를 대신하는 누군가(Client)가 Resource Owner의 대리인 역할을 수행한다는 것이다.
Authorization Code Grant = 권한 부여 승인 코드 방식
Implicit Grant = 암묵적 승인 방식
Resource Owner Password Credential Grant = 자원 소유자 자격 증명 승인 방식
Client Credentials Grant = 클라이언트 자격 증명 승인 방식
만들어지는데 1분정도 걸림
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // HTML 화면을 구성하기 위한 템플릿인 타임리프(Thymeleaf)를 추가
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // 구글의 OAuth 2 시스템을 이용해 클라이언트로써의 역할을 하기 위해 추가
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.mapstruct:mapstruct:1.5.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.2.Final'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'com.google.code.gson:gson'
}
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
jpa:
hibernate:
ddl-auto: create # 스키마 자동 생성
show-sql: true # SQL 쿼리 출력
properties:
hibernate:
format_sql: true # SQL pretty print
sql:
init:
data-locations: classpath*:db/h2/data.sql
security:
oauth2:
client:
registration:
google:
clientId: 아이디
clientSecret: 비번 노출안되게 조심!!!
logging:
level:
org:
springframework:
orm:
jpa: DEBUG
server:
servlet:
encoding:
force-response: true
mail:
address:
admin: admin@gmail.com
실습에서는 위와 같이 yml 파일에서 대충 했지만, 민감한 정보의 경우 application.yml 파일에 그대로 노출하는 것은 보안상 안좋다.
만약 실무에서 OAuth 2 클라이언트 ID와 Secret 같은 민감한 정보를 설정한다면 OS의 시스템 환경 변수에 설정하거나 또는 application.yml 파일에 구성하는 프로퍼티 정보를 애플리케이션 외부의 안전한 경로에 위치시키는 등의 방식으로 사용해야 한다.
build.gradle dependences {…}에 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
를 추가해 자동구성되는 부분이지만 직접 Configuration을 통해 빈등록으로 인증 설정을 해봤다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfiguration { // build.gradle dependences에 설정해뒀기 때문에 사실상 얜 없어도 자동구성 되는 클래스임
@Value("${spring.security.oauth2.client.registration.google.clientId}") // application.yml 파일에 설정되어 있는 구글의 Client ID 로드
private String clientId;
@Value("${spring.security.oauth2.client.registration.google.clientSecret}") // clientSecret 로드
private String clientSecret;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // CSRF 보호 기능을 비활성화
.formLogin().disable() // 폼 기반 로그인을 비활성화
.httpBasic().disable() // HTTP Basic 인증을 비활성화
.authorizeHttpRequests(authorize -> authorize // HttpSecurity를 인자로 받아서, 요청에 대한 인증 및 권한 부여
.anyRequest().authenticated() // 모든 요청에 대해서 인증된 사용자만 접근이 가능하도록 설정
)
.oauth2Login(withDefaults()); // OAuth2 로그인을 활성화하고 기본값으로 설정
return http.build();
}
@Bean // ClientRegistration을 저장하기 위한 Responsitory 빈등록
public ClientRegistrationRepository clientRegistrationRepository() {
var clientRegistration = clientRegistration(); // ClientRegistration(클라이언트 등록) 인스턴스 가져오기
return new InMemoryClientRegistrationRepository(clientRegistration); // ClientRegistrationRepository 인터페이스의 구현 클래스인InMemoryClientRegistrationRepository의 인스턴스를 생성
// InMemoryClientRegistrationRepository 는 ClientRegistration 을 메모리에 저장
}
private ClientRegistration clientRegistration() { // ClientRegistration 인스턴스를 생성하는 메서드
return CommonOAuth2Provider // 구글 말고도 깃헙, 페이스북 등 연결할 때 사용
.GOOGLE
.getBuilder("google")
.clientId(clientId)
.clientSecret(clientSecret)
.build();
}
}
무조건적인 자동 구성보다는 명시적으로 특정 설정을 선언해서 유지보수 용이하고 가독성 있는 코드를 구성하는 것 역시 중요하다.
인증정보(인증된 Authentication
객체)를 확인방법은 아래와 같이 3가지 방법이 있다.
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloHomeController {
@GetMapping("/hello-oauth2")
public String home() {
var oAuth2User = (OAuth2User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); // SecurityContext에서 인증된 Authentication 객체를 통해 Principal 객체 얻기
System.out.println(oAuth2User.getAttributes() // OAuth2User 객체에 저장되어 있는 사용자의 정보 중에서
.get("email")); // 사용자의 이메일 정보 얻기
return "hello-oauth2"; // oauth2(구글) 인증 성공시에만 볼 수 있는 화면(hello-oauth2.html) 리턴
}
}
- SecurityContext 객체를 담고 있으며, 이 SecurityContext는 인증된 사용자에 대한 정보를 담고 있는 Authentication 객체를 포함하고 있다.
- 현재 스레드에 대한 보안 관련 정보를 제공하는 데 사용되는 클래스
@GetMapping("/hello-oauth2") // Authentication 객체를 핸들러 메서드 파라미터로 전달받는 방법
public String home(Authentication authentication) {
var oAuth2User = (OAuth2User)authentication.getPrincipal();
System.out.println(oAuth2User);
System.out.println("User's email in Google: " + oAuth2User.getAttributes().get("email"));
return "hello-oauth2";
}
@GetMapping("/hello-oauth2")
public String home(@AuthenticationPrincipal OAuth2User oAuth2User) { // @AuthenticationPrincipal 애너테이션을 이용해 OAuth2User를 파라미터로 전달받는 방법
System.out.println("User's email in Google: " + oAuth2User.getAttributes().get("email"));
return "hello-oauth2";
}
@AuthenticationPrincipal
애너테이션을 이용해 OAuth2User
객체를 직접 전달할 수 있다.
Authorization Server로부터 전달받은 Access Token 확인하는 방법은 아래와 같이 2가지 방법이 있다.
OAuth2AuthorizedClientService
인터페이스
- 권한을 부여받은 Client(
OAuth2AuthorizedClient
)를 관리하는 역할
OAuth2AuthorizedClient
가 Access Token을 보유하고 있음
@RegisteredOAuth2AuthorizedClient
애너테이션
- 컨트롤러 메소드의 매개 변수로 사용됨
- 인증된 사용자와 연관된
OAuth2AuthorizedClient
객체를 자동으로 주입받는다.@GetMapping("/hello-oauth2") // OAuth2AuthorizedClient를 핸들러 메서드의 파라미터로 전달받는 방법 public String home(@RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient) { OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); System.out.println("Access Token Value: " + accessToken.getTokenValue()); System.out.println("Access Token Type: " + accessToken.getTokenType().getValue()); System.out.println("Access Token Scopes: " + accessToken.getScopes()); System.out.println("Access Token Issued At: " + accessToken.getIssuedAt()); System.out.println("Access Token Expires At: " + accessToken.getExpiresAt()); return "hello-oauth2"; }
하나 이상의 핸들러 메서드에서 OAuth2AuthorizedClient
를 사용해야 한다면 OAuth2AuthorizedClientService
를 DI 받아서 사용하는 것이 좋다.
spring-boot-starter-oauth2-client
라이브러리 추가 후, 별도의 설정을 하지 않아도 Spring Boot의 자동 구성을 통해 OAuth2 로그인 인증 기능이 활성화된다.
ClientRegistration
은 OAuth2 시스템을 사용하는 Client 정보를 표현하는 객체다.
Spring Security에서 제공하는 CommonOAuth2Provider
enum은 내부적으로 Builder 패턴을 이용해 ClientRegistration
인스턴스를 제공하는 역할을 한다.
OAuth2AuthorizedClientService
는 권한을 부여받은 Client인 OAuth2AuthorizedClient
를 관리하는 역할을 하는 인터페이스다.
OAuth2AuthorizedClientService
를 이용해서 OAuth2AuthorizedClient
가 보유하고 있는 Access Token에 접근할 수 있다.OAuth2AuthorizedClientService
의 loadAuthorizedClient("google", authentication.getName())
를 호출하면 OAuth2AuthorizedClientRepository
를 통해 OAuth2AuthorizedClient
객체를 로드할 수 있다.