ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OAuth2.0 패스워드 그랜트와 클라이언트 자격 증명 그랜트 구현
    Security 2021. 9. 10. 00:56

    실제 패스워드 그랜트와 클라이언트 자격 증명 그랜트 구현을 시작해 보겠습니다.

    OAuth2의 이론적인 이해가 선행되어야 실제 개발하는데 수월하기에 혹시 앞장을 못 보신 분들은 먼저 읽어보시고 시작하시길 권장드립니다.

    앞으로 진행될 포스팅과 관련된 소스는 아래 Github 주소에서 다운로드 가능합니다.

    URL : hhttps://github.com/chanheejung/OAuth2-PasswordNClientCredential

    • Postman Collection : 소스를 다운로드하시면 Postman 폴더에 Simple OAuth.postman_collction.json 파일이 있습니다. 예제를 실행하는데 필요한 Rest API를 저장해두었으니 편리하게 활용하실 수 있습니다.

     

    구현할 애플리케이션 전체 구성

    우선 구현에 앞서 애플리케이션의 전체 구성을 살펴보도록 하겠습니다.

    실제 업무용으로 제대로 구현하기 위해서는 DBMS 등을 사용하여 좀 더 확장성 있게 구현하여야 하지만, 간단한 예제로 쉬운 이해를 위해 In-Memory 방식으로 고정된 ROLE, APP 정보 등록 방식으로 구현하겠습니다.

     

    앞 장을 보신 분들은 위의 그림을 쉽게 이해하실 수 있을 거라 생각합니다.

     

    리소스 서버는 크게 일반 사용자가 접근할 수 있는 리소스관리자만이 접근할 수 있는 리소스 2 종류의 리소스가 있습니다.

     

    인증 서버에는 리소스 서버의 App ID와 Secret을 In-Memory에 코드로 미리 등록하고

    또한 일반 사용자 1명과 관리자 1명을 인증 서버에 가입시켜 놓도록 하겠습니다.

     

    클라이언트는 애플리케이션을 직접 구현하지 않고 포스트 맨으로 대체하겠습니다. 즉 사용자로부터 ID와 비밀번호를 입력받았다고 가정하고, 포스트맨으로 Access Token을 획득하여 리소스 서버의 자원을 획득할 것입니다.

     

     

    인증(OAuth 2) 서버 구현

    인증 서버 전체 패키지 구조

    구분 클래스(파일명) 설명
    자바 AuthServerApplication 어노테이션을 이용하여 인증 서버임을 명시한다.
    OAuth2Config OAuth2 서버의 설정 처리한다.
    App ID, Seceret 및 인증 타입, Scope를 설정한다.
    WebSecurityConfigurer 사용자에 대한 계정과 ROLE을 정의 한다.
    여기에서는 일반 사용자와 어드민 사용자를 정의 하였다.
    AuthController Access Token 소유자의 정보를 반환하는 간단한 예제 API를 만들었다.
    * 이를 위해 인증서버를 리소스 서버로 등록한다.
    프로퍼티 application.yml 인증서버 포트와 API Prefix를 설정한다.
    라이브러리 build.gradle OAuth2와 Spring Security 등의 라이브러리 의존성을 명시한다.

     

    라이브러리 설정 (build.gradle)

    우선 라이브러리 설정을 합니다. Oauth2를 구현하기 위해서는 Oauth2, Spring Security 라이브러리는 필수입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.cloud:spring-cloud-starter-oauth2'
        implementation 'org.springframework.cloud:spring-cloud-starter-security'
        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testImplementation 'org.springframework.security:spring-security-test'
    }
    cs

     

    AuthServerApplication

    @EnableAuthorizationServer로 인증서버임을 명시합니다.

    @EnableResourceServer는 인증서버의 리소스 서버임을 명시하는 어노 테이션인 데, 인증 서버에서 Access Token 소유자의 정보를 반환하는 간단한 예제 API(리소스)를 구현하였기에 인증서버임과 동시에 자기 자신을 리소스 서버로 등록하였습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @SpringBootApplication
    @EnableResourceServer
    @EnableAuthorizationServer
    public class AuthServerApplication {
        public static void main(String[] args) {
            SpringApplication.run(AuthServerApplication.class, args);
        }
    }
     
    cs

     

    OAuth2Config

    OAuth2의 환경 설정입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Configuration
    public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
     
        @Autowired
        private AuthenticationManager authenticationManager;
     
        @Autowired
        private UserDetailsService userDetailsService;
     
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("service1")
                    .secret("{noop}service1secret")
                    .authorizedGrantTypes("refresh_token""password""client_credentials")
                    .scopes("webclient""mobileclient");
        }
     
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
          endpoints
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService);
        }
    }
    cs

    inMemoryAuthentication()로 서버 내 정보를 In-Memory로 관리하겠다고 명시하였습니다.
    "service1"이라는 서비스의 App ID와 Secret을 등록하였으며, 그랜트 타입으로는 "패스워드 그랜트"와 "클라이언트 권한 부여 그랜트"를 허용하였습니다. 또한 Access Token이 발급 완료되었을 때 재발급할 수 있는 Refresh Token 설정도 하였습니다.

    Scope는 각각의 리소스의 범위 분류합니다. 예제에서는 웹 서비스에서 사용할 수 있는 "webclient" 영역과 모바일 서비스에서 사용할 수 있는 "mobileclient"로 분류하였습니다. 예를 들어 모바일 서비스에서 Http Body의 scope 값에 "webclient"를 설정하여 Access Token을 요청하면, 인증서버는 모바일 리소스가 사용 가능한 토큰을 발급합니다. 하여 이 토큰으로는 웹 영역의 리소스에는 접근할 수 없도록 합니다.

    * 이번 예제에서 클라이언트는 포스트맨으로 대체하기에 scope에 따란 범위 구분 처리는 하지 않았습니다. 별도의 얘기가 더 필요할 듯하여 추후 따로 다루어 보겠습니다.

     

    WebSecurityConfigurer 

    사용자 보안 설정입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
     
       @Override
        @Bean
        public UserDetailsService userDetailsServiceBean() throws Exception {
            return super.userDetailsServiceBean();
        }
     
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
            auth.inMemoryAuthentication()
                    .passwordEncoder(encoder)
                    .withUser("chani").password("{noop}password1").roles("USER")
                    .and()
                    .withUser("chaniadmin").password("{noop}password2").roles("USER""ADMIN");
        }
    }
    cs

    inMemoryAuthentication()로 사용자 정보를 In-Memory로 관리하겠다고 명시하였습니다.
    일반 사용자로 "chani" 계정과 비밀번호를 등록하였고, 관리자(일반 사용자 권한 포함)로 "chaniadmin" 계정과 비밀번호를 등록하였습니다.

     

    AuthController

    Access Token을 소유한 사용자의 상세 정보를 확인할 수 있는 API(리소스)를 구현하였습니다.

    (아래에서 포스트 맨으로 결과를 확인해 보도록 하겠습니다.)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @RestController
    public class AuthController {
     
        @RequestMapping(value = { "/user" }, produces = "application/json")
        public Map<String, Object> user(OAuth2Authentication user) {
            Map<String, Object> userInfo = new HashMap<>();
            userInfo.put("user", user.getUserAuthentication().getPrincipal());
            userInfo.put("authorities", AuthorityUtils
                            .authorityListToSet(user.getUserAuthentication().getAuthorities()));
            return userInfo;
        }
    }
     
    cs

     

    서비스(리소스) 서버 구현

    이번에는 리소스 서버를 구현해 보겠습니다.

    리소스 서버는 인증 서버에 비해 더욱 단순합니다. 여러분에게 친숙한 API 서버에 간단히 스프링 시큐리티로 API별로 사용자용 자원, 관리자용 자원을 설정합니다.

    그리고 프로퍼티를 이용하여 인증(OAuth2) 서버에 연결해 주면 끝입니다.

     

    인증 서버 전체 패키지 구조

    구분 클래스(파일명) 설명
    자바 Service1Application 어노테이션을 이용하여 리소스 서버임을 명시한다.
    ResourceServerConfiguration 스프링 시큐리티로 자원(API)별 인가 권한을 설정 합니다.
    Service1Controller API를 구현합니다.
    프로퍼티 application.yml 인증서버와 연결 설정 합니다.
    라이브러리 build.gradle OAuth2와 Spring Security 등의 라이브러리 의존성을 명시한다.

    * build.gradle의 상세 설정은 인증 서버와 동일함으로 설명은 제외합니다.

     

    Service1Application

    @EnableResourceServer로 리소스 서버임을 명시합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableResourceServer
    public class Service1Application {
        public static void main(String[] args) {
            SpringApplication.run(Service1Application.class, args);
        }
    }
     
    cs

     

    ResourceServerConfiguration

    권한(ROLE) 별로 리소스에 접근할 수 있는 인가 설정을 합니다.

    아래 설정은 모든 요청은 유효한 인증이 되어야 접속이 가능하며, URI가 "/admin/"으로 시작되는 자원은 관리자만 접근 가능하도록 설정하였습니다. 그 외 API 자원은 인증된 일반 사용자가 접근 가능합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Configuration
    public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
                http
                        .authorizeRequests()
                        // "/services/**" 경로는 ADMIN 권한만 접근 가능
                        // 나머지 API는 USER/ADMIN 권한 접근 가능
                        .antMatchers("/admin/**")
                        .hasRole("ADMIN")
                        .anyRequest()
                        .authenticated();
        }
    }
    cs

     

    Service1Controller

    간단한 테스트를 위해 일반 사용자용 API 1개, 관리자용 API 1개를 구현하였습니다. API를 요청하면 단순히 "문자열"을 반환하는 API입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Slf4j
    @RestController
    public class Service1Controller {
        /** 일반 사용자 API */
        @PostMapping(value = "/access-resource")
        public String services() {
            return "Resources";
        }
     
        /** 관리자 API */
        @PostMapping(value = "/admin/access-resource")
        public String adminServices() {
            return "Admin Resources";
        }
    }
     
    cs

    application.yml

    리소스 서버의 포트 설정과 인증 서버의 연결 정보를 설정하였습니다.

    1
    2
    3
    4
    5
    6
    7
    server:
      port: 8011
     
    security:
      oauth2:
        resource:
          userInfoUri: http://localhost:8901/auth/user
    cs

     

     

    포스트맨(클라이언트)으로 동작 확인 (패스워드 그랜트)

    드디어 인증(OAuth2) 서버와 리소스 서버의 구현이 완료되었습니다. 이제  포스트 맨으로 클라이언트를 흉내 내어 Access Token도 발급받고 자원도 획득해 보겠습니다.

     

    GIT에 포함된 Simple OAuth.postman_collction.json 파일을 포스트맨으로 Import 해 봅시다.

     

    Access Token 획득

    사용자의 Access Token을 먼저 획득해 봅시다.

    인증서버로 Access Token을 발급받기 위해 "http://localhost:8901/auth/oauth/token"을 설정합니다.

    그리고 Auth 탭에서 App ID와 Secret을 입력합니다.

     

    다음으로 HTTP BODY에 사용자 ID, 비밀번호, 그랜트 타입, 범위 정보를 입력하고 인증 서버로 전송을 하면

    인증 서버는 보내온 정보가 유효하다면 아래 그림과 같이 Access Token을 발급합니다. (유효하지 않다면 오류 메시지를 전달합니다.)

     

    리소스 접근

    아까 구현할 때 인증서버 또한 리소스 서버로 구현하고, Access Token 소유자의 상세정보를 API로 가져오는 기능을 구현한 것을 기억하실 겁니다. 실제 토큰을 이용해서 상세 정보를 가져와 보겠습니다.

    HTTP HEADER에 "Authorization"이라는 키에 "bearer {발급된 Access Token}"을 담아서 요청합니다.

    다음으로는 동일한 방식으로 리소스 서버의 자원을 가져와 보겠습니다.

    URL은 http://localhost:8011/access-resource로 접속합니다.

    만약 일반 사용자 Access Token으로 관리자 자원(/admin/access-resource)에 접근한다면 권한이 없음이 뜨는 것을 확인하실 수 있습니다.

    같은 방식으로 관리자의 Access Token을 발급받아서 관리자 자원에 접근하는 것도 해보시면 좋을 듯합니다.

     

     

    포스트맨(클라이언트)으로 동작 확인 (클라이언트 권한 부여 그랜트)

    지금까지는 포스트맨 실 사용자를 대신해서 토큰을 발급받고 자원에 접근한 패스워드 그랜트 방식을 실습해 보았습니다. 이제 사용자가 개입하지 않는 클라이언트 권한 부여 그랜트 방식으로 실습해 보겠습니다.

    인증서버를 구현할 때 환경설정 부분을 다시 살펴보시면, 패스워드 그랜트와 클라이언트 권한 부여 그랜트 둘 다 허용하도록 설정하였음을 알 수 있습니다.

    차이점은 Access Token 요청 시에 사용자의 ID와 비밀번호가 필요 없다는 점입니다. 대신 grant_type에는 "client_credentials"을 입력해 줘야겠지요.

     

    마무리

    지금까지 OAuth2 패스워드 그랜트와 클라이언트 권한 부여 그랜트를 구현하고 실습해 보았습니다. 이로서 OAuth2에 대해 어느 정도의 감이 생겼을 듯합니다.

    다음 포스팅에서는 권한 부여 코드 그랜트 (Authorization Code Grant)에 대해 알아보겠습니다. 권한 부여 코드 그랜트는 제 3자(Third Part)에게 중요한 ID와 패스워드를 공개하지 않으면서 리소스 소유자의 자원 사용을 위임할 수 있는 방법으로 기존 그랜트보다는 상대적으로 동작 메커니즘이 복잡합니다. 하지만 위 내용을 충분히 익히셨다면 어렵지 않게 이해하실 거라 생각합니다.

     

     

    참고문헌

    - 스프링 마이크로서비스 코딩 공작소 정성권 옮김 길벗

     

    댓글

Designed by Tistory.