Spring Boot Security OAuth 2.0

Minjun Kang·2022년 7월 5일
0
post-thumbnail

OAuth는 프로토콜로 규약되어 있으며, 그와 관련된 자세한 내용은 아래에서 확인해볼 수 있습니다.
참고

OAuth란?

OAuth("Open Authorization")는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.

회원가입 & 로그인 로직을 구현할 때, 외부 플랫폼으로부터 리소스를 가져와서 해당 리소스를 이용하여 특정 기능을 수행하고 싶을 때 사용한다.

OAuth 인증을 통하면 신뢰할 수 있는 플랫폼이 인증과 리소스에 대한 권한을 외부 플랫폼에 부여하므로서 사용자는 추가적으로 개인정보를 입력하지 않고도 이전에 입력된 정보를 활용할 수 있다는 사용자 경험적 장점도 존재한다.

OAuth2.0 용어

  1. Resource Owner
    • 민감 정보에 대한 접근 권한이 있는 Entity (일반적으로 사용자)
    • Resource Server에 자신의 정보를 저장한 주체
  2. Resource Server
    • 민감 정보를 보유하고 있는 서버 (ex. 구글, 네이버, ...)
    • access token 과 함께 온 민감 정보 Request에 대한 Respond를 담당하는 서버
  3. Client
    • Resource Owner와 Resource Server 사이의 애플리케이션 (일반적으로 third-party app)
    • Resource Server에서 제공해주는 자원을 사용하는 외부 플랫폼
  4. Authorization Server
    • 인증 과정이 성공하면, access token을 Client에게 발행해주는 서버
  5. Access token
    • Resource Server에 Resource를 요청할 수 있는 token
    • 유효기간 존재, 만료시 Refresh token 방식으로도 재발급 가능
  6. Authorization Grant
    • Resource Owner의 권한(authorization)을 표현하는 기밀 정보
    • Client가 Access token을 얻기 위해 필요한 정보
    • 이를 4가지 타입으로 정의할 수 있음
      1. Authorization Code (권한 코드 승인 방식)
      2. Implicit (암시적 승인 방식)
      3. Resource Owner Password Credentials (비밀번호 자격 증명 방식)
      4. Client Credentials (클라이언트 자격 증명 방식)

OAuth 2.0 Protocol Flow

Authroiztion Code 방식

  1. ClientResource Owner에게 특정 자원을 얻기 위한 권한을 얻기 위한 Request(간접적으로도 가능)

  2. Resource OwnerClient에게 자원을 가져올 권한을 부여한다. 이 과정에서 credential한 authorization grantClient가 수신한다.

  3. ClientAuthorization Server에게 Access token을 요청한다. 이 때, Request 메세지에 2번 과정에서 받은 Authorization grant가 포함된다.

  4. Authorization ServerClient에게 받은 Authorization grant가 정당하다면 Access token을 발행하여 전달한다.

  5. ClientAccess token을 이용하여 Resource Server에게 자원을 요청한다.

  6. Resource ServerAccess token이 정당하다면, 요청된 자원을 전달한다.

Authorization Grant

  1. Authorization Code
    • clientresource owner 사이에서 중계를 하는 authorization server를 이용하여 Authorization GrantAccess token이 교환되는 방식.
    • 서버 사이드 방식의 인증 처리
    • Access token이 간접적으로 Client에게 전달되기 때문에 다른 3가지 방식보다 보안 측면에서 좋다.
  2. Implicit
    • Authorization code 없이 바로 access token을 발급하여 사용하는 방식
    • 서버와의 연동이 없는 Application에서 주로 사용
    • 클라이언트 사이드 방식의 인증 처리
    • Authorization code validation이 포함되지 x
  3. Resource Owner Password Credentials
    • Client에 Service provider(ex. 구글, 네이버, ...)의 ID와 PW를 저장해두고 사용하는 방식
    • Client와 Service Provider의 관계가 매우 밀접할 때, 사용하는 것이 권장됨
  4. Client Credentials
    • Client = Resource Owner 일 때 사용하는 인증 방식
    • 추가적인 Authorization Request/Grant 과정이 불필요
    • Authorization Server로부터 access token 발급

아래부터는 Authorization Code방식의 Authorization Grant에 대한 내용만 기술한다.
(추가 예정)

실제 구현체

