[spring boot + security oauth] 세팅 + 테스트

개요

먼저 OAuth의 개념에 관한 자료는 http://helloworld.naver.com/helloworld/textyle/24942http://earlybird.kr/1584 를 참고하길 바란다. 이 글에서는 개념보다는 Provider쪽 세팅 예제와 테스트를 해보겠다. 테스트는 client_credentials, password, authorization_code 인증 방식을 사용하겠다.

참고 : 예제에서 curl 을 사용하기 때문에 윈도우용 curl을 설치 하시거나 cygwin 혹은 git bash 에서 curl을 사용하셔도 된다.

예제

먼저 STS 플러그인에 있는 Spring Starter Project로 프로젝트를 생성하자.

Web과 Security를 체크하고 생성한 다음, spring-security-oauth2 라이브러리를 추가한다. 그러면 pom.xml은 다음과 같다.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tistory.jekalmin</groupId>
    <artifactId>boot_oauth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>boot_oauth</name>
    <description></description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>com.tistory.jekalmin.Application</start-class>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

다음 Application.java를 아래와 같이 수정한다.


Application.java

package com.tistory.jekalmin;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
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.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Configuration
@ComponentScan
@EnableAutoConfiguration
@RestController
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @RequestMapping("/")
    public String home() {
        return "Hello World";
    }

    @Configuration
    @EnableResourceServer
    protected static class ResourceServer extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                // Just for laughs, apply OAuth protection to only 2 resources
                .requestMatchers().antMatchers("/","/admin/beans").and()
                .authorizeRequests()
                .anyRequest().access("#oauth2.hasScope('read')");
            // @formatter:on
        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId("sparklr");
        }

    }

    @Configuration
    @EnableAuthorizationServer
    protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

        @Autowired
        private AuthenticationManager authenticationManager;

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

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            // @formatter:off
            clients.inMemory()
                .withClient("my-trusted-client")
                    .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                    .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
                    .scopes("read", "write", "trust")
                    .resourceIds("sparklr")
                    .accessTokenValiditySeconds(60)
            .and()
                .withClient("my-client-with-registered-redirect")
                    .authorizedGrantTypes("authorization_code")
                    .authorities("ROLE_CLIENT")
                    .scopes("read", "trust")
                    .resourceIds("sparklr")
                    .redirectUris("http://localhost:8080")
            .and()
                .withClient("my-client-with-secret")
                    .authorizedGrantTypes("client_credentials", "password")
                    .authorities("ROLE_CLIENT")
                    .scopes("read")
                    .resourceIds("sparklr")
                    .secret("secret");
        // @formatter:on
        }

    }

    @Configuration
    @EnableWebMvcSecurity
    protected static class SecurityConfig extends WebSecurityConfigurerAdapter{

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication().withUser("min").password("min").roles("USER");
        }

        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean()
                throws Exception {
            return super.authenticationManagerBean();
        }

    }

}

이러면 서버쪽 세팅은 끝이다. 이제 AccessToken을 받아오자.



인증 방법

  • client_credentials

    my-client-with-secret 라는 클라이언트에 client_credentials 인증방법으로 인증해보자.

      curl -u my-client-with-secret:secret http://localhost:8080/oauth/token -d "grant_type=client_credentials"
    

    결과는 다음과 같았다.

      {"access_token":"4444455c-a8a5-4988-ae61-c4b15080e433","token_type":"bearer","expires_in":43200,"scope":"read"}
    



  • password

    이번엔 my-trusted-client 클라이언트에 password 방법으로 인증해보자.

      curl -u my-trusted-client: http://localhost:8080/oauth/token -d "grant_type=password&username=min&password=min"
    

    결과는 다음과 같다.

      {"access_token":"c09bfac0-274b-4dc6-a15c-ec46d2680d44","token_type":"bearer","refresh_token":"9b5c996c-eede-4111-b3d1-aaff28e5c063","expires_in":49,"scope":"read write trust"}
    



  • authorization_code

    my-client-with-registered-redirect 클라이언트에 authorization_code 방법으로 인증하자.
    이전과는 다르게 AccessToken을 발급 받기 위해서는 권한 승인해서 코드를 가져오는 과정이 필요하다. http://localhost:8080/oauth/authorize?client_id=my-client-with-registered-redirect&response_type=code 로 가서 먼저 등록된 min / min 계정으로 로그인 하면 다음과 같은 창이 뜬다.

    둘 다 허용하고 승인하면 아래 화면으로 이동한다.

    보시는 바와 같이 http://localhost:8080/?code=h6cdAM 로 리다이렉트 된다. 이제 code를 이용해서 AccessToken을 발급 받아보자.

      curl -u my-client-with-registered-redirect: http://localhost:8080/oauth/token -d "grant_type=authorization_code&code=h6cdAM"
    

    결과는 다음과 같았다.

      {"access_token":"1ce32b00-05fa-40f2-a1f5-f6ed2719df4e","token_type":"bearer","expires_in":43199,"scope":"trust read"}
    

어떤 방법이든 인증에 성공했으면, AccessToken을 받급 받았을 것이다. 토큰을 가지고 테스트 하기에 앞서 Application.java에서 설정했던 부분을 다시 살펴보자.

@RequestMapping("/")
public String home() {
    return "Hello World";
}

접근에 성공하면 Hello World가 출력되야 한다.
이제 http://localhost:8080 에 접근해보자. 먼저 토큰을 사용하지 않고 접근해서 보호된 자원임을 확인하자.

