├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── src ├── main │ ├── resources │ │ ├── data.sql │ │ ├── application.yml │ │ └── schema.sql │ └── java │ │ └── com │ │ └── example │ │ └── oauth │ │ ├── OauthApplication.java │ │ ├── user │ │ ├── repository │ │ │ └── UserRepository.java │ │ └── User.java │ │ └── config │ │ ├── AppConfig.java │ │ ├── H2ServerConfiguration.java │ │ ├── ResourceServerConfigurerAdapterImpl.java │ │ └── AuthorizationServiceConfigurerAdapterImpl.java └── test │ └── java │ └── com │ └── example │ └── oauth │ └── OauthApplicationTests.java ├── .gitignore ├── README.md ├── pom.xml ├── mvnw.cmd ├── mvnw └── docs ├── step-1:oauth1,oauth2란.md └── step-2:spring-oauth2-구현.md /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minwan1/spring-security-oauth2-example/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | 5 | install: true 6 | 7 | script: mvn install 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | after_success: 14 | - mvn clean cobertura:cobertura coveralls:report 15 | -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO PUBLIC.OAUTH_CLIENT_DETAILS (CLIENT_ID, RESOURCE_IDS, CLIENT_SECRET, SCOPE, AUTHORIZED_GRANT_TYPES, WEB_SERVER_REDIRECT_URI, AUTHORITIES, ACCESS_TOKEN_VALIDITY, REFRESH_TOKEN_VALIDITY, ADDITIONAL_INFORMATION, AUTOAPPROVE) VALUES ('foo', '', 'bar', 'read', 'password,authorization_code,refresh_token', '', '', null, null, '{}', ''); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /src/main/java/com/example/oauth/OauthApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class OauthApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(OauthApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth/user/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth.user.repository; 2 | 3 | import com.example.oauth.user.User; 4 | import org.springframework.data.repository.PagingAndSortingRepository; 5 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 6 | 7 | @RepositoryRestResource 8 | public interface UserRepository extends PagingAndSortingRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth/user/User.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth.user; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.Id; 8 | 9 | 10 | @Entity 11 | @Getter 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class User { 14 | 15 | 16 | @Id 17 | @GeneratedValue 18 | private Long id; 19 | private String name; 20 | private String username; 21 | private String remark; 22 | 23 | @Builder 24 | public User(String name, String username, String remark) { 25 | this.name = name; 26 | this.username = username; 27 | this.remark = remark; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #MYSQL 2 | #spring: 3 | # datasource: 4 | # url: jdbc:mysql://localhost:3306/oauth 5 | # driverClassName: com.mysql.jdbc.Driver 6 | # username: root 7 | # password: 8 | # jpa: 9 | # hibernate: 10 | # ddl-auto: update 11 | 12 | #H2 13 | spring: 14 | datasource: 15 | url: jdbc:h2:tcp://localhost:9092/mem:testdb;MVCC=TRUE 16 | driverClassName: org.h2.Driver 17 | username: root 18 | password: 19 | jpa: 20 | hibernate: 21 | ddl-auto: update 22 | 23 | spring.h2.console: 24 | enabled: true 25 | path: /h2-console 26 | 27 | 28 | 29 | # 사용 30 | security: 31 | user: 32 | name: user 33 | password: test 34 | 35 | logging: 36 | level: 37 | ROOT: error 38 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth.config; 2 | 3 | import com.example.oauth.user.User; 4 | import com.example.oauth.user.repository.UserRepository; 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class AppConfig { 11 | 12 | @Bean 13 | public CommandLineRunner commandLineRunner(UserRepository userRepository) { 14 | return args -> { 15 | userRepository.save(new User("테스트1", "test1", "test111")); 16 | userRepository.save(new User("테스트2", "test2", "test222")); 17 | userRepository.save(new User("테스트3", "test3", "test333")); 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth/config/H2ServerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth.config; 2 | 3 | import org.apache.tomcat.jdbc.pool.DataSource; 4 | import org.h2.tools.Server; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import java.sql.SQLException; 10 | 11 | 12 | //@Profile("local") for local 13 | @Configuration 14 | public class H2ServerConfiguration { 15 | 16 | @Bean 17 | @ConfigurationProperties("spring.datasource") 18 | // yml의 설정값을 Set한다. 19 | public DataSource dataSource() throws SQLException { 20 | Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092").start(); 21 | return new org.apache.tomcat.jdbc.pool.DataSource(); 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/minwan1/spring-security-oauth2-example.svg?branch=master)](https://travis-ci.com/minwan1/spring-security-oauth2-example) 2 | [![Coverage Status](https://coveralls.io/repos/github/minwan1/spring-security-oauth2-example/badge.svg)](https://coveralls.io/github/minwan1/spring-security-oauth2-example) 3 | 4 | # Spring-Security-OAuth2-example 5 | Spring-Security-OAuth2 구현 예제입니다. 6 | 7 | # 개발환경 8 | * Spring boot 1.5.9 9 | * Java 8 10 | * Mockito, 11 | * Spring REST 12 | * JPA 13 | 14 | 15 | 16 | # 문서 17 | 18 | 1. [step1 : oauth1, oauth2란](https://github.com/minwan1/spring-security-oauth2-example/blob/master/docs/step-1%3Aoauth1%2Coauth2%EB%9E%80.md) 19 | 2. [step2 : spring-security-oauth2구현(in-memory, jdbc방식을 사용한 예제)](https://github.com/minwan1/spring-security-oauth2-example/blob/master/docs/step-2%3Aspring-oauth2-%EA%B5%AC%ED%98%84.md) 20 | 21 | 22 | # 브랜치 23 | 24 | * [example-1 : in-memory 방식을 사용한 OAuth2 구현](https://github.com/minwan1/spring-security-oauth2/tree/example-1) 25 | * [example-2 : JDBC 방식을 사용한 OAuth2 구현](https://github.com/minwan1/spring-security-oauth2/tree/example-2) 26 | 27 | # 실행 28 | ``` 29 | $ mvn spring-boot:run 30 | ``` 31 | 32 | # Test 방법 33 | ``` 34 | Spring-oauth/src/test/java/com/example/oauth/OauthApplicationTests.java 35 | ``` 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth/config/ResourceServerConfigurerAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth.config; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 8 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 10 | import org.springframework.security.oauth2.provider.token.TokenStore; 11 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; 12 | 13 | import javax.sql.DataSource; 14 | 15 | @EnableAuthorizationServer // 자원서버 설정 16 | @Configuration 17 | public class ResourceServerConfigurerAdapterImpl extends ResourceServerConfigurerAdapter { 18 | 19 | @Override 20 | public void configure(HttpSecurity http) throws Exception { 21 | http.headers().frameOptions().disable(); 22 | http.authorizeRequests() 23 | .antMatchers("/users").access("#oauth2.hasScope('read')"); 24 | } 25 | 26 | @Bean 27 | public TokenStore JdbcTokenStore(@Qualifier("dataSource") DataSource dataSource) { 28 | return new JdbcTokenStore(dataSource); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | -- used in tests that use HSQL 2 | create table oauth_client_details ( 3 | client_id VARCHAR(256) PRIMARY KEY, 4 | resource_ids VARCHAR(256), 5 | client_secret VARCHAR(256), 6 | scope VARCHAR(256), 7 | authorized_grant_types VARCHAR(256), 8 | web_server_redirect_uri VARCHAR(256), 9 | authorities VARCHAR(256), 10 | access_token_validity INTEGER, 11 | refresh_token_validity INTEGER, 12 | additional_information VARCHAR(4096), 13 | autoapprove VARCHAR(256) 14 | ); 15 | 16 | create table oauth_client_token ( 17 | token_id VARCHAR(256), 18 | token LONGVARBINARY, 19 | authentication_id VARCHAR(256) PRIMARY KEY, 20 | user_name VARCHAR(256), 21 | client_id VARCHAR(256) 22 | ); 23 | 24 | create table oauth_access_token ( 25 | token_id VARCHAR(256), 26 | token LONGVARBINARY, 27 | authentication_id VARCHAR(256) PRIMARY KEY, 28 | user_name VARCHAR(256), 29 | client_id VARCHAR(256), 30 | authentication LONGVARBINARY, 31 | refresh_token VARCHAR(256) 32 | ); 33 | 34 | create table oauth_refresh_token ( 35 | token_id VARCHAR(256), 36 | token LONGVARBINARY, 37 | authentication LONGVARBINARY 38 | ); 39 | 40 | create table oauth_code ( 41 | code VARCHAR(256), authentication LONGVARBINARY 42 | ); 43 | 44 | create table oauth_approvals ( 45 | userId VARCHAR(256), 46 | clientId VARCHAR(256), 47 | scope VARCHAR(256), 48 | status VARCHAR(10), 49 | expiresAt TIMESTAMP, 50 | lastModifiedAt TIMESTAMP 51 | ); 52 | 53 | 54 | -- customized oauth_client_details table 55 | create table ClientDetails ( 56 | appId VARCHAR(256) PRIMARY KEY, 57 | resourceIds VARCHAR(256), 58 | appSecret VARCHAR(256), 59 | scope VARCHAR(256), 60 | grantTypes VARCHAR(256), 61 | redirectUrl VARCHAR(256), 62 | authorities VARCHAR(256), 63 | access_token_validity INTEGER, 64 | refresh_token_validity INTEGER, 65 | additionalInformation VARCHAR(4096), 66 | autoApproveScopes VARCHAR(256) 67 | ); -------------------------------------------------------------------------------- /src/main/java/com/example/oauth/config/AuthorizationServiceConfigurerAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 10 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 11 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 12 | import org.springframework.security.oauth2.provider.token.TokenStore; 13 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; 14 | 15 | import javax.sql.DataSource; 16 | 17 | @EnableResourceServer // API 서버 인증, 권한 설정 18 | @Configuration 19 | public class AuthorizationServiceConfigurerAdapterImpl extends AuthorizationServerConfigurerAdapter{ 20 | 21 | @Autowired 22 | private AuthenticationManager authenticationManager; 23 | 24 | @Autowired 25 | private TokenStore JdbcTokenStore; 26 | 27 | @Autowired 28 | private DataSource dataSource; 29 | 30 | @Override 31 | public void configure(ClientDetailsServiceConfigurer clients) 32 | throws Exception { 33 | clients.jdbc(dataSource); 34 | } 35 | 36 | @Override 37 | public void configure( 38 | AuthorizationServerEndpointsConfigurer endpoints) 39 | throws Exception { 40 | 41 | endpoints 42 | .tokenStore(JdbcTokenStore) 43 | .authenticationManager(authenticationManager); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | oauth 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | oauth 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.5.9.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-jpa 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-web 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-devtools 40 | runtime 41 | 42 | 43 | mysql 44 | mysql-connector-java 45 | runtime 46 | 47 | 48 | org.projectlombok 49 | lombok 50 | true 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-test 55 | test 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-security 60 | 61 | 62 | org.springframework.security 63 | spring-security-test 64 | 65 | 66 | org.springframework.security.oauth 67 | spring-security-oauth2 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-starter-data-rest 72 | 73 | 74 | com.h2database 75 | h2 76 | compile 77 | 78 | 79 | 80 | 81 | junit 82 | junit 83 | test 84 | 85 | 86 | org.hamcrest 87 | hamcrest-library 88 | test 89 | 90 | 91 | org.mockito 92 | mockito-core 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-maven-plugin 102 | 103 | 104 | 105 | org.eluder.coveralls 106 | coveralls-maven-plugin 107 | 108 | 2dLtyuHzczfoHhK7iqUPcgQZhi6mXhyUd 109 | 110 | 111 | 112 | 113 | org.codehaus.mojo 114 | cobertura-maven-plugin 115 | 116 | xml 117 | 256m 118 | 119 | true 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/test/java/com/example/oauth/OauthApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth; 2 | 3 | import static org.hamcrest.Matchers.is; 4 | 5 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 7 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; 8 | 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.security.web.FilterChainProxy; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | import org.springframework.test.web.servlet.ResultActions; 19 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 20 | import org.springframework.util.LinkedMultiValueMap; 21 | import org.springframework.util.MultiValueMap; 22 | import org.springframework.web.context.WebApplicationContext; 23 | 24 | import org.springframework.boot.json.JacksonJsonParser; 25 | import org.springframework.boot.test.context.SpringBootTest; 26 | 27 | @RunWith(SpringRunner.class) 28 | @SpringBootTest 29 | public class OauthApplicationTests { 30 | 31 | private MockMvc mockMvc; 32 | 33 | @Autowired 34 | private WebApplicationContext webApplicationContext; 35 | 36 | @Autowired 37 | private FilterChainProxy springSecurityFilterChain; 38 | private String CONTENT_TYPE = "application/json;charset=UTF-8";; 39 | private String SCOPE = "read"; 40 | private String CLIENT_ID = "foo"; 41 | private String CLIENT_SECRET = "bar"; 42 | private String SECURITY_USERNAME = "user"; 43 | private String SECURITY_PASSWORD = "test"; 44 | 45 | @Before 46 | public void setUp() throws Exception { 47 | mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).addFilter(springSecurityFilterChain).build(); 48 | } 49 | 50 | @Test 51 | public void when_callApi_expect_unauthorized() throws Exception { 52 | mockMvc.perform(get("/users")).andExpect(status().isUnauthorized()); 53 | } 54 | 55 | @Test 56 | public void when_callUsers_expect_success() throws Exception { 57 | String token =obtainAccessToken(SECURITY_USERNAME,SECURITY_PASSWORD); 58 | mockMvc.perform(get("/users") 59 | .header("Authorization", "Bearer " + token) 60 | .accept(CONTENT_TYPE)) 61 | .andExpect(status().isOk()) 62 | .andExpect(content().contentType(CONTENT_TYPE)); 63 | } 64 | 65 | @Test 66 | public void when_ckeck2User_expect_success() throws Exception{ 67 | 68 | String accessToken =obtainAccessToken(SECURITY_USERNAME, SECURITY_PASSWORD); 69 | 70 | mockMvc.perform(get("/users/2") 71 | .header("Authorization", "Bearer " + accessToken) 72 | .accept(CONTENT_TYPE)) 73 | .andExpect(jsonPath("$.username", is("test2"))); 74 | } 75 | 76 | @Test 77 | public void when_callGetPost_expect_success() throws Exception { 78 | 79 | JSONObject user = setUser(); 80 | String accessToken =obtainAccessToken(SECURITY_USERNAME, SECURITY_PASSWORD); 81 | 82 | createUser(user, accessToken); 83 | verifyUser(accessToken); 84 | } 85 | 86 | private void verifyUser(String accessToken) throws Exception { 87 | mockMvc.perform(get("/users/1") 88 | .header("Authorization", "Bearer " + accessToken) 89 | .accept(CONTENT_TYPE)) 90 | .andExpect(jsonPath("$.username", is("test"))); 91 | } 92 | 93 | private void createUser(JSONObject user, String accessToken) throws Exception { 94 | mockMvc.perform(post("/users") 95 | .header("Authorization", "Bearer " + accessToken) 96 | .contentType(CONTENT_TYPE) 97 | .content(user.toString()) 98 | .accept(CONTENT_TYPE)) 99 | .andExpect(status().isCreated()); 100 | } 101 | 102 | private JSONObject setUser() throws JSONException { 103 | JSONObject user = new JSONObject(); 104 | user.put("username","test"); 105 | user.put("id","1"); 106 | return user; 107 | } 108 | 109 | private String obtainAccessToken(String username, String password) throws Exception { 110 | 111 | MultiValueMap params = new LinkedMultiValueMap<>(); 112 | params.add("grant_type", "password"); 113 | params.add("client_id", CLIENT_ID); 114 | params.add("username", username); 115 | params.add("password", password); 116 | params.add("scope", SCOPE); 117 | 118 | ResultActions result 119 | = mockMvc.perform(post("/oauth/token") 120 | .params(params) 121 | .with(httpBasic(CLIENT_ID, CLIENT_SECRET)) 122 | .accept(CONTENT_TYPE)) 123 | .andExpect(status().isOk()) 124 | .andExpect(content().contentType(CONTENT_TYPE)); 125 | 126 | String resultString = result.andReturn().getResponse().getContentAsString(); 127 | 128 | JacksonJsonParser jsonParser = new JacksonJsonParser(); 129 | return jsonParser.parseMap(resultString).get("access_token").toString(); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /docs/step-1:oauth1,oauth2란.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # OAuth 4 | 5 | 6 | 7 | 8 | - [OAuth](#oauth) 9 | - [OAuth1 란](#oauth1-란) 10 | - [동작 방식](#동작-방식) 11 | - [OAuth의 개념](#oauth의-개념) 12 | - [OAuth의 WorkFlow](#oauth의-workflow) 13 | - [Request Token 발급 매개변수](#request-token-발급-매개변수) 14 | - [Request Token Signature 생성](#request-token-signature-생성) 15 | - [Access Token 매개변수](#access-token-매개변수) 16 | - [API호출을 위한 매개변수](#api호출을-위한-매개변수) 17 | - [OAuth2란](#oauth2란) 18 | - [OAuth의1.0 과 OAuth의2.0차이점](#oauth의10-과-oauth의20차이점) 19 | - [인증 절차 간소화](#인증-절차-간소화) 20 | - [용어 변경](#용어-변경) 21 | - [Resource Server와 Authorization Server서버의 분리](#resource-server와-authorization-server서버의-분리) 22 | - [다양한 인증 방식(Grant_type)](#다양한-인증-방식grant_type) 23 | - [인증 종류](#인증-종류) 24 | - [Authorization Code Grant](#authorization-code-grant) 25 | - [Implicit Grant](#implicit-grant) 26 | - [Resource Owner Password Credentials Grant](#resource-owner-password-credentials-grant) 27 | - [Client Credentials Grant](#client-credentials-grant) 28 | - [Device Code Grant](#device-code-grant) 29 | - [Refresh Token Grant](#refresh-token-grant) 30 | 31 | 32 | 33 | # OAuth1 란 34 | OAuth는 Open Authorization, Open Authentication 뜻하는 것으로 애플리케이션(페이스북,구글,트위터)(Service Provider)의 유저의 비밀번호를 Third party앱에 제공 없이 인증,인가를 할 수 있는 오픈 스탠다드 프로토콜이다. OAuth 인증을 통해 애플리케이션 API를 유저대신에 접근할 수 있는 권한을 얻을 수 있다. OAuth가 사용되기 전에는 외부 사이트와 인증기반의 데이터를 연동할 때 인증방식의 표준이 없었기 때문에 기존의 기본인증인 아이디와 비밀번호를 사용하였는데, 이는 보안상 취약한 구조였다. 유저의 비밀번호가 노출될 가망성이 크기 때문이다. 그렇기 때문에 이 문제를 보안하기 위해 OAuth의 인증은 API를 제공하는 서버에서 진행하고, 유저가 인증되었다는 Access Token을 발급하였다. 그 발급된 Access token으로 Third party(Consumer)애플리케이션에서는 Service Provider의 API를 안전하고 쉽게 사용할 수 있게 되었다. 35 | 36 | ## 동작 방식 37 | 먼저 OAuth의 동작 방식을 알기 위해서는 아래의 개념을 알고 있어야 한다. 38 | ### OAuth의 개념 39 | 먼저 OAuth의는 아래와 같은 개념을 가지고있다. 40 | 41 | | 용어 | 설명 | 42 | | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | 43 | | User | Service Provider에 계정을 가지고 있으면서, Consumer앱을 이용하려는 사용자 | 44 | | Service Provider | OAuth를 사용하는 Open API를 제공하는 서비스 (facebook,google등) | 45 | | Protected Resource | Service Provider로부터 제공되어지는 API 자원들 | 46 | | Consumer | OAuth 인증을 사용해 Service Provider의 기능을 사용하려는 애플리케이션이나 웹 서비스 | 47 | | Consumer Key | Consumer가 Service Provider에게 자신을 식별하는 데 사용하는키 | 48 | | Consumer Secret | Consumer Key의 소유권을 확립하기 위해 Consumer가 사용하는 Secret | 49 | | Request Token | Consumer가 Service Provider에게 접근 권한을 인증받기 위해 사용하는 값. 인증이 완료된 후에는 Access Token으로 교환한다. | 50 | | Access Token | 인증 후 Consumer가 Service Provider의 자원에 접근하기 위한 키를 포함한 값 | 51 | | Token Secret | 주어진 토큰의 소유권을 인증하기 위해 소비자가 사용하는 Secret | 52 | 53 | ### OAuth의 WorkFlow 54 | 다음은 OAuth의 1.0의 WorkFlow이다. 55 | ![](https://i.imgur.com/7T48KvR.png) 56 | 57 | 58 | 위 WorkFlow 흐름은 간단하게 이렇다. 59 | 1. 그림에는 없지만 가장 먼저 Consumer는 Service Provider로부터 Client key와 Secret을 발급 받아야한다. 이것은 Service Provider에 API를 사용할것을 등록하는것과 동시에 Service Provider가 Consmer를 식별할 수 있게 해준다. 60 | 2. 그림에 A 처럼 Request Token을 요청할 때 Consumer 정보, Signature 정보를 포함하여 Request token을 요청을하고 B의 흐름처럼 Request token을 발급받는다. 61 | 3. Request Token값을 받은후 Consumer는 C처럼 User를 Service Provider에 인증 사이트로 다이렉트시키고, 유저는 그곳에서 Service Provider에 유저임을 인증하게 된다. 62 | 4. 그러면 Consumer는 D의 정보처럼 해당 유저가 인증이되면 OAuth_token와 OAuth_verifier를 넘겨준다. 63 | 5. 그이후에 Consumer는 OAuth_token와 OAuth_verifier받았다면 E의 흐름처럼 다시 서명을 만들어 Access Token을 요청하게 된다. 64 | 6. 그리고 Service Provider는 받은 토큰과 서명들이 인증이 되었으면 Access Token을 F의 정보 처럼 넘기게된다. 65 | 7. 그리고 그 Access Token 및 서명정보를 통해 Service Provider에 Protected Resource에 접근할 수 있게 된다. 66 | 67 | 아래는 Consumer의 요청시 매개변수의 종류에 대한 설명이다. 68 | 69 | #### Request Token 발급 매개변수 70 | Request Token 발급 요청 시 사용하는 매개변수는 다음 표와 같다.
71 | 72 | | 매개변수 | 설명 | 73 | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 74 | | OAuth_callback | Service Provider가 인증을 완료한 후 리다이렉트할 Consumer의 웹 주소. 만약 Consumer가 웹 애플리케이션이 아니라 리다이렉트할 주소가 없다면 소문자로 'oob'(Out Of Band라는 뜻)를 값으로 사용한다. | 75 | | OAuth_consumer_key |Consumer를 구별하는 키 값. Service Provider는 이 키 값으로 Consumer를 구분한다. | | 76 | | OAuth_nonce |Consumer에서 임시로 생성한 임의의 문자열. OAuth_timestamp의 값이 같은 요청에서는 유일한 값이어야 한다. 이는 악의적인 목적으로 계속 요청을 보내는 것을 막기 위해서이다. | | 77 | | OAuth_signature |OAuth 인증 정보를 암호화하고 인코딩하여 서명 값. OAuth 인증 정보는 매개변수 중에서 OAuth_signature를 제외한 나머지 매개변수와 HTTP 요청 방식을 문자열로 조합한 값이다. 암화 방식은 OAuth_signature_method에 정의된다. | | 78 | | OAuth_signature_method |OAuth_signature를 암호화하는 방법. HMAC-SHA1, HMAC-MD5 등을 사용할 수 있다. | | 79 | | OAuth_timestamp |요청을 생성한 시점의 타임스탬프. 1970년1월 1일 00시 00분 00초 이후의 시간을 초로 환산한 초 단위의 누적 시간이다. | | 80 | | OAuth_version | OAuth 사용 버전. 1.0a는 1.0이라고 명시하면 된다. | 81 | #### Request Token Signature 생성 82 | 83 | OAuth 1.0에서 Service Provider에게 요청을 할려면 매번 전자 서명을 만들어서 보내야한다. 아래는 전자서명을 만드는 순서이다. 84 | 85 | 1. 요청 매개변수를 모두 모은다. 86 | OAuth_signature를 제외하고 'OAuth_'로 시작하는 OAuth 관련 매개변수를 모은다. 모든 매개변수를 사전순으로 정렬하고 각각의 키(key)와 값(value)에 URL 인코딩(rfc3986)을 적용한다. URL 인코딩을 실시한 결과를 = 형태로 나열하고 각 쌍 사이에는 &을 넣는다. 이렇게 나온 결과 전체에 또 URL 인코딩을 적용한다. 87 | 88 | 2. 매개변수를 정규화(Normalize)한다. 89 | 모든 매개변수를 사전순으로 정렬하고 각각의 키(key)와 값(value)에 URL 인코딩(rfc3986)을 적용한다. URL 인코딩을 실시한 결과를 = 형태로 나열하고 각 쌍 사이에는 &을 넣는다. 이렇게 나온 결과 전체에 또 URL 인코딩을 적용한다. 90 | 91 | 3. Signature Base String을 만든다. 92 | HTTP method 명(GET 또는 POST), Consumer가 호출한 HTTP URL 주소(매개변수 제외), 정규화한 매개변수를 '&'를 사용해 결합한다. 즉 '[GET|POST] + & + [URL 문자열로 매개변수는 제외] + & + [정규화한 매개변수]' 형태가 된다. 93 | 94 | 4. 키 생성 95 | 3번 과정까지 거쳐 생성한 문자열을 암호화한다. 암호화할 때 Consumer Secret Key를 사용한다. Consumer Secret Key는 Consumer가 Service Provider에 사용 등록을 할 때 발급받은 값이다. HMAC-SHA1 등의 암호화 방법을 이용하여 최종적인 OAuth_signature를 생성한다. 96 | 97 | 98 | #### Access Token 매개변수 99 | 다음은 Access Token 발급을 요청할 때 사용하는 매개변수 표이다. 100 | 101 | | 매개변수 | 설명 | 102 | | ---------------------- | ---- | 103 | | OAuth_consumer_key | Consumer를 구별하는 키 값. Service Provider는 이 키 값으로 Consumer를 구분한다. | 104 | | OAuth_nonce | Consumer에서 임시로 생성한 임의의 문자열. OAuth_timestamp의 값이 같은 요청에서는 유일한 값이어야 한다. 이는 악의적인 목적으로 계속 요청을 보내는 것을 막기 위해서이다. | 105 | | OAuth_signature |OAuth 인증 정보를 암호화하고 인코딩하여 서명 값. OAuth 인증 정보는 매개변수 중에서 OAuth_signature를 제외한 나머지 매개변수와 HTTP 요청 방식을 문자열로 조합한 값이다. 암화 방식은 OAuth_signature_method에 정의된다. | 106 | | OAuth_signature_method | OAuth_signature를 암호화하는 방법. HMAC-SHA1, HMAC-MD5 등을 사용할 수 있다. | 107 | | OAuth_timestamp | 요청을 생성한 시점의 타임스탬프. 1970년1월 1일 00시 00분 00초 이후의 시간을 초로 환산한 초 단위의 누적 시간이다. | 108 | | OAuth_version | OAuth 사용 버전 | 109 | | OAuth_verifier | Request Token 요청 시 OAuth_callback으로 전달받은 OAuth_verifier 값 | 110 | | OAuth_token | Request Token 요청 시 OAuth_callback으로 전달받은 OAuth_token 값 | 111 | 112 | 113 | #### API호출을 위한 매개변수 114 | 다음은 Access Token을 사용해 API를 호출할 때 사용하는 매개변수는 다음 표이다. 115 | 116 | | 매개변수 | 설명 | 117 | | ---------------------- | ---- | 118 | | OAuth_consumer_key | Consumer를 구별하는 키 값. Service Provider는 이 키 값으로 Consumer를 구분한다. | 119 | | OAuth_nonce | Consumer에서 임시로 생성한 임의의 문자열. OAuth_timestamp의 값이 같은 요청에서는 유일한 값이어야 한다. 이는 악의적인 목적으로 계속 요청을 보내는 것을 막기 위해서이다. | 120 | | OAuth_signature | OAuth 인증 정보를 암호화하고 인코딩하여 서명 값. OAuth 인증 정보는 매개변수 중에서 OAuth_signature를 제외한 나머지 매개변수와 HTTP 요청 방식을 문자열로 조합한 값이다. 암화 방식은 OAuth_signature_method에 정의된다. | 121 | | OAuth_signature_method | OAuth_signature를 암호화하는 방법. HMAC-SHA1, HMAC-MD5 등을 사용할 수 있다. | 122 | | OAuth_timestamp |요청을 생성한 시점의 타임스탬프. 1970년1월 1일 00시 00분 00초 이후의 시간을 초로 환산한 초 단위의 누적 시간이다. | 123 | | OAuth_version | OAuth 버전 | 124 | | OAuth_token | OAuth_callback으로 전달받은 OAuth_token | 125 | 126 | 127 | # OAuth2란 128 | OAuth의2는 OAuth의1의 유저의 인증플로우, 전반적인 목적만 공유하고 OAuth의1.0을 새로 작성한것이다. OAuth의1.0과 OAuth의2.0의 차이는 앱 애플리케이션, 웹 애플리케이션, 데스크탑 애플리케이션등의 인증방식을 강화하고 Consumer에 개발 간소화를 중심으로 개발 되었다. 129 | 130 | ## OAuth의1.0 과 OAuth의2.0차이점 131 | 아래는 OAuth 1.0 에서 OAuth2.0 차이점은 일단 인증 절차 간소화 됨으로써 개발자들이 구현하기 더쉬워졌고, 기존에 사용하던 용어도 바뀌면서 Authorizaiton server와 Resource서버의 분리가 명시적으로 되었다. 또한 다양한 인증 방식을 지원하게 됐다. 아래는 1.0과 2.0의 차이점을 나열한것이다. 132 | ### 인증 절차 간소화 133 | * 기능의 단순화, 기능과 규모의 확장성 등을 지원하기 위해 만들어 졌다. 134 | * 기존의 OAuth1.0은 디지털 서명 기반이었지만 OAuth2.0의 암호화는 https에 맡김으로써 복잡한 디지털 서명에관한 로직을 요구하지 않기때문에 구현 자체가 개발자입장에서 쉬워짐. 135 | 136 | ### 용어 변경 137 | * Resource Owner : 사용자 (1.0 User해당) 138 | * Resource Server : REST API 서버 (1.0 Protected Resource) 139 | * Authorization Server : 인증서버 (API 서버와 같을 수도 있음)(1.0 Service Provider) 140 | * Client : 써드파티 어플리케이션 (1.0 Service Provider 해당) 141 | 142 | ### Resource Server와 Authorization Server서버의 분리 143 | * 커다란 서비스는 인증 서버를 분리하거나 다중화 할 수 있어야 함. 144 | * Authorization Server의 역할을 명확히 함. 145 | 146 | ### 다양한 인증 방식(Grant_type) 147 | * Authorization Code Grant 148 | * Implicit Grant 149 | * Resource Owner Password Credentials Grant 150 | * Client Credentials Grant 151 | * Device Code Grant 152 | * Refresh Token Grant 153 | 154 | ## 인증 종류 155 | OAuth 2.0의 인증종류는 6가지 입니다. 아래는 각각의 인증방식의 flow와 간단한 설명입니다. 156 | 157 | ### Authorization Code Grant 158 | 일반적인 웹사이트에서 소셜로그인과 같은 인증을 받을 때 가장 많이 쓰는 방식으로 기본적으로 지원하고 있는 방식이다. 아래는 Authorization Code Grant type 으로 Access Token을 얻어오는 시퀀스 다이어그램이다. 159 | ![](https://i.imgur.com/xaKCz9E.png) 160 | 1. 먼저 클라이언트가 Redirect URL을 포함하여 Authorization server 인증 요청을 한다. 161 | 2. AuthorizationServer는 유저에게 로그인창을 제공하여 유저를 인증하게 된다. 162 | 3. AuthorizationServer는 Authorization code를 클라이언트에게 제공해준다. 163 | 4. Client는 코드를 Authorization server에 Access Token을 요청한다. 164 | 5. Authorization 서버는 클라이언트에게 Access token을 발급해준다. 165 | 6. 그 Access token을 이용하여 Resource server에 자원을 접근할 수 있게 된다. 166 | 7. 그이후에 토큰이 만료된다면 refresh token을 이용하여 토큰을 재발급 받을 수 있다. 167 | 168 | ### Implicit Grant 169 | Public Client인 브라우저 기반의 애플리케이션(Javascript application)이나 모바일 애플리케이션에서 바로 Resource Server에 접근하여 사용할 수 있는 방식이다. 170 | ![](https://i.imgur.com/DayLoth.png) 171 | 1. 클라이언트는 Authorization server에 인증을 요청한다. 172 | 2. 유저는 Authorization server를 통해 인증한다. 173 | 3. Authorization server는 Access token을 포함하여 클라이언트의 Redirect url을 호출한다. 174 | 4. 클라이언트는 해당 Access token이 유효한지 Authorization server에 인증요청한다. 175 | 5. 인증서버는 그 토큰이 유효하다면 토큰의 만기시간과함께 리턴해준다. 176 | 6. 클라이언트는 Resource server에 접근할 수 있게된다. 177 | 178 | 179 | ### Resource Owner Password Credentials Grant 180 | Client에 아이디/패스워드를 받아 아이디/패스워드로 직접 access token을 받아오는 방식이다. Client가 신용이 없을 때에는 사용하기에 위험하다는 단점이 있다. 클라이언트가 확실한 신용이 보장될 때 사용할 수 있는 방식이다. 181 | ![](https://i.imgur.com/FVv86QT.png) 182 | 1. User가 Id와 Password를 입력한다 183 | 2. 클라이언트는 유저의 id와 password와 클라이언트 정보를 넘긴다. 184 | 3. Authorization sever는 Access token을 넘긴다. 185 | 186 | 187 | ### Client Credentials Grant 188 | 애플리케이션이 Confidential Client일 때 id와 secret을 가지고 인증하는 방식이다. 189 | ![](https://i.imgur.com/TMXAjvo.png) 190 | 1. 클라이언트 정보를 Authorization server에 넘긴다. 191 | 2. Access Token을 Client에 전달한다. 192 | 193 | ### Device Code Grant 194 | 장치 코드 부여 유형은 브라우저가 없거나 입력이 제한된 장치에서 사용됩니다. 195 | 196 | ### Refresh Token Grant 197 | 기존에 저장해둔 리프러시 토큰이 존재할 때 엑세스토큰 재발급 받을 필요가 있을 때 사용한다. 그리고 기존 액세스는 토큰이 만료된다. 198 | 199 | 200 | 201 | 참고 사이트 202 | 203 | * [Naver D2](http://d2.naver.com/helloworld/24942) 204 | * [OAuth.net](https://OAuth.net/core/1.0/) 205 | * [Showerbugs](https://showerbugs.github.io/2017-11-16/OAuth-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C) 206 | * [OAuth 2.0 Servers](https://www.OAuth.com/OAuth2-servers/differences-between-OAuth-1-2/) 207 | * [이수홍](https://brunch.co.kr/@sbcoba/4) 208 | * [Authorization Code Flow](https://developer.accela.com/docs/construct-authCodeFlow.html) 209 | * [OX Wiki](https://ox.gluu.org/doku.php?id=oxauth:implicitgrant) 210 | -------------------------------------------------------------------------------- /docs/step-2:spring-oauth2-구현.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Spring Security OAuth2구현 4 | 5 | 6 | 7 | - [Spring Security OAuth2구현](#spring-security-oauth2구현) 8 | - [OAuth란?](#oauth란) 9 | - [OAuth 실 사용 예](#oauth-실-사용-예) 10 | - [OAuth2 역할](#oauth2-역할) 11 | - [Spring으로 OAuth2구현](#spring으로-oauth2구현) 12 | - [Resource Owner Password Credentials Grant 동작방식](#resource-owner-password-credentials-grant-동작방식) 13 | - [OAuth2 in-memory 방식으로 간단한 구현](#oauth2-in-memory-방식으로-간단한-구현) 14 | - [OAuth2 JdbcTokenStore등을 이용한 데이터 영속화](#oauth2-jdbctokenstore등을-이용한-데이터-영속화) 15 | - [Authorization Server, Resource Server 애플리케이션 분리](#authorization-server-resource-server-애플리케이션-분리) 16 | - [마무리 하며...](#마무리-하며) 17 | 18 | 19 | 20 | ## OAuth란? 21 | OAuth는 Open Authorization, Open Authentication 뜻하는 것으로 자신의 애플리케이션 서버의 데이터로 다른 Third party에게 자원을 공유하거나 대신 유저 인증을 처리해줄 수 있는 오픈 표준 프로토콜이다. 22 | 23 | ## OAuth 실 사용 예 24 | 간단히 OAuth인증에 대해 실 사용 예를 보자면 웹, 앱들을 사용하면서 네이버 로그인, 카카오 로그인, 페이스북 로그인 등을 한 번쯤은 보았을 것이다. 이것이 바로 OAuth인 증 방식 중 하나이다. 네이버 로그인, 카카오 로그인, 페이스북 로그인 등으로 로그인을 하게 되면 그 애플리케이션들로부터 Access_token이라는 값을 받아 써드파티 애플리케이션에게 준다. Access_token이라는 값이 어떻게 보면 유저의 인증 키라고 보면 된다. 물론 유저가 로그인을 실패한다면 Access_token을 받지 못할 것이다. Access_token 을 받았다는 것은 해당 유저가 인증이 되었다는 것을 의미한다. 이러한 기술을 통해서 써드파티 애플리케이션에게 유저를 인증 시킨다. 이것이 OAuth인증이다. 물론 OAuth가 Authentication(인증) 기능만 하는 것은 아니다. 25 | 26 | OAuth는 Authorization(인가)에 대한 기능도 있다. Authorization라는 기능(?)을 이용하면 써드파티 앱들은 유저가 선택한 로그인 방식에 애플리케이션의 유저 정보 등을 얻을 수 있다. 이정보를 바탕으로 써드파티 앱들은 유저를 자신의 앱에 가입시킨다. 뿐만 아니라 서드파티 앱들은 유저가 로그인한 애플리케이션의 기능 등을 사용할 수 있다. 이것을 잘 사용하는 로켓펀치라는 구직 사이트가 있다. 이 사이트에 페이스북으로 로그인하기 기능 이용하여 로그인과 함께 친구 정보 제공을 동의하고 my account를 들어가면 내 친구들의 로켓펀치의 등록된 구직 정보들을 볼 수 있다. 이것이 가능한 이유는 아까 받은 Access_token으로 페이스북에게 유저의 정보를 요청할 수 있기 때문이다. 이러한 조회 기능뿐만 아니라 유저의 동의만 얻는다면 페이스북 담벼락에 글 남기기 등의 기능들도 사용할 수 있다. 이런 것들 가능하게 해주는 것이 OAuth 인증이다. OAuth도 버전이 있고, 그 차이점이 있는데 미흡하지만 [OAuth란?](https://minwan1.github.io/2018/02/24/2018-02-24-OAuth/)을 참조하면 될 것 같다. 27 | 28 | 위와 같은 방법뿐만 아니라 Microservices에 인증 방식으로도 사용된다. 마이크로서비스란 하나의 큰 애플리케이션을 여러개의 작은 애플리케이션으로 쪼개어 변경과 조합이 가능하도록 만든 아키텍처를 말한다. 이 [마이크로서비스 아키텍처, 그것이 뭣이 중헌디?](http://guruble.com/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4microservice-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B7%B8%EA%B2%83%EC%9D%B4-%EB%AD%A3%EC%9D%B4-%EC%A4%91%ED%97%8C%EB%94%94/)글에 마이크로서비스에 대한 내용들이 잘 설명돼 있다. 아무래도 자원들이 각각의 서버에 분리해있다보니 인증이나, 인가를 하나로 적절히 관리하는것이 필요하다. 만약 모놀리틱 아키텍처로 이러한 자원들을 관리하게된다면 상당히 비효율적일 것이다. 각각의 자원 서버마다 필요한 자원의 서비스를 호출하려고하면 인증,인가처리를 해야하기 떄문에 매우 비효율적으로 될것이다. 하지만 OAuth방식을 이용한다면 인증서버를 두고 각각의 자원서비스들의 인증, 인가를 관리한다면 효율적으로 자원 서비스들을 관리할 수 있다. 29 | 30 | 31 | ## OAuth2 역할 32 | 33 | 구현전에 OAuth2에 역할에대해 알아보자. OAuth2는 크게 Resource Owner, Authorization Server, Resource Server, Client 4개의 역할로 나누어진다. 먼저 Resource Owner는 유저를 말한다. 이유저는 위에서말한 카카오톡,네이버,구글등에 로그인 할 수 있고 그 해당 애플리케이션의 기능을 이용 가능한 유저를 말한다. Authorization Server는 인증 서버를 말한다. 유저가 OAuth를 통해 로그인한다면 카카오톡,네이버,구글의 유저가 맞는지 확인해주는 서버를 말한다. Resource Server는 카카오톡,네이버,구글의 자원(API)이 있는 서버를 말한다. Client는 써드파티앱들을 말한다. Authorization Server, Resource Server를 이용하는 앱들은 모두 Clinet라고 생각하면 된다. 34 | 35 | 36 | ## Spring으로 OAuth2구현 37 | 뭔가 서론이 장황해진 것 같다. 결론을 말씀드리자면 이러한 장점들을 이용하기 위해 OAuth인증 시스템을 구현해 볼 것이다. 구현 방법은 Spring을 이용해서 Spring security의 하위 프로젝트 Spring OAuth를 구현할 것이다. 여기에서 구현하고자하는 방식은 OAuth2방식이고 인증 방식은 Password 방식을 구현할 것이다. OAuth2부터는 다양한 인증 방식을 제공하는데 여기에대한 차이점은 [OAuth란?](https://minwan1.github.io/2018/02/24/2018-02-24-OAuth/)글에서 확인할 수 있다. 38 | 39 | 40 | ### Resource Owner Password Credentials Grant 동작방식 41 | 먼저 구현하려고하는 password 인증 방식이 무엇인지 간단하게 살펴보자. 이방식을 사용하기 위해서는 AuthorizationServer에 Client-id와 secret을 등록해야한다. 그리고 Client가 Resource Owner로부터 아이디/패스워드를 받아 직접 access token을 받아오는 방식이다. 이때 클라이언트는 AccessToken을 얻기 위해 AuthorizationServer에 호출할때 이 Client-id와 secret을 Basic-auth를 해줘야 한다. 하지만 이방식은 Client가 신용이 없을 때에는 사용하기에 위험하다는 단점이 있다. 클라이언트가 확실한 신용이 보장될 때 사용할 수 있는 방식이다. 42 | ![](https://i.imgur.com/7LhjI1L.png) 43 | 동작 순서는 대략 이렇다. 44 | 1. AuthorizationServer에 Client-id와 Secret을 등록한다(그림에는 안나와있음) 45 | 2. User가 Id와 Password를 입력한다 46 | 3. 클라이언트는 유저의 id와 password와 클라이언트 정보를 넘긴다. 47 | 4. Authorization sever는 Access token을 넘긴다. 48 | 5. Client는 token로 자원서버에 접근할 수 있게 된다. 49 | 50 | 그럼 이제 실제 구현을 해볼것이다. 51 | 52 | ### OAuth2 in-memory 방식으로 간단한 구현 53 | OAuth는 인증서버와 자원 서버를 하나의 애플리케이션에서 구현할 수도 있고, 분리할 수도 있다. 여기에서는 두 개를 하나의 애플리케이션에서 구현할 것이다. Resource Owner, Client 관리는 in-memory를 이용할 것이다. 아래와 같이 application.yml에 Resource Owner를 지정하고, Client를 쉽게 등록할 수 있다. 54 | ``` 55 | security: 56 | user: 57 | name: user 58 | password: test 59 | oauth2: 60 | client: 61 | client-id: foo 62 | client-secret: bar 63 | ``` 64 | 그다음은 Resource Server, Authorization Server의 설정을 구현할 것이다. @EnableResourceServer, @EnableAuthorizationServer 두개의 어노테이션으로 쉽게 인증서버와 자원서버를 구성할 수 있다. 65 | ```java 66 | @EnableResourceServer // API 서버 인증(또는 권한 설정) 67 | @EnableAuthorizationServer // 자원서버 설정 68 | @Configuration 69 | public class ResourceServerConfigurerAdapterImpl extends ResourceServerConfigurerAdapter { 70 | 71 | @Override 72 | public void configure(HttpSecurity http) throws Exception { 73 | http.authorizeRequests() 74 | .antMatchers("/users").access("#oauth2.hasScope('read')"); 75 | } 76 | 77 | } 78 | ``` 79 | 위와 같이 지정을 하게 되면 /users라는 자원에 접근하기 위해서는 access_token이 있어야 접근을 할 수 있다. 자원에 대한 CRUD는 Rest Repositories(도메인만으로 REST API를 자동으로 만들어준다.)를 이용할 것이다. 도메인 구성등에 대한 정보는 [소스](https://github.com/minwan1/spring-security-oauth2-example/blob/example-1/src/main/java/com/example/oauth/user/User.java)을 참조 할 수 있다. 사실 너무 간단해서 도메인이라고 할것도 없다. 이제 실제로 /users를 접근을 할 수 없는지 테스트를 해보자. 테스트는 아래와 같이 junit을 사용할 것이다. 80 | ```java 81 | @Test 82 | public void when_callApi_expect_unauthorized() throws Exception { 83 | mockMvc.perform(get("/users")).andExpect(status().isOk()); 84 | } 85 | ``` 86 | 테스트 결과는....... 87 | ![](https://i.imgur.com/90IClaN.png) 88 | 그렇다. response 결과는 401이 넘어와 실패했다. access_token없이 접근해서 그렇다. 당연한 결과이다. 위 소스는 isOk에서 isUnauthorized로 수정해주면될 것 같다. 그러면 테스트 결과는 성공일 것이다. 그다음은 토큰을 생성한후 API를 호출해 성공하는 테스트를 작성할 것 이다. 먼저 rest로 토큰을 얻어보자. 이 /oauth/token는 Spring OAuth에서 지정해준 URI이다. 이 URI는 Basic Auth를 사용하여 ClientID와 Secret을 포함하고 바디값으로 grant_type, client_id, username, password, scope를 넘기면 토큰 값을 얻을 수 있다. 89 | ```java 90 | private String obtainAccessToken(String username, String password) throws Exception { 91 | 92 | MultiValueMap params = new LinkedMultiValueMap<>(); 93 | params.add("grant_type", "password"); 94 | params.add("client_id", CLIENT_ID); //foo 95 | params.add("username", username); 96 | params.add("password", password); 97 | params.add("scope", SCOPE); //read 98 | 99 | ResultActions result 100 | = mockMvc.perform(post("/oauth/token") 101 | .params(params) 102 | .with(httpBasic(CLIENT_ID, CLIENT_SECRET)) //foo, bar 103 | .accept(CONTENT_TYPE)) //"application/json;charset=UTF-8" 104 | .andExpect(status().isOk()) 105 | .andExpect(content().contentType(CONTENT_TYPE));//"application/json;charset=UTF-8" 106 | 107 | String resultString = result.andReturn().getResponse().getContentAsString(); 108 | 109 | JacksonJsonParser jsonParser = new JacksonJsonParser(); 110 | return jsonParser.parseMap(resultString).get("access_token").toString(); 111 | } 112 | ``` 113 | 위와같이 넘기면 access_token값을 얻을 수 있다. 114 | 115 | ```java 116 | @Test 117 | public void when_callUsers_expect_success() throws Exception { 118 | String accessToken =obtainAccessToken(SECURITY_USERNAME,SECURITY_PASSWORD); 119 | mockMvc.perform(get("/users") 120 | .header("Authorization", "Bearer " + accessToken) 121 | .accept(CONTENT_TYPE))//"application/json;charset=UTF-8" 122 | .andExpect(status().isOk()) 123 | .andExpect(content().contentType(CONTENT_TYPE));//"application/json;charset=UTF-8" 124 | } 125 | ``` 126 | 그다음 생성한 토큰을 이용해서 header 값에 access_token을 넣은 후 호출하면 호출에 성공한 모습을 확인할 수 있다. 이것의 전체적인 구현인 토큰 관리, client, user 등록등 In-memory기반에 의하여 구현되었다. 그렇기 때문에 서버가 종료되면 기존에 발급되었던 토큰들이 모두 소멸된다. Product로 사용하기 위해서는 JdbcTokenStore빈을 통해서 토큰 관리, client, user 등록등을 영속화하여 사용해야 한다. in-memory 방식으로 구현한 OAuth소스는 [in-memory 방식을 사용한 OAuth2 구현](https://github.com/minwan1/spring-security-oauth2-example/tree/example-1)에서 확인할 수 있다. 127 | 128 | 129 | ### OAuth2 JdbcTokenStore등을 이용한 데이터 영속화 130 | 이제 Authorization Server, Resource Server을 커스터마이징하여 데이터들의 관리를 영속화할 것이다. 가장먼저 해야할것은 Spring Security Oauth에 맞는 디비에 대한 [스키마 ](https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql)를 만드는것이다. 그 후 TokenStore의 빈을 등록하는것이다. TokenStore 빈을 등록을하게 되면 Token관리, Client관리등을 디비로 할 수 있게된다. 131 | 132 | ```java 133 | @Bean 134 | public TokenStore JdbcTokenStore(@Qualifier("dataSource") DataSource dataSource) { 135 | return new JdbcTokenStore(dataSource); 136 | } 137 | ``` 138 | 139 | 140 | 그 다음 Authorization Server, Resource Server를 두개의 클래스로 분리할 것이다. ResourceServerConfigurerAdapter, AuthorizationServerConfigurerAdapter를 상속해서 세부내용을 구현해야 한다. 그래야 좀 더 세부적으로 Authorization Server, Resource Server 컨트롤 가능하다. 아래는 각각의 구현체 구조이다. 141 | 142 | 143 | ```java 144 | @Configuration 145 | @EnableResourceServer 146 | public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { 147 | 148 | @Override 149 | public void configure(ResourceServerSecurityConfigurer resources) { 150 | 151 | } 152 | @Override 153 | public void configure(HttpSecurity http) throws Exception { 154 | // 자원서버 접근권한 설정 155 | } 156 | 157 | } 158 | ``` 159 | ```java 160 | 161 | @Configuration 162 | @EnableAuthorizationServer 163 | public class DemoApplication extends AuthorizationServerConfigurerAdapter { 164 | // ... 165 | @Override 166 | public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { 167 | // OAuth2 인증서버 자체의 보안 정보를 설정하는 부분 168 | } 169 | @Override 170 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 171 | // Client 에 대한 정보를 설정하는 부분 172 | } 173 | @Override 174 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 175 | // OAuth2 서버가 작동하기 위한 Endpoint에 대한 정보를 설정 176 | } 177 | // ... 178 | } 179 | ``` 180 | 181 | Authorization Server, Resource Server를 분리하게 되면 yml에 등록한 클라이언트는 의미가없어진다. 이유는 AuthorizationServerConfigurerAdapter 클래스를 상속함으로써 오버라이딩이되어 authorizationServer에 대한 정의를 여기에 할것이기 때문이다. 그리고 만약 세부구현체를 구현을안하고 accessToken을 생성하려고하면 clientDetailsService를 정의하라는 에러를 만날것이다. 먼저 테스트를 위해 인메모리 등록을 할 것이다. 만약 inMemory가 아닌 외부 db를 사용하고싶으면 아래의 소스 부분에 inMemory부분을 jdbc 방식으로 바꾸면 된다. 182 | 183 | ```java 184 | @EnableResourceServer // API 서버 인증, 권한 설정 185 | @Configuration 186 | public class AuthorizationServiceConfigurerAdapterImpl extends AuthorizationServerConfigurerAdapter{ 187 | 188 | @Override 189 | public void configure(ClientDetailsServiceConfigurer clients) 190 | throws Exception { 191 | clients.inMemory() // 이부분을 jdbc를 쓰면 데이터베이스값을 이용할 수 있다. 192 | .withClient("foo") 193 | .secret("bar") 194 | .authorizedGrantTypes( 195 | "password","authorization_code", "refresh_token") 196 | .scopes("read"); 197 | } 198 | 199 | } 200 | ``` 201 | 그런데 이렇게 클라이언트 정보를 정의하고 accessToken을 생성하려고 하면 "Unsupported grant type: password"라는 메시지를 만난다. 이 메시지를 해결하기 위해서는 AuthorizationServer에 AuthenticationManager를 제공해야한다. 그래서 아래와같이 소스를 추가해줘야한다. 202 | 203 | ```java 204 | public class AuthorizationServiceConfigurerAdapterImpl extends AuthorizationServerConfigurerAdapter{ 205 | 206 | @Autowired 207 | private AuthenticationManager authenticationManager; 208 | 209 | @Autowired 210 | private TokenStore JdbcTokenStore; 211 | 212 | @Override 213 | public void configure(ClientDetailsServiceConfigurer clients) 214 | throws Exception { 215 | clients.inMemory() 216 | .withClient("foo") 217 | .secret("bar") 218 | .authorizedGrantTypes( 219 | "password","authorization_code", "refresh_token") 220 | .scopes("read"); 221 | } 222 | 223 | @Override 224 | public void configure( 225 | AuthorizationServerEndpointsConfigurer endpoints) 226 | throws Exception { 227 | 228 | endpoints 229 | .tokenStore(JdbcTokenStore) 230 | .authenticationManager(authenticationManager); 231 | } 232 | 233 | } 234 | ``` 235 | 236 | 위와 같이 추가되면 이제 토큰은 inMemory가 아닌 DB에 저장이 된다. 그리고 "Unsupported grant type: password"에대한 에러도 사라지고 AccessToken이 생성 될것이다. 만약 클라이언트 관리도 디비에서 하려면 아래와 같이 변경해주면 된다. 237 | ```java 238 | @Override 239 | public void configure(ClientDetailsServiceConfigurer clients) 240 | throws Exception { 241 | clients.jdbc(dataSource); 242 | } 243 | ``` 244 | 그다음 oauth_client_details 테이블의 아래와같이 클라이언트 정보를 추가해줘야 한다. 245 | ```sql 246 | INSERT INTO PUBLIC.OAUTH_CLIENT_DETAILS (CLIENT_ID, RESOURCE_IDS, CLIENT_SECRET, SCOPE, AUTHORIZED_GRANT_TYPES, WEB_SERVER_REDIRECT_URI, AUTHORITIES, ACCESS_TOKEN_VALIDITY, REFRESH_TOKEN_VALIDITY, ADDITIONAL_INFORMATION, AUTOAPPROVE) VALUES ('foo', '', 'bar', 'read', 'password,authorization_code,refresh_token', '', '', null, null, '{}', ''); 247 | ``` 248 | 그리고 위와 같이 Client에 관한 데이터를 Insert해주면 된다. 그렇게 되면 DB에서 클라이언 정보를 관리할 수 있게 된다. 249 | 250 | ### Authorization Server, Resource Server 애플리케이션 분리 251 | 252 | 지금은 하나의 어플리케이션에 Authorization Server, Resource Server를 구현했다. 만약 다른 어플리케이션에 다른 데이터베이스를 바라보고 Authorization Server, Resource Server를 구성했다면 HTTP 통신을 통해 토큰을 확인해야한다. 그럴려면 먼저 인증서버에서 accessToken이 유효한지 확인할 수 있는 URL이 있어야 한다. 하지만 이것은 스프링에서 아래와 같이 설정해준다면 기본적으로 기능을 제공해준다. 253 | 254 | ```java 255 | @Override 256 | public void configure( 257 | AuthorizationServerSecurityConfigurer oauthServer) 258 | throws Exception { 259 | oauthServer 260 | .tokenKeyAccess("permitAll()") 261 | .checkTokenAccess("isAuthenticated()"); 262 | // Token 정보를 API(/oauth/check_token)를 활성화 시킨다. ( 기본은 denyAll ) 263 | } 264 | 265 | ``` 266 | 그리고 자원 서버에서도 다른 클라이언트가 접근한다면 token이 유효한지 인증서버를 통해 체크를 해줘야 한다. 이 설정 또한 쉽게 아래와 같이 URL만 등록해주면 된다. 아래는 YML, 빈을 통해 등록하는 방식을 나열했다. 267 | **YML등록방법** 268 | // OAuth2 서버에서 기본적으로 Token정보를 받아오는 URL 269 | ``` 270 | security.resource.token-info-uri:url주소/oauth/check_token 271 | ``` 272 | **빈등록방법** 273 | ```java 274 | @Primary 275 | @Bean 276 | public RemoteTokenServices tokenService() { 277 | RemoteTokenServices tokenService = new RemoteTokenServices(); 278 | tokenService.setCheckTokenEndpointUrl( 279 | "http://localhost:8080/spring-security-oauth-server/oauth/check_token"); 280 | tokenService.setClientId("foo"); 281 | tokenService.setClientSecret("bar"); 282 | return tokenService; 283 | } 284 | ``` 285 | 위와 같이 등록하면 자원서버는 등록된 URL로 accessToken이 유효한지 체크를 할것이다. 286 | 287 | # 마무리 하며... 288 | 스프링을 통해 OAuth2를 간단하게 구현해봤다. 첫 부분에는 토큰 관리, 클라이언트 등록 등 In-Memory를 통해 간단하게 OAuth2 구현했고, 중간 부분부터는 인증서버와 자원 서버의 클래스를 분리했다. 또한 토큰 관리, 클라이언트 등록 등을 InMemmory로 관리했었는데 이러한 것을 외부 DB 등록을 통해 관리하게 해봤다. OAuth2 인증 방식은 password 인증 방식만 구현해봤는데, 다른 인증 방식을 넣는 것은 어렵지 않다. 인증서버와 자원 서버만 설정되어 있다면 금방 도입할 수 있다. 물론 Product 용으로 개발하는 데까지는 많은 시간이 걸릴 것이다. 요즘 점점 시간이 지나면서 쿠키를 통한 session 방식의 로그인은 없어지고 있는 것 같다. 점점 모바일 환경 등 멀티 디바이스를 지원해야 하므로 웹 하나만을 위한 서버를 만드는 일은 비효율적이기 때문일 것이다. 그래서 많은 서버들이 Token 방식의 로그인 방식으로 옮기고 있는 것 같다. 그중에서도 OAuth2는 많은 인증 방식 등과 인증의 간편함을 가져 인기가 좋은것같다. 289 | 290 | 이 예제와 관련한 소스는 [JDBC 방식을 사용한 OAuth2 구현](https://github.com/minwan1/spring-security-oauth2-example/tree/example-2)에서 확인할 수 있습니다 291 | 292 | 참고 293 | * [OAuth 2 Developers Guide](http://projects.spring.io/spring-security-oauth/docs/oauth2.html) 294 | * [baeldung](http://www.baeldung.com/oauth-api-testing-with-spring-mvc) 295 | * [기억보단 기록을](http://jojoldu.tistory.com/234) 296 | * [이수홍](https://brunch.co.kr/@sbcoba/4) 297 | 298 | --------------------------------------------------------------------------------