일반적으로 웹 애플리케이션은 다음과 같이 OAuth 2.0 프로토콜을 구현한다.

초기.

  • Resource Server: client-id, client-secret, redirect_url
  • Client: client-id, client-secret
    에 대한 정보를 가지고 있음

1-1. 인증 요청

  • 사용자가 로그인 폼에 접속하였을 때, Client는 사용자에게 OAuth 2.0 로그인을 지원하는 폼을 제공
  • 해당 폼에는 인증과정 이후에 client 자신에게로 리다이렉트하는 url를 포함
  • 요청을 보내는 사이트를 구분하기 위한 client-id, 정보의 범위인 scope가 query parameter에 포함
  • ex.
    https://server.authorization/?client-id=1&scope=email,name&redirect_url=https://client/callback

1-2. Resource Owner는 리다이렉트된 url로 접속하여 로그인

  • Authorization Server에서 Resource Owner에 대한 자체 인증 과정
  • 로그인에 성공한다면, 1-1과정의 Request에 포함된 client-id, scope, redirect_url 등을 검증
  • 검증이 완료될 경우, Authorization Code를 발행하여 Resource OwnerResource Server임시저장
  • Authorization Code 는 임시 비밀번호의 역할을 한다

2-1. Resource Server는 발급된 Authorization Code를 Client에게 논리적으로 전달

  • Resource Server는 request에 포함된 client에 대한redirect_urlResource Owner에게 전달

2-2. Resource Owner는 리다이렉트 주소로 접속

  • Client 또한 Authorization Code의 정보를 알게됨.

3-1. Client가 Access token 발급을 위해 Authorization Server에게 Request

  • 이 때, 포함되는 정보는 Authorization Code, client-id, client-secret, redirect_url

4-1. Authorization Server가 Request에 대한 검증과정 수행

  • Authorization Code를 통해 Auth-server가 가진 client-id, client-secret, Redirect-url 비교
    - client-secret은 절대 노출되선 안된다.
  • 만약 모든 정보가 일치한다면, Access token 발행 후 Authorization Code 폐기
    - Authorization Code를 폐기하는 이유는 Replay 공격을 막기 위함
  • 발행된 Access token을 Response로 전달

5-1. 이 후, 요청되는 자원들에 대해서는 Access token을 통해 인증을 수행할 수 있음

  • 인증이 필요한 API들에 대해서 2가지 방식을 통해 이용 가능
    1. query_parameter에 access_token 값 지정 (depreciated)
      - ex) https://apis/v1/cal?access_token={}
    1. http header에 Authorization: Bearer <access_token> 값 지정

Spring Boot OAuth2.0 구현

  1. OAuth2.0 을 이용하기 위한 OAuth2.0 Client-ID 발행

    • 발행 이후, Client Application에 Client-id와 Client-secret 지정
    • application-oauth.yaml 파일 생성 후, 관련 property 정의
    • 이것을 기준으로 OAuth2ClientProperties 라는 객체 생성 후 Bean 등록
    • 이 후 OAuth2ClientPropertiesRegistrationAdapter 를 통해 각 서버마다 ClientRegistration 생성
    • 이 정보를 In-memory에 저장.
    • Common-OAuth2Provider enum class에 정의된 서비스들에 대해서는 client-id, client-secret만 지정해주면 됨
    • 그 외의 서비스들은 redirect-url, authorization_grant, scope, client_name 등의 필드 값들도 지정해주어야함.
  2. application.yaml에 oauth 관련 profile 등록

    spring:
        (생략)
        profiles:
            include: oauth # application-oauth.yaml 관련 설정 사용 위함
    	* spring-boot에서는 `application-xxx.yaml` 로 profile을 지정할 경우, `xxx`만으로 값들을 가져올 수 있다.

  1. 의존성 추가
# build.gradle
dependencies {
	...
    implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
}

  1. 도메인 정의 (User)
    • OAuth 로그인을 통해 얻어온 정보를 로컬 DB에 저장하기 위한 도메인을 정의
    • OAuth로 얻어올 수 있는 정보를 포함하는 Entity로 매핑한다.
    • Spring Security를 사용한다면, Role에 따른 권한 부여 과정이 필요
package com.fakedevelopers.bidderbidder.model.user;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import javax.validation.constraints.Email;