C:\Users\Min>curl http://localhost:8080
{"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}

접근이 안된다. 이제 위에서 받았던 AccessToken을 사용해서 접근해보자.

C:\Users\Min>curl -H "authorization: bearer 1ce32b00-05fa-40f2-a1f5-f6ed2719df4e" http://localhost:8080
Hello World

AccessToken을 가지고 요청할 때에만 접근이 되는 것을 확인할 수 있다.

결론

인증 방법에 따라 파라미터로 넘겨야 하는 필수 항목들이 있는데, 최소한의 파라미터로 테스트를 했다. 필수 파라미터들은 http://tutorials.jenkov.com/oauth2/authorization-code-request-response.html 에서 확인할 수 있다.

spring boot를 사용해서 spring security oauth 설정해서 간단해 보이지만, xml 설정을 해보면 TokenEndPoint, TokenStore, Filter 등 설정해야 할 부분이 많다. 그렇게 설정을 해도 결과가 좋지 않았다. 나중에 xml 기반 세팅도 완료 되는데로 포스팅 하겠다.

ps : 예제는 https://github.com/dsyer/sparklr-boot 소스에서 security 세팅을 추가했다.

참고

[spring boot] 로그 설정

간단하지만 매번 까먹어서 새로운 프로젝트 생성할 때마다 찾게 되는 스프링 부트의 로그 설정 방법이다. 물론 resources 밑에 logback.xml에 설정할 수도 있겠지만, 이 글에서는 application.properties에 설정하는 예제이다.

logging.level.org.hibernate.SQL: DEBUG
logging.level.org.springframework: DEBUG

예제와 같이 logging.level 다음에 패키지명으로 설정하면 된다.

spring boot + spring-data-jpa 설정

개요

이전 글인 (http://jekalmin.tistory.com/entry/springdatajpa-%EA%B8%B0%EB%B3%B8-%EC%98%88%EC%A0%9C) 에서 봤듯이 spring-data-jpa 를 설정하려면 xml에 상당히 많은 설정이 필요한 것을 볼 수 있다. 이런 많은 설정들이 spring boot를 사용하면서 기본으로 많이 제공해준다. 예제를 보면서 얼마나 간편해졌는지 확인해보자.

예제

먼저 eclipse를 쓰고 있다면 STS(Spring Tool Suite) 플러그인을 설치하자. 설치하고 나면 New > Project > Spring Starter Project 로 프로젝트를 생성하자.

생성할 때 스타일을 사용할 것인지 선택할 수 있다. 예제에서는 JPA와 Web만 선택하겠다.

먼저 hsqldb 라이브러리를 추가하자.

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
</dependency>

자, 이제 설정 준비가 끝났다. 클래스를 작성해보자.


Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Member {

    @Id
    @GeneratedValue
    private int memberSeq;
    private String name;
    private int age;

    /**
     * 다른 생성자를 만들었다면 기본 생성자를 따로 만들어 주는 것을 잊지말자.
     */
    public Member(){}

    public Member(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    public int getMemberSeq() {
        return memberSeq;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Member [memberSeq=" + memberSeq + ", name=" + name + ", age="
                + age + "]";
    }

}


MemberRepository.java

package com.tistory.jekalmin.repository;

import org.springframework.data.repository.CrudRepository;

import com.tistory.jekalmin.domain.Member;

public interface MemberRepository extends CrudRepository<Member, Integer>{

}


MemberController.java

package com.tistory.jekalmin.controller;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.tistory.jekalmin.domain.Member;
import com.tistory.jekalmin.repository.MemberRepository;

@Controller
public class MemberController {

    @Resource
    private MemberRepository repository;

    @RequestMapping(value="/member/join")
    public void join(Member member){
        System.out.println( repository.save(member) );
    }

}


save 하고나서 저장된 객체를 리턴해주기 때문에 바로 print 찍어보았다.

http://localhost:8080/member/join?name=min&age=26 요청을 해본 결과는 다음과 같았다.

Member [memberSeq=1, name=min, age=26]

결론

이전의 어마어마한 세팅들이 다 어디갔나 싶을 정도로 boot에서는 기본 설정이 많이 내장되어 있는 것 같다. boot를 이용해서 만들면 설정은 hsqldb 라이브러리 추가하는 것이 전부였고, entity와 repository를 만들어서 바로 사용하니 된다.

설정을 변경할 필요가 있다면 지원하는 어노테이션을 검색해 봐야 겠지만, 기본 설정하나는 정말 쉬워진 것 같다.

[Spring Boot] properties 등록하기 (@PropertySource)

개요

Spring Boot로 프로젝트를 실행하면 기본적으로 application.properties를 참조하여 스프링이 실행 되지만, 스프링에 관련 없는 properties는 따로 설정파일을 만들어 빼고 싶은데, xml 설정파일이 없으니 어디에 추가해야 할지 막연했다. 검색을 하다보니 어노테이션에 properties 파일을 등록하면 사용이 가능했다.

예제

먼저 config.properties라는 파일을 resource 폴더에 생성했다.


config.properties

name=jekalmin


그리고 Application.java에 어노테이션을 이용하여 등록해주었다.


Application.java

package com.tistory.jekalmin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@ComponentScan
@EnableAutoConfiguration
@PropertySource("config.properties")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

마지막으로 컨트롤러를 생성하여 잘 가져오는지 테스트하였다.


TestController.java

package com.tistory.jekalmin;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/test")
public class TestController {

    @Value("${name}")
    String name;

    @RequestMapping("/index")
    public void index(){
        System.out.println("name : " + name);
    }
}


결과는 다음과 같았다.

name : jekalmin

config.properties가 무사히 스프링에 등록된 것을 확인할 수 있다.