아카이브

[스프링 기반 REST API 개발] 시큐리티 OAuth2 인증 서버 설정하기 본문

Spring/스프링 기반 REST API 개발

[스프링 기반 REST API 개발] 시큐리티 OAuth2 인증 서버 설정하기

주멘이 2021. 1. 9. 20:46

OAuth2(Open Authorization, Open Authentication 2) : 인증을 위한 표준 프로토콜

PAYCO OAuth2 프로세스 설명 (Authorization Code Grant Type 방식)

  • 1~5 단계는 Authorization Code 발급 요청 URL을 통해 진행할 수 있습니다.
  • 7~8 단계는 서비스에서 callback URL을 통해 전달받은 Authorization Code를 사용하여 Access Token 요청 API를 통해 진행할 수 있습니다.
  • 8 단계에서 발급받은 Access Token은 서비스에서 자체적으로 저장, 관리해야 합니다.
  • 10~11 사용자의 서비스 요청 시 회원정보가 필요하다면 Access Token을 사용해 API를 호출할 수 있습니다.

OAuth2 서버를 통해 인증 토큰을 받는다.

토큰 발행 테스트

  • User
  • Client
  • POST /oauth/token
    • HTTP Basic 인증 헤더 (클라이언트 아이디 + 클라이언트 시크릿)
    • 요청 매개변수 (MultiValueMap<String, String>)
      • grant_typepassword
      • username
      • password
    • 응답에 access_token 나오는지 확인

Grant Type: Password

 

What is the OAuth 2.0 Password Grant Type?

The Password Grant Type is a way to get an OAuth access token given a username and password.

developer.okta.com

 

공식 문서 

  • (A) 사용자가 클라이언트에 username과 password를 전달한다.
  • (B) 사용자의 username과 password를 OAuth 서버에 제공한다.
  • (C) OAuth 서버에서 확인해 유효하면 access token을 발급해준다.(선택적으로 refresh token)
  • Granty Type: 토큰 받아오는 방법
  • 서비스 오너가 만든 클라이언트에서 사용하는 Grant Type
  • 요청과 응답이 한쌍으로 토큰을 바로 발급받을 수 있다.(장점)
  • username과 password를 직접 요구하므로 third-party에게 허용해서는 안되고, 본인 서비스에서만 허용해야 한다.

요청

POST /oauth/token HTTP/1.1
Host: authorization-server.com
Content-type: application/x-www-form-urlencoded

grant_type=password
&username=exampleuser
&password=1234luggage
&client_id=xxxxxxxxxx
  • grant_type=password : password 유형을 사용하는 서버에 알려줍니다.
  • username : 응용 프로그램에 입력 한 사용자 이름
  • password : 응용 프로그램에 입력 한 사용자의 암호
  • client_id : 개발자가 등록 과정에서 얻은 응용 프로그램의 공용 식별자
  • client_secret : client_id와 연결된 secret(비밀번호)

client_id와 client_secret은 HTTP Basic Authentication 인증 헤더로 넣는다.

응답

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
  "token_type":"bearer",
  "expires_in":3600,
  "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
  "scope":"create delete"
}

AuthorizationServer 설정

  • @EnableAuthorizationServer
  • extends AuthorizationServerConfigurerAdapter
  • configure(AuthorizationServerSecurityConfigurer security)
    • PassswordEncoder 설정
  • configure(ClientDetailsServiceConfigurer clients)
    • 클라이언트 설정
    • grantTypes
      • password
      • refresh_token: OAuth Token을 발급받을 때 refresh_token도 같이 발급해주는데 이 토큰으로 새로운 액세스 토큰을 발급받음
    • scopes
    • secret / name
    • accessTokenValiditySeconds
    • refreshTokenValiditySeconds
  • AuthorizationServerEndpointsConfigurer
    • tokenStore
    • authenticationManager
    • userDetailsService

HTTP Basic을 사용하기 위해 의존성 추가

        <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-test -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <version>5.4.2</version>
            <scope>test</scope>
        </dependency>

HTTP Basic이란? HTTP 표준에 정의된 가장 단순한 인증 기법이다.

Client는 ID:PASSWOD 문자열을 Base64 Encoding 한 문자열을 Authentication Header에 추가한 뒤 요청하고 200 OK 응답을 받는다.

파라미터가 쉽게 노출될 수 있지만, SSL이나 TLS로 노출을 막을 수 있다.

 

AuthServerConfig @Configuration class 작성하기

@Configuration
@EnableAuthorizationServer /* OAuth2 인증 서버 */
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    AccountService accountService;

    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.passwordEncoder(passwordEncoder);  
        // client secret도 password encode 관리
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()	// 원래는 jdbc db로 생성해야 한다
                .withClient("myApp")	// client_id 생성
                .authorizedGrantTypes("password", "refresh_token")	// 지원하는 type
                .scopes("read", "write")	// scope 설정
                .secret(this.passwordEncoder.encode("pass")) // client_secret encoding
                .accessTokenValiditySeconds(10 * 60)	// 유효시간
                .refreshTokenValiditySeconds(6 * 10 * 60)	// 유효시간
        ;

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)	// 우리의 auth_manager로 설정하고
                .userDetailsService(accountService)	// 우리의 userDetailService인 accountService를 설정하고
                .tokenStore(tokenStore)	// 우리의 tokenStore를 설정
        ;
    }
}

 

getToken() Test 코드 작성하기

    @Test
    @TestDescription("인증 토큰을 발급 받는 테스트")
    void getAuthToken() throws Exception {
        // Given
        String username = "jumen@naver.com";
        String password = "5215";

        Account build = Account.builder()
                .email(username)
                .password(password)
                .roles(Set.of(AccountRole.ADMIN, AccountRole.USER))
                .build();
        this.accountService.saveAccount(build);

        String clientId = "myApp";
        String clientSecret = "pass";


        this.mockMvc.perform(post("/oauth/token")
                        .with(httpBasic(clientId, clientSecret))	// BasicAuth 방식으로 auth_header에 넣어준다
                        .param("username", username)
                        .param("password", password)
                        .param("grant_type", "password")
                )

                .andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("access_token").exists())
        ;
    }