ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot - OAuth2.0 password 방식으로 간단한 인증서버 구현하기
    Spring boot 2020. 1. 9. 23:10

    1.개요

    1-1.프로젝트 환경

    지난 시간에는 OAuth2.0의 개념을 알아 봤다.
    이번에는 Spring Boot로 간단하게 OAuth 2.0 인증서버를 구현 해 볼 것이다.
    authorization grant type은 password grant 방식으로 할 것이다.

    -   인텔리제이 커뮤니티
    
    -   스프링부트 2.1.4
    
    -   gradle 5.2.1
    
    -   postman을 통한 테스트

    2.소스

    2-0 application.yml.

    server:
      port: 8081
    spring:
      h2:
        console:
          enabled: true
          settings:
            web-allow-others: true
      jpa:
        database-platform: org.hibernate.dialect.H2Dialect
        properties.hibernate.hbm2ddl.auto: update
        showSql: true

    port는 8081로 했고 db는 h2를 사용했다.

    2-1 build.gradle.

    plugins {
        id 'org.springframework.boot' version '2.1.4.RELEASE'
        id 'java'
    }
    
    apply plugin: 'io.spring.dependency-management'
    
    group = 'com.rest'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '1.8'
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.cloud:spring-cloud-starter-security:2.1.2.RELEASE'
        implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.1.2.RELEASE'
        implementation 'com.google.code.gson:gson'
        compileOnly 'org.projectlombok:lombok'
        runtimeOnly 'com.h2database:h2'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

    2-2 Oauth2AuthorizationConfig.

    package com.rest.oauth2.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    
    @EnableAuthorizationServer
    @Configuration
    public class Oauth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory() .withClient("root")
                    .secret(passwordEncoder.encode("root"))
                    .authorizedGrantTypes("authorization_code", "password", "refresh_token")
                    .scopes("read", "write") .accessTokenValiditySeconds(60*60)
                    .refreshTokenValiditySeconds(6*60*60) .autoApprove(true);
        }
    
        @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(tokenStore)
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService);
        }
    }
    
    

    해당 클래스는 Authorization Server 역할을 할 클래스이다
    @EnableAuthorizationServer 어노테이션을 통해 oauth 서버에 필요한 기본 설정을 셋팅 할 수 있다.
    AuthorizationServerConfigurerAdapter 를 상속 받아 configure 메소드를 override 했다.

    configure 메소드는 파라미터에 따라서 세 가지 종류를 설정 할 수 있다.

        @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory() .withClient("root")
                    .secret(passwordEncoder.encode("root"))
                    .authorizedGrantTypes("authorization_code", "password", "refresh_token")
                    .scopes("read", "write") .accessTokenValiditySeconds(60*60)
                    .refreshTokenValiditySeconds(6*60*60) .autoApprove(true);
        }

    1.ClientDetailsServiceConfigurer
    이름 그대로 클라이언트에 대한 설정을 할 수 있다.
    해당 소스에서는 inmemory 방식으로 설정 했고, 클라이언트의 아이디와 비밀번호를 root로 줬다.
    authorization grant type은 password 방식으로 설정했다. 또한 토큰의 유효시간 등을 설정해줬다.
    이런 소스를 보면 항상 막막 했는데 자세히 보면 정말 별거 없는 내용들이다
    어려워 하지말고 그대로 읽으면서 아 inmemory 방식이구나. 클라이언트 이름 root로 했구나. 이런식으로 읽으면서 넘어가자

        @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(tokenStore)
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService);
        }

    2.AuthorizationServerEndpointsConfigurer
    인증, 토큰엔드포인트, 토큰 서비스를 정의 할 수 있다.

    여기선 구현 안함

    3.AuthorizationServerSecurityConfigurer
    토큰 엔드포인트에 대한 보안관련 설정을 할 수 있다.

    여기서는 ClientDetailsServiceConfigurer와 AuthorizationServerEndpointsConfigurer를 파라미터로 받는 configue 메소드를 오버이드 했다. 위에서 tokenStore, userDetailsService 등 객체를 빈으로 가져와 설정에 등록해주고 있는데, 이 빈들은 뒤에 나올 SecurityConfig 에서 등록할 것이다.

    2-2.ResourceServerConfig

    package com.rest.oauth2.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;
    
    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.anonymous().disable()
                    .authorizeRequests()
                    .antMatchers("/users/**").authenticated()
                    .and()
                    .exceptionHandling()
                    .accessDeniedHandler(new OAuth2AccessDeniedHandler());
        }
    }
    

    구글링을 해서 다른 예제들을 찾아보면 하나의 클래스가 Authorization Server와 Resource Server의 역할을 다하도록 구현한 경우도 있다. 여기서는 둘을 구분해서 구현 하려고 한다. Spring OAuth 에서는 자원에 대한 보호를 구현하는 Spring Security Filter 를 제공한다. @EnableResourceServer 어노테이션을 붙임으로서 해당 필터를 활성화 할 수 있다.
    이 설정에서는 각 리소스(여기에서는 user 정보)에 대한 접근 권한을 설정해주고 있다.
    authorizeRequests는 URL별 권한 관리를 설정하는 옵션의 시작점이다.
    authorizeRequests가 선언되어야만 antMatchers 옵션을 사용 할 수 있다.
    antMatchers는 권한 관리 대상을 지정하는 옵션이다 URL,HTTP 메소드 별로 관리가 가능하다.
    여기서는 "/users/**" 주소를 가진 API는 인증된 사용자에게만 허용하게 했다

    2-3.SecurityConfig

    package com.rest.oauth2.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.factory.PasswordEncoderFactories;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
    
    import javax.annotation.Resource;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Resource(name="userService")
        private UserDetailsService userDetailsService;
    
        @Bean
        @Override
        protected AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
        @Bean
        public PasswordEncoder encoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    
        @Bean
        public TokenStore tokenStore() {
            return new InMemoryTokenStore();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService) .passwordEncoder(encoder());
        }
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .cors()
                        .and()
                    .csrf()
                        .disable()
                    .anonymous()
                        .disable()
                    .authorizeRequests()
                        .antMatchers("/api-docs/**")
                    .permitAll();
        }
    }

    서버에 대한 전반적인 security 설정을 한다. 대부분 Spring Security 와 관련된 설정들이며, OAuth 관련 설정은 TokenStore 빈을 등록해주는 부분이다. 여기서는 inMemory 토큰을 사용하는 것으로 설정했다. UserDetailsService는 Spring Security 에서 제공하는 인터페이스로, 해당 인터페이스를 구현해 회원정보를 관리할 수 있다.

    2-4.User

    package com.rest.oauth2.user;
    
    import com.sun.javafx.beans.IDProperty;
    import lombok.Data;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    
    @Entity
    @Data
    public class User {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String username;
    
        private String password;
    
    
    }
    

    User의 정보를 담을 엔티티다

    2-4.UserController

    컨트롤러.

    package com.rest.oauth2.user;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @GetMapping("/user")
        public List<User> listUser() {
            return userService.findAll();
        }
    
        @PostMapping("/user")
        public User create(@RequestBody User user) {
            return userService.save(user);
        }
    
    }
    

    2-4.UserService

    UserService

    위에서 언급한 UserDetailsService 를 구현하는 UserService 를 만들어준다. 또한 처음 빈 등록 시 User 객체 하나를 생성해준다. (테스트를 위해)

    package com.rest.oauth2.user;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.PostConstruct;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.List;
    
    @Service
    public class UserService implements UserDetailsService {
        @Autowired
        private UserRepository userRepository;
    
        @Autowired private PasswordEncoder passwordEncoder;
    
        public List<User> findAll() {
            return userRepository.findAll();
        }
    
        public User save (User user) {
            user.setPassword(passwordEncoder.encode(user.getPassword()));
            return userRepository.save(user);
        }
        @PostConstruct
        public void init(){
            User autumn = userRepository.findByUsername("autumn");
            if(autumn == null){
                User user = new User();
                user.setUsername("autumn");
                user.setPassword("pass");
                System.out.println(this.save(user));
            }
        }
        @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepository.findByUsername(username);
            return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthorities());
        }
    
        private Collection<? extends GrantedAuthority> getAuthorities() {
            return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
        }
    }
    
    

    2-4.UserRepository

    package com.rest.oauth2.user;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface UserRepository extends JpaRepository<User, Long> {
    
        User findByUsername(String username);
    }
    

    3-1.테스트

    스프링부트 어플리케이션을 실행시키고 테스트는 Postman을 이용했다.
    테스트 방식에 curl을 이용하는 방법도 있지만 나는 윈도우고 윈도우는 또 따로 설정을 해줘야 해서 postman을 활용했다
    postman 사용법은 구글링을 하면 많이 나오니 생략하겠다

    순서는 이렇다.
    1.토큰 정보 요청. (postman)
    2.서버가 토큰 줌
    3.토큰 가지고 서버에 리소스 요청 (postman)
    4.서버가 리소스 줌.
    의 순서로 진행 된다.

    해당 테스트에 대한 내용은 https://autumnly.tistory.com/65?category=813931를 참조 하면 된다.

    출처

    https://autumnly.tistory.com/65?category=813931


    'Spring boot' 카테고리의 다른 글

    JWT  (0) 2019.12.26
    OAuth2.0  (0) 2019.12.24
    스프링부트-mustache 연동시 404에러 뜰 때.  (0) 2019.12.03

    댓글

Designed by Tistory.