@Getter
@NoArgsConstructor
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Email
    @Column(nullable = false)
    private String email;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role; // TODO : OAuth2을 이용하여 Role별로 접근 권한 설정

    @Builder
    public User(String name, String email, Role role) {
        this.name = name;
        this.email = email;
        this.role = role;
    }

    public User update(String name) {
        // 추후 사용자의 요구에 따라 이름 변경 가능.
        this.name = name;
        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}
  1. Role 정의 (with. Enum)
package com.fakedevelopers.bidderbidder.model.user;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
/* Spring Security는 Role 별로 권한 설정 가능 */
public enum Role {
    CUSTOMER("ROLE_CUSTOMER", "구매자"),
    SELLER("ROLE_SELLER", "판매자"),
    ADMIN("ROLE_ADMIN","관리자"),
    GUEST("ROLE_GUEST", "비회원");



    private final String key;
    private final String title;
}
  1. Security Config 파일 작성
package com.fakedevelopers.bidderbidder.config.oauth;


import com.fakedevelopers.bidderbidder.model.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomOAuth2UserService customOAuth2UserService;

    // 필터링 규칙
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .authorizeRequests() // URL 별 권한 관리를 설정하는 옵션의 시작점
                .antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
                // .antMatchers("권한이 필요한 페이지").hasRole(Role.CUSTOMER.name())  { TODO }
                // .anyRequest().authenticated()
                .and()
                .logout()
                .logoutSuccessUrl("/")

                .and()
                .oauth2Login()
                .userInfoEndpoint()// 로그인 성공 후 사용자 정보 가져올 때 설정
                .userService(customOAuth2UserService); // 이 후 호출할 Service 루틴
    }
}
  • Spring에서 OAuth2를 사용하기 위해서는 Security 기능을 적용해야함.
  • Spring Security의 필터링 규칙을 위와 같이 정의 가능
  • CustomOAuth2UserService 인터페이스에 맞게 서비스 로직 작성
  1. CustomOAuth2UserService 작성
package com.fakedevelopers.bidderbidder.config.oauth;

import com.fakedevelopers.bidderbidder.config.oauth.dto.OAuthAttributes;
import com.fakedevelopers.bidderbidder.config.oauth.dto.SessionUser;
import com.fakedevelopers.bidderbidder.model.user.User;
import com.fakedevelopers.bidderbidder.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;
import java.util.Collections;

@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        // 현재 진행중인 서비스를 구분하기 위해 String으로 값을 가져옴 ex.) Naver, Google ....
        String registrationId = userRequest.getClientRegistration().getRegistrationId();

        // OAuth2 로그인 성공시, 키 값이 된다. 인증 서버(ex. 네이버, 구글)에 따라 키 이름이 달라질 수 있으므로 변수로 가져옴
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

        // OAuth 로그인을 통해 가져온 OAuth2User의 attribute를 담아주는 of 메소드 (Dto로 취급)
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        // 사용자의 외부 프로필에 업데이트가 있을 경우, 어플리케이션에도 반영
        // 만약, 새로운 사용자라면 DB에 저장
        User user = saveOrUpdate(attributes);

        // 사용자 정보를 Session에 저장.
        // Session User는 Serializable한 Dto임
        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
                attributes.getAttributes(),
                attributes.getNameAttributeKey()
        );
    }

    private User saveOrUpdate(OAuthAttributes attributes) {
        User user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName()))
                .orElse(attributes.toEntity());
        return userRepository.save(user);
    }
}
  • OAuth2UserService<OAuth2UserRequest, OAuth2User> 스펙에 맞게 작성
  • OAuth2UserRequest에는 RegistrationID, accessToken, parameter가 포함
  • OAuth2User에는 email, name, ..., 등의 사용자 정보가 포함
  • OAuth2UserDefaultOAuth2UserServiceloadUser(OAuth2UserRequest)로 가져올 수 있음.
  • OAuth2UserOAuth2UserRequest를 통해 OAuth에 종속적이지 않은 dto(User)를 정의할 수 있음
  • OAuth 로그인 마다 profile의 변경 또는 수정이 있을 시 변경 사항을 적용하기 위한 saveOrUpdate 정의
  1. API테스트
  • 스프링 OAuth의 기본 경로는 /oauth2/authorization/{RegistrationID} 이다.
  • 기본 경로로 지정할 경우, 별도의 Controller 정의가 필요없다.
profile
성장하는 개발자

0개의 댓글