├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── assets ├── Implicit Grant-image.png ├── oauth-code.png ├── oauth-prove.png ├── oauth2-login.png ├── oauth2-table-access-token.png ├── oauth2-table-refresh-token.png ├── ouath2-http.png └── ouath2-schema.png ├── docker-compose.yaml ├── docs ├── OAuth2-Grant.md └── OAuth2-RDBMSt.md ├── http └── api.http ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── example │ │ └── oauth2 │ │ ├── Application.java │ │ ├── SimpleController.java │ │ └── config │ │ ├── AuthorizationServerConfig.java │ │ ├── ResourceServerConfig.java │ │ └── SecurityConfig.java └── resources │ ├── application.yml │ ├── data-oauth2.sql │ └── schema-oauth2.sql └── test └── java └── com └── example └── oauth2 └── ApplicationTests.java /.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 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | /volumes/ 27 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fcheese10yun%2Fspringboot-oauth2&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) 2 | # 프로젝트 소개 3 | 4 | Spring Boot 2.1 기반으로 Spring Security OAuth2를 살펴보는 프로젝트입니다. Authorization Code Grant Type, Implicit Grant, Resource Owner Password Credentials Grant, Client Credentials Grant Type OAuth2 인증 방식에 대한 간단한 셈플 코드부터 OAuth2 TokenStore 저장을 mysql, redis 등 저장하는 예제들을 다룰 예정입니다. 계속 학습하면서 정리할 예정이라 심화 과정도 다룰 수 있게 될 거 같아 깃허브 Start, Watching 버튼을 누르시면 구독 신청받으실 수 있습니다. 저의 경험이 여러분에게 조금이라도 도움이 되기를 기원합니다. 5 | 6 | ## 구성 7 | * Spring Boot 2.1.0 8 | * Spring Security OAuth2 9 | * Lombok 10 | * Java8 11 | * MySQL 12 | * Docker 13 | 14 | ## 목차 15 | 16 | * [step-01 OAuth2 인증 방식 Flow 및 Sample Code](https://github.com/cheese10yun/springboot-oauth2/blob/master/docs/OAuth2-Grant.md) 17 | * [step-02 토큰과 클라이언트 정보 RDBMS 저장](https://github.com/cheese10yun/springboot-oauth2/blob/master/docs/OAuth2-RDBMSt.md) 18 | * [step-03 Redis를 이용한 토큰 저장 작업중...]() 19 | 20 | **step-XX Branch 정보를 의미합니다. 보고 싶은 목차의 Branch로 checkout을 해주세요** 21 | 22 | ## Project 실행 23 | 24 | 25 | ### Docker MySQL 설정 26 | ``` 27 | $ cd springboot-oauth2 28 | $ docker-compose up -d 29 | ``` 30 | 31 | ### 애플리케이션 구동 32 | * 최초 구동, MySQL Schema, Data Set Up 진행시 `"spring.profiles.active=init` 33 | * 일반 구동시 `"spring.profiles.active=local` 34 | -------------------------------------------------------------------------------- /assets/Implicit Grant-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/assets/Implicit Grant-image.png -------------------------------------------------------------------------------- /assets/oauth-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/assets/oauth-code.png -------------------------------------------------------------------------------- /assets/oauth-prove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/assets/oauth-prove.png -------------------------------------------------------------------------------- /assets/oauth2-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/assets/oauth2-login.png -------------------------------------------------------------------------------- /assets/oauth2-table-access-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/assets/oauth2-table-access-token.png -------------------------------------------------------------------------------- /assets/oauth2-table-refresh-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/assets/oauth2-table-refresh-token.png -------------------------------------------------------------------------------- /assets/ouath2-http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/assets/ouath2-http.png -------------------------------------------------------------------------------- /assets/ouath2-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/springboot-oauth2/1aa6afa90f0996df0705ef2ce687e956420e9ad1/assets/ouath2-schema.png -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mysql_member: 5 | container_name: mysql.oauth2.local 6 | image: mysql/mysql-server:5.7 7 | environment: 8 | MYSQL_ROOT_HOST: '%' 9 | MYSQL_DATABASE: 'oauth2' 10 | MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' 11 | ports: 12 | - '3306:3306' 13 | volumes: 14 | - './volumes/mysql/default:/var/lib/mysql' 15 | command: 16 | - 'mysqld' 17 | - '--character-set-server=utf8mb4' 18 | - '--collation-server=utf8mb4_unicode_ci' 19 | - '--sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' -------------------------------------------------------------------------------- /docs/OAuth2-Grant.md: -------------------------------------------------------------------------------- 1 | 2 | ## 목차 3 | - [목차](#%EB%AA%A9%EC%B0%A8) 4 | - [OAuth2 승인 방식의 종류](#oauth2-%EC%8A%B9%EC%9D%B8-%EB%B0%A9%EC%8B%9D%EC%9D%98-%EC%A2%85%EB%A5%98) 5 | - [Authorization Code Grant Type 방식](#authorization-code-grant-type-%EB%B0%A9%EC%8B%9D) 6 | - [Code](#code) 7 | - [인증](#%EC%9D%B8%EC%A6%9D) 8 | - [API 호출](#api-%ED%98%B8%EC%B6%9C) 9 | - [Implicit Grant Type 방식](#implicit-grant-type-%EB%B0%A9%EC%8B%9D) 10 | - [Code](#code-1) 11 | - [인증](#%EC%9D%B8%EC%A6%9D-1) 12 | - [API 호출](#api-%ED%98%B8%EC%B6%9C-1) 13 | - [Resource Owner Password Credentials Grant 방식](#resource-owner-password-credentials-grant-%EB%B0%A9%EC%8B%9D) 14 | - [Code](#code-2) 15 | - [인증](#%EC%9D%B8%EC%A6%9D-2) 16 | - [API 호출](#api-%ED%98%B8%EC%B6%9C-2) 17 | - [Client Credentials Grant Type 방식](#client-credentials-grant-type-%EB%B0%A9%EC%8B%9D) 18 | - [인증](#%EC%9D%B8%EC%A6%9D-3) 19 | - [API 호출](#api-%ED%98%B8%EC%B6%9C-3) 20 | - [Refresh Token](#refresh-token) 21 | - [Code](#code-3) 22 | - [인증](#%EC%9D%B8%EC%A6%9D-4) 23 | - [참고](#%EC%B0%B8%EA%B3%A0) 24 | 25 | 26 | ## OAuth2 승인 방식의 종류 27 | 28 | * Authorization Code Grant Type : 권한 부여 코드 승인 타입 29 | 클라이언트가 다른 사용자 대신 특정 리소스에 접근을 요청할 때 사용됩니다. 리스소 접근을 위한 사용자 명과 비밀번호, 권한 서버에 요청해서 받은 권한 코드를 함께 활용하여 리소스에 대한 엑세스 토큰을 받는 방식입니다. 30 | * Implicit Grant Type : 암시적 승인 31 | 권한 부여 코드 승인 타입과 다르게 권한 코드 교환 단계 없이 엑세스 토큰을 즉시 반환받아 이를 인증에 이용하는 방식입니다. 32 | * Resource Owner Password Credentials Grant Type : 리소스 소유자 암호 자격 증명 타입 33 | 클라이언트가 암호를 사용하여 엑세스 토큰에 대한 사용자의 자격 증명을 교환하는 방식입니다. 34 | * Client Credentials Grant Type : 클라이언트 자격 증명 타입 35 | 클라이언트가 컨텍스트 외부에서 액세스 토큰을 얻어 특정 리소스에 접근을 요청할 때 사용하는 방식입니다. 36 | 37 | ## Authorization Code Grant Type 방식 38 | 39 | ![oauth2-doe-grant-type](https://github.com/cheese10yun/TIL/raw/master/assets/oauth2-doe-grant-type_gnojt19me.png) 40 | 41 | * (1) 클라이언트가 파리미터러 클라이언트 ID, 리다이렉트 URI, 응답 타입을 code로 지정하여 권한 서버에 전달합니다. 정상적으로 인증이 되면 권한 코드 부여 코드를 클라이언트에게 보냅니다. 42 | - 응답 타입은 code, token 이 사용 가능합니다. 43 | - **응답 타입이 token 일 경우 암시적 승인 타입에 해당합니다.** 44 | * (2) 성공적으로 권한 부여 코드를 받은 클라이언트는 권한 부여 코드를 사용하여 엑세스 토큰을 권한 서버에 추가로 요청합니다. 이때 필요한 파라미터는 클라이언트 ID, 클라이언트 비밀번호, 리다이렉트 URI, 인증 타입 입니다. 45 | * (3) 마지막으로 받은 엑세스 토큰을 사용하여 리소스 서버에 사용자의 데이터를 보냅니다. 46 | 47 | ### Code 48 | 49 | ```java 50 | @Configuration 51 | @EnableAuthorizationServer 52 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { 53 | 54 | @Override 55 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 56 | clients 57 | .inMemory() // (1) 58 | .withClient("client") //(2) 59 | .secret("{bcrypt} $2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG") //(3) password 60 | .redirectUris("http://localhost:9000/callback") // (4) 61 | .authorizedGrantTypes("authorization_code") // (5) 62 | .scopes("read_profile"); // (6) 63 | } 64 | } 65 | ``` 66 | * (1): 간단한 설정을 위해 메모리에 저장시키겠습니다. 67 | * (2): 클라이언트 이름을 작성합니다. 68 | * (3): 시크릿을 작성해야합니다. 스프링 시큐리티 5.0 부터는 암호화 방식이 조 변경되서 반드시 암호화해서 저장하는 것을 권장합니다. 해당 암호는 password입니다. (현재 프로젝트는 스프링부트 2.1 이기 때문에 스프링 시큐리티 5.1 의존성을 주입받습니다.) 69 | * (4): 리다이렉트 URI을 설정합니다. 70 | * (5): `Authorization Code Grant` 타입을 설정합니다. 71 | * (6): scope를 지정합니다. 72 | 73 | 74 | ```java 75 | @Configuration 76 | @EnableResourceServer 77 | public class ResourceServerConfig extends ResourceServerConfigurerAdapter { 78 | 79 | @Override 80 | public void configure(HttpSecurity http) throws Exception { 81 | http.authorizeRequests().anyRequest().authenticated().and() 82 | .requestMatchers().antMatchers("/api/**"); 83 | } 84 | } 85 | 86 | @EnableWebSecurity 87 | @AllArgsConstructor 88 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 89 | 90 | @Override 91 | protected void configure(HttpSecurity http) throws Exception { 92 | //@formatter:off 93 | http 94 | .csrf().disable() 95 | .authorizeRequests().anyRequest().authenticated().and() 96 | .formLogin().and() 97 | .httpBasic(); 98 | //@formatter:on 99 | } 100 | } 101 | ``` 102 | * 리소스 서버 설정은 configure 설정만 간단하게 설정합니다. 103 | * 시큐리티 설정은 기본설정에서 csrf 설정만 disable 설정했습니다. 104 | 105 | 106 | ### 인증 107 | [http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=http://localhost:9000/callback&response_type=code&scope=read_profile](http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=http://localhost:9000/callback&response_type=code&scope=read_profile) 해당 페이지로 이동하면 아래와 같이 로그인 페이지(스프링 시큐리티 기본폼 이전보다 많이 이뻐졌다...)로 리다이렉트 됩니다. 108 | 109 | ![oauth2-login](/assets/oauth2-login.png) 110 | 로그인 정보를 입력합니다. 소셜 가입에서 해당 소셜의 아이디로 로그인하는 것과 동일한 행위입니다. 111 | 112 | ``` 113 | username: user 114 | password: pass 115 | ``` 116 | application.yml 에서 설정한 security user 정보를 입력해줍니다. 117 | 118 | 119 | ![oauth-code](/assets/oauth-prove.png) 120 | 유저 정보 인증이 완료되면 scope에 대한 권한 승인이 페이지가 나옵니다. 소셜 가입에서 프로필 정보, 프로필 사진 등을 요구하는 것과 마찬가지입니다. 121 | 122 | ![oauth-code](/assets/oauth-code.png) 123 | 권한 승인이 완료하면 권한 코드가 전송됩니다. [Authorization Code Grant Type 방식](#authorization-code-grant-type-%EB%B0%A9%EC%8B%9D) 에서 말한 `권한 부여 코드`를 응답받은 것입니다. 124 | 125 | 리다이렉트된 페이지에서 위에서 발급 받은 `권한 부여 코드`로 권한 서버에 `Access Token`을 요청 받을 수 있습니다. 이 부분이 실제로 구현하지 않았고 넘겨 받은 `권한 부여 코드`를 기반으로 권한 서버에 수동으로 호출 해보겠습니다. 126 | 127 | 아래의 curl을 실행 해보면 응답값을 확인해 볼 수 있습니다. 128 | ```curl 129 | curl -X POST \ 130 | http://localhost:8080/oauth/token \ 131 | -H 'Authorization: Basic Y2xpZW50OnBhc3N3b3Jk' \ 132 | -H 'Content-Type: application/x-www-form-urlencoded' \ 133 | -d 'code=xoS4mt&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&scope=read_profile' 134 | ``` 135 | 만약 IntelliJ를 사용하신다면 `api.http`를 이용해서 더 쉽게 호출 해볼 수 있습니다. 136 | ![ouath2-http](/assets/ouath2-http.png) 137 | 138 | 139 | ```json 140 | { 141 | "access_token": "623d5bc4-7172-44ae-85c1-73a297e6ab04", 142 | "token_type": "bearer", 143 | "expires_in": 43199, 144 | "scope": "read_profile" 145 | } 146 | ``` 147 | 148 | ### API 호출 149 | ``` 150 | curl -X GET \ 151 | http://localhost:8080/api/session \ 152 | -H 'Authorization: Bearer 623d5bc4-7172-44ae-85c1-73a297e6ab04' 153 | ``` 154 | 155 | curl을 이용해서 요청을 보내면 아래와 같이 응답값을 확인할 수 있습니다. 156 | ```json 157 | { 158 | "authorities": [], 159 | "details": { 160 | "remoteAddress": "0:0:0:0:0:0:0:1", 161 | "sessionId": null, 162 | "tokenValue": "623d5bc4-7172-44ae-85c1-73a297e6ab04" 163 | } 164 | .... 165 | } 166 | ``` 167 | 168 | 물론 token 정보를 넘기지않거나 유효하지 않으면 401 응답을 받습니다. 169 | 170 | 171 | ## Implicit Grant Type 방식 172 | ![Implicit Grant](https://github.com/cheese10yun/TIL/raw/master/assets/Implicit%20Grant.png) 173 | 174 | * (1) 클라이언트가 파리미터러 클라이언트 ID, 리다이렉트 URI, 응답 타입을 code로 지정하여 권한 서버에 전달합니다. 정상적으로 인증이 되면 권한 코드 부여 코드를 클라이언트에게 보냅니다. 175 | - 응답 타입은 code, token 이 사용 가능합니다. 176 | - **응답 타입이 token 일 경우 암시적 승인 타입에 해당합니다.** 177 | * (2) 응답 해준 Access Token 이 유효한지 검증 요청을 합니다. 178 | * (3) 요청 받은 Access Token 정보에 대한 검증에 대한 응답값을 돌려줍니다. 179 | * (4) 유효한 Access Token 기반으로 Resource Server와 통신합니다. 180 | 181 | ### Code 182 | 183 | ```java 184 | @Override 185 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 186 | clients 187 | .inMemory() 188 | .withClient("client") 189 | .secret("{bcrypt}$2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG") // password 190 | .redirectUris("http://localhost:9000/callback") 191 | .authorizedGrantTypes("authorization_code", "implicit") // (1) implicit 추가 192 | .accessTokenValiditySeconds(120) 193 | .refreshTokenValiditySeconds(240) 194 | .scopes("read_profile"); 195 | } 196 | ``` 197 | 코드는 변경은 거의 없습니다. `authorizedGrantTypes()` 메서드의 매개변수가 String 배열이기 때문에 `"implicit"`를 추가해주면 Implicit Grant 방식 설정이 완료됩니다. 198 | 199 | 주요한 차이점은 암시적이라는 그랜트 타입의 이름처럼 액세스 토큰이 암시적으로 획득되기 때문에 /auth/token 으로 요청을 보낼 필요가 없습니다. 200 | 201 | 202 | ### 인증 203 | [http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=http://localhost:9000/callback&response_type=token&scope=read_profile&state=test](http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=http://localhost:9000/callback&response_type=token&scope=read_profile&state=test) 으로 웹브라우저로 요청합니다. 204 | 205 | * 여기서 중요한점은 `response_type=token` 으로 요청합니다. `Authorization Code Grant Type` 에서는 `code`를 요청했지만 여기서는 `token` 정보를 응답받기 **위해서 `token` 정보를 요청합니다.** 206 | * `state=test`는 서버의 인가 엔드포인트로 **리다이렉트 될 때 전달할 수 있는 값입니다.** 아래에서 자세히 살펴보겠습니다. 207 | 208 | ![oauth2-login](/assets/oauth2-login.png) 209 | 210 | 위 플로우 처럼 로그인을 시도합니다. (username: user, password: pass) 211 | 212 | ![oauth-code](/assets/oauth-prove.png) 213 | 214 | 유저 정보 인증이 완료되면 scope에 대한 권한 승인이 페이지가 나옵니다. 소셜 가입에서 프로필 정보, 프로필 사진 등을 요구하는 것과 마찬가지입니다. (위에서 사용한 그림을 그대로 가져왔습니다.) 215 | 216 | 217 | ![Implicit Grant-image](/assets/Implicit%20Grant-image.png) 218 | [http://localhost:9000/callback#access_token=13474f0a-eb0f-4423-9bf3-9a2e9dd3e124&token_type=bearer&state=test&expires_in=120](http://localhost:9000/callback#access_token=13474f0a-eb0f-4423-9bf3-9a2e9dd3e124&token_type=bearer&state=test&expires_in=120) 219 | 220 | 리다이렉트된 URL 값입니다. 221 | * access_token=13474f0a-eb0f-4423-9bf3-9a2e9dd3e124 - Implicit Grant 방식에서는 Token 정보를 바로 응답 합니다. 222 | * state=test - 서버 인가 엔드포인트로 리다이렉트 될때 전달 받을 수 있습니다. 넘겨야할 값이 있다면 state를 힐 수 있습니다. 223 | * expires_in=120 - 토근 만료시간초입니다. `accessTokenValiditySeconds(120)` 메서드로 넘긴 토큰 만료 시간을 전달받습니다. 224 | * **Implicit Grant는 리프레시 토큰을 발급하지 않습니다.** 225 | * Implicit Grant은 서드파티 애플리케이션에 의한 리다이렉트 URI 등록이 필요합니다. 등록되지 않은 클라이언트에 액세스 토큰이 전달되는 것을 막기 위한 장치입니다. 226 | 227 | ### API 호출 228 | ``` 229 | curl -X GET \ 230 | http://localhost:8080/api/session \ 231 | -H 'Authorization: Bearer 623d5bc4-7172-44ae-85c1-73a297e6ab04' 232 | ``` 233 | 234 | curl을 이용해서 요청을 보내면 아래와 같이 응답값을 확인할 수 있습니다. 235 | ```json 236 | { 237 | "authorities": [], 238 | "details": { 239 | "remoteAddress": "0:0:0:0:0:0:0:1", 240 | "sessionId": null, 241 | "tokenValue": "623d5bc4-7172-44ae-85c1-73a297e6ab04" 242 | } 243 | .... 244 | } 245 | ``` 246 | 247 | ## Resource Owner Password Credentials Grant 방식 248 | 249 | ![Resource Owner Password Credentials Grant](https://github.com/cheese10yun/TIL/raw/master/assets/Resource%20Owner%20Password%20Credentials%20Grant.png) 250 | 251 | * (1) 인증을 진행합니다. 대부분 ID, Password를 통해서 자격 증명이 진행됩니다. 252 | * (2) 넘겨 받은 정보기반으로 권한 서버에 Access Token 정보를 요청합니다. 253 | * (3) Access Token 정보를 응답 받습니다. 이때 Refresh Token 정보도 넘겨 줄 수도 있습니다. 254 | * (4) Access Token 기반으로 Resource Server와 통신합니다. 255 | 256 | ### Code 257 | 258 | ```java 259 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { 260 | private final AuthenticationManager authenticationManager; 261 | 262 | @Override 263 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 264 | clients 265 | .inMemory() 266 | .withClient("client") 267 | .secret("{bcrypt}$2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG") // password 268 | .redirectUris("http://localhost:9000/callback") 269 | .authorizedGrantTypes("authorization_code", "implicit", "password") // (1) password 타입 추가 270 | .accessTokenValiditySeconds(120) // access token 만료시간 271 | .refreshTokenValiditySeconds(240) // refresh token 만료시간 272 | .scopes("read_profile"); 273 | } 274 | 275 | 276 | //(2) 277 | @Override 278 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 279 | //@formatter:off 280 | endpoints 281 | .authenticationManager(authenticationManager) // Bean 등록은 SecurityConfig 에서 등록합니다. 282 | ; 283 | //@formatter:on 284 | } 285 | } 286 | ``` 287 | * (1) password 타입을 추가합니다. 288 | * (2) authenticationManager 빈등록을 해주고 의존성 주입을 받아 `authenticationManager(authenticationManager)` 메서드를 통해 객체를 넘겨줍니다. 289 | 290 | 291 | ### 인증 292 | 293 | ``` 294 | curl -X POST \ 295 | http://localhost:8080/oauth/token \ 296 | -H 'Authorization: Basic Y2xpZW50OnBhc3N3b3Jk' \ 297 | -H 'Content-Type: application/x-www-form-urlencoded' \ 298 | -d 'username=user&password=pass&grant_type=password&scope=read_profile' 299 | ``` 300 | 301 | 인증 요청은 위 플로우 설명 처럼 password 기반으로 token 정보를 요청합니다. 유저의 비밀번호 인증이 완료되면 아래와 같이 응답을 받습니다. 302 | 303 | ```json 304 | { 305 | "access_token": "bb438582-0d12-4a67-94f5-0bdbcd8b29ef", 306 | "token_type": "bearer", 307 | "expires_in": 103, 308 | "scope": "read_profile" 309 | } 310 | ``` 311 | 312 | 313 | ### API 호출 314 | ``` 315 | curl -X GET \ 316 | http://localhost:8080/api/session \ 317 | -H 'Authorization: Bearer 623d5bc4-7172-44ae-85c1-73a297e6ab04' 318 | ``` 319 | 320 | curl을 이용해서 요청을 보내면 아래와 같이 응답값을 확인할 수 있습니다. 321 | ```json 322 | { 323 | "authorities": [], 324 | "details": { 325 | "remoteAddress": "0:0:0:0:0:0:0:1", 326 | "sessionId": null, 327 | "tokenValue": "623d5bc4-7172-44ae-85c1-73a297e6ab04" 328 | } 329 | .... 330 | } 331 | ``` 332 | 333 | ## Client Credentials Grant Type 방식 334 | 335 | ![Client Credentials Grant Type](https://github.com/cheese10yun/TIL/raw/master/assets/Client%20Credentials%20Grant%20Type.png) 336 | 337 | * (1) Access Token 정보를 요청합니다. 338 | * (3) Access Token 정보를 응답합니다. 이때 Refresh Token 정보는 응답하지 않는 것을 권장합니다. 별다른 인증 절차가 없기 때문에 Refresh Token 까지 넘기지 않는 것이라고 생각합니다. 339 | * (4) Access Token 기반으로 Resource Server와 통신합니다. 340 | 341 | 342 | ```java 343 | @Override 344 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 345 | clients 346 | .inMemory() 347 | .withClient("client") 348 | .secret("{bcrypt}$2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG") // password 349 | .redirectUris("http://localhost:9000/callback") 350 | .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials") // client_credentials 추가 351 | .accessTokenValiditySeconds(120) 352 | .refreshTokenValiditySeconds(240) 353 | .scopes("read_profile"); 354 | } 355 | ``` 356 | * 코드의 변경사항은 `client_credentials` 변경 뿐입니다. 357 | * 리소스 주인이 어떤 권한인증을 하지 않기 때문에 Refresh Token을 넘겨주지 않는것이 바람직합니다. 358 | * Client Credentials Grant Type은 리소스 소유자에게 권한 위임 받아 리소스에 접근하는 것이 아니라 자기 자신이 애플리케이션을 사용할 목적으로 사용하는 것이 일반적입니다. 359 | 360 | ### 인증 361 | ``` 362 | curl -X POST \ 363 | http://localhost:8080/oauth/token \ 364 | -H 'Authorization: Basic Y2xpZW50OnBhc3N3b3Jk' \ 365 | -H 'Content-Type: application/x-www-form-urlencoded' \ 366 | -d 'grant_type=client_credentials&scope=read_profile&undefined=' 367 | ``` 368 | 369 | curl을 이용해서 요청을 보내면 아래와 같이 응답값을 확인할 수 있습니다. 370 | 371 | ```json 372 | { 373 | "access_token": "fd4123f6-1f4f-4eeb-8342-e4eefefaee40", 374 | "token_type": "bearer", 375 | "expires_in": 119, 376 | "scope": "read_profile" 377 | } 378 | ``` 379 | 380 | ### API 호출 381 | ``` 382 | curl -X GET \ 383 | http://localhost:8080/api/session \ 384 | -H 'Authorization: Bearer 623d5bc4-7172-44ae-85c1-73a297e6ab04' 385 | ``` 386 | 387 | curl을 이용해서 요청을 보내면 아래와 같이 응답값을 확인할 수 있습니다. 388 | ```json 389 | { 390 | "authorities": [], 391 | "details": { 392 | "remoteAddress": "0:0:0:0:0:0:0:1", 393 | "sessionId": null, 394 | "tokenValue": "623d5bc4-7172-44ae-85c1-73a297e6ab04" 395 | } 396 | .... 397 | } 398 | ``` 399 | 400 | ## Refresh Token 401 | 리프레시 토큰 그랜트 타입은 액세스 토큰이 만료될 때마다 리소스 소유자가 매번 인가 서버를 통해 수행해야 하는 인증, 인가 절차를 수행하지 않아도 되기 때문에 조금더 좋은 사용자 경험을 제공할 수 있습니다. 402 | 403 | ### Code 404 | ```java 405 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { 406 | 407 | private final AuthenticationManager authenticationManager; 408 | 409 | @Override 410 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 411 | clients 412 | .inMemory() 413 | .withClient("client") 414 | .secret("{bcrypt}$2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG") // password 415 | .redirectUris("http://localhost:9000/callback") 416 | .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token") // refresh_token 추가 417 | .accessTokenValiditySeconds(120) 418 | .refreshTokenValiditySeconds(240) 419 | .scopes("read_profile"); 420 | } 421 | } 422 | ``` 423 | * `authorizedGrantTypes()` 메서드에 refresh을 추가합니다. 424 | 425 | 426 | ### 인증 427 | 428 | ``` 429 | curl -X POST \ 430 | http://localhost:8080/oauth/token \ 431 | -H 'Content-Type: application/x-www-form-urlencoded' \ 432 | -d 'username=user&password=pass&grant_type=password&scope=read_profile' 433 | ``` 434 | 아래의 JSON이 응답값입니다. 435 | 436 | ```json 437 | { 438 | "access_token": "bbcb6b66-c822-4816-a5ef-afe57becdb12", 439 | "token_type": "bearer", 440 | "refresh_token": "eb465a46-0b9f-4180-bce6-bf2918c378c2", 441 | "expires_in": 119, 442 | "scope": "read_profile" 443 | } 444 | ``` 445 | **refresh_token token이 추가 된것을 확인 할 수 있습니다.** 446 | 447 | 448 | **access token의 만료시간이 지났을 경우 해당 토큰으로 접근하면 아래와 같은 응답값을 받게 됩니다.** 449 | ```json 450 | { 451 | "error": "invalid_token", 452 | "error_description": "Access token expired: a5ea1096-a6e3-4d4b-a511-8a896b8e85b0" 453 | } 454 | ``` 455 | 456 | 이때 위에서 발금받은 refresh token 기반으로 access token 정보를 다시 요청 할 수 있습니다. 457 | 458 | ``` 459 | curl -X POST \ 460 | http://localhost:8080/oauth/token \ 461 | -H 'Authorization: Basic Y2xpZW50OnBhc3N3b3Jk' \ 462 | -H 'Content-Type: application/x-www-form-urlencoded' \ 463 | -d 'grant_type=refresh_token&scope=read_profile&refresh_token=b7d6a15c-f063-4457-b417-0555cb54b57a' 464 | ``` 465 | refresh token 정보로 access token 정보를 받을 수 있습니다. 466 | 467 | ```json 468 | { 469 | "access_token": "8edce9ad-e256-4601-9388-510d37e951bc", 470 | "token_type": "bearer", 471 | "refresh_token": "b7d6a15c-f063-4457-b417-0555cb54b57a", 472 | "expires_in": 119, 473 | "scope": "read_profile" 474 | } 475 | ``` 476 | 477 | **위에서 설명했듯이 Refresh Token을 받을 수 있는 타입은 `Authorization Code Grant`, `Resource Owner Password Credentials Grant`입니다.** 그 이외에는 Refresh Token을 넘겨주지 않습니다. 478 | 479 | ## 참고 480 | * [OAuth2 이해하기](http://www.bubblecode.net/en/2016/01/22/understanding-oauth2/) 481 | * [처음으로 배우는 스프링 부트2](http://www.hanbit.co.kr/store/books/look.php?p_code=B4458049183) 482 | * [OAuth 2.0 쿡북](http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9791161752211&orderClick=LAG&Kc=) -------------------------------------------------------------------------------- /docs/OAuth2-RDBMSt.md: -------------------------------------------------------------------------------- 1 | ## 목차 2 | - [목차](#%EB%AA%A9%EC%B0%A8) 3 | - [데이터베이스 스키마 구성하기](#%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%8A%A4%ED%82%A4%EB%A7%88-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0) 4 | - [oauth_client_details table](#oauthclientdetails-table) 5 | - [프로젝트 구동](#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EB%8F%99) 6 | - [OAuth2 테이블](#oauth2-%ED%85%8C%EC%9D%B4%EB%B8%94) 7 | - [Code](#code) 8 | - [Resource Owner Password Credentials Grant](#resource-owner-password-credentials-grant) 9 | - [요청](#%EC%9A%94%EC%B2%AD) 10 | - [응답](#%EC%9D%91%EB%8B%B5) 11 | - [Authorization Code Grant Type](#authorization-code-grant-type) 12 | - [요청](#%EC%9A%94%EC%B2%AD-1) 13 | - [응답](#%EC%9D%91%EB%8B%B5-1) 14 | - [입력된 데이터](#%EC%9E%85%EB%A0%A5%EB%90%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0) 15 | - [참고](#%EC%B0%B8%EA%B3%A0) 16 | 17 | ## 데이터베이스 스키마 구성하기 18 | 19 | 가장 먼저 데이터베이스를 생성해야합니다. 20 | 21 | ``` 22 | mysql> create database oauth2; 23 | ``` 24 | 25 | ```yml 26 | spring: 27 | profiles: init 28 | jpa: 29 | database: mysql 30 | properties.hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBDialect 31 | hibernate: 32 | ddl-auto: create 33 | show-sql: true 34 | properties: 35 | hibernate.format_sql: true 36 | datasource: 37 | url: jdbc:mysql://localhost:3306/oauth2?useSSL=false&serverTimezone=UTC 38 | username: 39 | password: 40 | driver-class-name: com.mysql.cj.jdbc.Driver 41 | initialization-mode: always 42 | platform: oauth2 43 | ``` 44 | 45 | 데이터 베이스 구성을 위한 profile을 구성합니다. 해당 profile은 OAuth2에 대한 스키마 생성 및 더미데이터를 insert 해줍니다. 46 | 47 | `datasource.platform` 속성 값에 `oauth2`가 입력되어 있습니다. 간단하게 설명드리면 `resources`에 위치한 `${platform}.sql`을 실행시킵니다. 48 | 49 | 프로젝트에 `resources` 디렉토리에는 `schema-oauth2.sql`, `data-oauth2.sql`가 존재하고 각각은 테이블 스미카, `oauth_client_details` 더미데이터 insert 입니다. 세부적인 sql 파일을 직접 확인하는것을 권장드립니다. 50 | 51 | 프로젝트의 profile은 init으로 구동시에 `schema-oauth2.sql` , `data-oauth2.sql`가 실행됩니다. 프로젝트 실행시에 딱 한번만 init으로 진행하시고 이후 부터는 local로 진행하시면 됩니다. 52 | 53 | 54 | ### oauth_client_details table 55 | ```sql 56 | INSERT INTO `oauth_client_details`( 57 | `client_id`, 58 | `resource_ids`, 59 | `client_secret`, 60 | `scope`, 61 | `authorized_grant_types`, 62 | `web_server_redirect_uri`, 63 | `authorities`, 64 | `access_token_validity`, 65 | `refresh_token_validity`, 66 | `additional_information`, 67 | `autoapprove` 68 | ) 69 | 70 | VALUES( 71 | 'client', 72 | null, 73 | '{bcrypt}$2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG', 74 | 'read_profile,read_posts', 75 | 'authorization_code,implicit,password,client_credentials,refresh_token', 76 | 'http://localhost:9000/callback', 77 | null, 78 | 3000, 79 | 6000, 80 | null , 81 | 'false' 82 | ); 83 | ``` 84 | * client_id : 클라이언트를 구분하는 ID 85 | * client_secret : 클라이언트의 비밀번호로 OAuth2 서버에 요청할때 인증을 하기위한 용도로 사용한다. 86 | * authorized_grant_types: OAuth2 승인 방식의 종류 `...`, `...` 이런 형식으로`,`를 이용해서 구분한다. 87 | * access_token_validity : Access Token의 유효시간 88 | * refresh_token_validity : Refresh Token의 유효 시간 89 | * scope: 클라이언트로 발급된 Access Token의 Scope, 리소스에 접근 가능한 권한 구분은 `,` 으로한다 90 | * autoapprove: 권한코드 방식 같은 형태로 Access Token을 발급받을 때에는 사용자에게 scope 범위를 허가받는 화면이 나옵니다. 이 화면 자체가 나오지 않게 설정하는 값입니다. true하면 아래 화면이 나오지 않습니다. 91 | 92 | ![](/assets/oauth-prove.png) 93 | 94 | 95 | ## 프로젝트 구동 96 | ```yml 97 | spring: 98 | profiles: 99 | active: init # 기본은 local로 되어있습니다. 100 | ``` 101 | 102 | 프로젝트를 실행할때 `application.yml` 최상단에 있는 active를 local로 변경합니다. **스키마, 더미데이터 입력이 목적이기 때문에 프로젝트 최초 1회 구동시 init으로 진행하시고 이후는 local로 진행하면됩니다.** 103 | 104 | 105 | 106 | ### OAuth2 테이블 107 | ![ouath2-schema](/assets/ouath2-schema.png) 108 | 109 | 프로젝트를 구동하면 테이블 생성 및 더미데이터를 확인 할 수 있습니다. 110 | 111 | 112 | ## Code 113 | 이전에 코드에 몇가지 빈들을 등록하게 되면 간단하게 RDBMS에 토큰 정보를 저장할 수 있습니다. 114 | 115 | ```java 116 | @Configuration 117 | @EnableAuthorizationServer 118 | @AllArgsConstructor 119 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { 120 | 121 | private final DataSource dataSource; // (1) 122 | 123 | 124 | @Override 125 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 126 | clients 127 | .jdbc(dataSource) // (5) 128 | ; 129 | //기존 코드 130 | // .inMemory() 131 | // .withClient("client") 132 | // .secret("{bcrypt}$2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG") // password 133 | // .redirectUris("http://localhost:9000/callback") 134 | // .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token") 135 | // .accessTokenValiditySeconds(120) 136 | // .refreshTokenValiditySeconds(240) 137 | // .scopes("read_profile"); 138 | } 139 | 140 | @Override 141 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 142 | //@formatter:off 143 | endpoints //(4) 144 | .approvalStore(approvalStore()) 145 | .tokenStore(tokenStore()) 146 | .authenticationManager(authenticationManager) 147 | ; 148 | //@formatter:on 149 | } 150 | 151 | @Bean 152 | public TokenStore tokenStore() { //(2) 153 | return new JdbcTokenStore(dataSource); 154 | } 155 | 156 | @Bean 157 | public ApprovalStore approvalStore() { //(3) 158 | return new JdbcApprovalStore(dataSource); 159 | } 160 | 161 | @Bean 162 | public PasswordEncoder passwordEncoder() { 163 | return new BCryptPasswordEncoder(); 164 | } 165 | } 166 | ``` 167 | * (1) 토큰에 대한 영속화를 진행하기 위해서 `Datasoruce` 의존성을 주입받습니다. 168 | * (2) 주입 받은 `Datasoruce` 의존성을 기반으로 `JdbcTokenStore`을 생성합니다. 169 | * (3) 2번과 마찬가지로 `Datasoruce`을 주입시켜 `JdbcApprovalStore`을 생성합니다. 170 | * (4) 2,3 번에서 생성한 객체을 `AuthorizationServerEndpointsConfigurer` 객체에 넣어줍니다. 171 | * (5) clinet `inMemory()` 방식에서 `jdbc()` 방식으로 변경합니다. 의존성은 dataSource 주입해줍니다. 172 | 173 | TokenStore 인터페이스는 Access Token, Refresh Token과 관련된 인증 데이터를 저장, 검색, 제거, 읽기에 대한 정의입니다. 174 | ApprovalStore 인터페이스는 리소스의 소유자의 승인을 추가, 검색, 취소 하기위한 메서드들이 정의되있습니다. 175 | 176 | **이렇듯 스프링에서는 인터페이스를 재공함으로써 확장포인트를 열어두어 확장에는 열려있는 모듈을 지향하고 있습니다.** 177 | 178 | 대표적으로 Resource Owner Password Credentials Grant, Authorization Code Grant Type 인증을 살펴보겠습니다. 179 | 180 | ### Resource Owner Password Credentials Grant 181 | 182 | #### 요청 183 | ```bash 184 | curl -X POST \ 185 | http://localhost:8080/oauth/token \ 186 | -H 'Authorization: Basic Y2xpZW50OnBhc3N3b3Jk' \ 187 | -H 'Content-Type: application/x-www-form-urlencoded' \ 188 | -d 'username=user&password=pass&grant_type=password&scope=read_profile' 189 | ``` 190 | 191 | #### 응답 192 | ```json 193 | { 194 | "access_token": "a7cce128-bd4a-4986-a56b-de75f5246364", 195 | "token_type": "bearer", 196 | "refresh_token": "a7c43419-4875-47f5-9d79-829301ed0030", 197 | "expires_in": 871, 198 | "scope": "read_profile" 199 | } 200 | ``` 201 | 202 | ### Authorization Code Grant Type 203 | 204 | #### 요청 205 | [http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=http://localhost:9000/callback&response_type=code&scope=read_profile](http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=http://localhost:9000/callback&response_type=code&scope=read_profile) 해당 페이지로 이동 206 | 207 | ![oauth2-login](/assets/oauth2-login.png) 208 | 로그인 정보를 입력합니다. 209 | 210 | ``` 211 | username: user 212 | password: pass 213 | ``` 214 | 215 | ![oauth-code](/assets/oauth-prove.png) 216 | 217 | ![oauth-code](/assets/oauth-code.png) 218 | 권한 승인이 완료하면 권한 코드가 전송됩니다. [Authorization Code Grant Type 방식](#authorization-code-grant-type-%EB%B0%A9%EC%8B%9D) 에서 말한 `권한 부여 코드`를 응답받은 것입니다. 219 | 220 | 넘겨받은 승인 코드로 Authorization Code Grant 인증을 진행합니다. 221 | 222 | ```bash 223 | curl -X POST \ 224 | http://localhost:8080/oauth/token \ 225 | -H 'Authorization: Basic Y2xpZW50OnBhc3N3b3Jk' \ 226 | -H 'Content-Type: application/x-www-form-urlencoded' \ 227 | -d 'code=rNHo29&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&scope=read_profile' 228 | ``` 229 | 만약 IntelliJ를 사용하신다면 `api.http`를 이용해서 더 쉽게 호출 해볼 수 있습니다. 230 | ![ouath2-http](/assets/ouath2-http.png) 231 | 232 | #### 응답 233 | ```json 234 | { 235 | "access_token": "883c329b-8f05-457c-907c-ce8637a7aa80", 236 | "token_type": "bearer", 237 | "refresh_token": "a7c43419-4875-47f5-9d79-829301ed0030", 238 | "expires_in": 2942, 239 | "scope": "read_profile" 240 | } 241 | ``` 242 | 243 | ### 입력된 데이터 244 | ![oauth2-table-access-token](/assets/oauth2-table-access-token.png) 245 | ![oauth2-table-refresh-token](/assets/oauth2-table-refresh-token.png) 246 | 247 | 위에서 발급 받은 Access Token, Refresh Token을 확인 할 수 있습니다. 248 | 249 | 250 | ## 참고 251 | * [Spring Boot로 만드는 OAuth2 시스템 8 OAuth2 서버를 커스터마이징 해보자(클라이언트 관리 편)](https://brunch.co.kr/@sbcoba/8) 252 | * [OAuth 2.0 쿡북](http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9791161752211&orderClick=LAG&Kc=) -------------------------------------------------------------------------------- /http/api.http: -------------------------------------------------------------------------------- 1 | # For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). 2 | # 3 | # Following HTTP Request Live Templates are available: 4 | # * 'gtrp' and 'gtr' create a GET request with or without query parameters; 5 | # * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; 6 | # * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); 7 | 8 | 9 | # password 10 | 11 | #http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=http://localhost:9000/callback&response_type=code&scope=read_profile 12 | #http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=http://localhost:9000/callback&response_type=token&scope=read_profile&state=test 13 | 14 | 15 | # Authorization Code Grant Type 토큰 요청 16 | POST http://localhost:8080/oauth/token 17 | Content-Type: application/x-www-form-urlencoded 18 | Authorization: Basic Y2xpZW50OnBhc3N3b3Jk 19 | 20 | code=lg8HLw&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&scope=read_profile 21 | ### 22 | 23 | # Password Grant Type 토큰 요청 24 | POST http://localhost:8080/oauth/token 25 | Content-Type: application/x-www-form-urlencoded 26 | Authorization: Basic Y2xpZW50OnBhc3N3b3Jk 27 | 28 | username=user&password=pass&grant_type=password&scope=read_profile 29 | ### 30 | 31 | # Client Credentials Grant Type 방식 32 | POST http://localhost:8080/oauth/token 33 | Content-Type: application/x-www-form-urlencoded 34 | Authorization: Basic Y2xpZW50OnBhc3N3b3Jk 35 | 36 | grant_type=client_credentials&scope=read_profile 37 | ### 38 | 39 | # Refresh Token Type 40 | POST http://localhost:8080/oauth/token 41 | Content-Type: application/x-www-form-urlencoded 42 | Authorization: Basic Y2xpZW50OnBhc3N3b3Jk 43 | 44 | grant_type=refresh_token&scope=read_profile&refresh_token=2ec7cfcc-110b-42a0-859f-fc72b6067935 45 | ### 46 | 47 | 48 | # session 조회 49 | GET http://localhost:8080/api/session 50 | Accept: application/json 51 | Authorization: Bearer 9298f76f-c10c-4802-ba60-6c49d3e715f3 52 | 53 | ### -------------------------------------------------------------------------------- /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 Mingw, 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 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /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 set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | oauth2 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | oauth2 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.1.0.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-security 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-thymeleaf 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | 46 | org.springframework.security.oauth 47 | spring-security-oauth2 48 | 2.3.4.RELEASE 49 | 50 | 51 | 52 | mysql 53 | mysql-connector-java 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-jdbc 59 | 60 | 61 | 62 | com.h2database 63 | h2 64 | runtime 65 | 66 | 67 | mysql 68 | mysql-connector-java 69 | runtime 70 | 71 | 72 | org.projectlombok 73 | lombok 74 | true 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-starter-test 79 | test 80 | 81 | 82 | org.springframework.restdocs 83 | spring-restdocs-mockmvc 84 | test 85 | 86 | 87 | org.springframework.security 88 | spring-security-test 89 | test 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-maven-plugin 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth2/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth2; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth2/SimpleController.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth2; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @RestController 9 | public class SimpleController { 10 | 11 | @GetMapping("/api/session") 12 | public Authentication session() { 13 | return SecurityContextHolder.getContext().getAuthentication(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth2/config/AuthorizationServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth2.config; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 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.EnableAuthorizationServer; 11 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 12 | import org.springframework.security.authentication.AuthenticationManager; 13 | import org.springframework.security.oauth2.provider.approval.Approval; 14 | import org.springframework.security.oauth2.provider.approval.ApprovalStore; 15 | import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore; 16 | import org.springframework.security.oauth2.provider.token.TokenStore; 17 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; 18 | 19 | import javax.sql.DataSource; 20 | 21 | @Configuration 22 | @EnableAuthorizationServer 23 | @AllArgsConstructor 24 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { 25 | 26 | private final AuthenticationManager authenticationManager; 27 | private final DataSource dataSource; 28 | 29 | @Override 30 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 31 | clients 32 | .jdbc(dataSource) 33 | ; 34 | // .inMemory() 35 | // .withClient("client") 36 | // .secret("{bcrypt}$2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG") // password 37 | // .redirectUris("http://localhost:9000/callback") 38 | // .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token") 39 | // .accessTokenValiditySeconds(120) 40 | // .refreshTokenValiditySeconds(240) 41 | // .scopes("read_profile"); 42 | } 43 | 44 | 45 | @Override 46 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 47 | //@formatter:off 48 | endpoints 49 | .approvalStore(approvalStore()) 50 | .tokenStore(tokenStore()) 51 | .authenticationManager(authenticationManager) 52 | ; 53 | //@formatter:on 54 | } 55 | 56 | @Bean 57 | public TokenStore tokenStore() { 58 | return new JdbcTokenStore(dataSource); 59 | } 60 | 61 | @Bean 62 | public ApprovalStore approvalStore() { 63 | return new JdbcApprovalStore(dataSource); 64 | } 65 | 66 | @Bean 67 | public PasswordEncoder passwordEncoder() { 68 | return new BCryptPasswordEncoder(); 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth2/config/ResourceServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth2.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 6 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 7 | 8 | @Configuration 9 | @EnableResourceServer 10 | public class ResourceServerConfig extends ResourceServerConfigurerAdapter { 11 | 12 | @Override 13 | public void configure(HttpSecurity http) throws Exception { 14 | http.authorizeRequests().anyRequest().authenticated().and() 15 | .requestMatchers().antMatchers("/api/**"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/example/oauth2/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth2.config; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | import org.springframework.security.crypto.factory.PasswordEncoderFactories; 12 | import org.springframework.security.crypto.password.PasswordEncoder; 13 | 14 | @EnableWebSecurity 15 | @AllArgsConstructor 16 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 17 | 18 | @Override 19 | protected void configure(HttpSecurity http) throws Exception { 20 | //@formatter:off 21 | http 22 | .csrf().disable() 23 | .authorizeRequests().anyRequest().authenticated().and() 24 | .formLogin().and() 25 | .httpBasic(); 26 | //@formatter:on 27 | } 28 | 29 | @Bean 30 | public PasswordEncoder encoder() { 31 | return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 32 | } 33 | 34 | 35 | @Bean 36 | @Override 37 | protected AuthenticationManager authenticationManager() throws Exception { 38 | return super.authenticationManager(); 39 | } 40 | 41 | @Autowired 42 | public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { 43 | //@formatter:off 44 | auth 45 | .inMemoryAuthentication() 46 | .withUser("user").password("{noop}pass").roles("USER"); 47 | //@formatter:on 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: local 4 | 5 | logging: 6 | level: 7 | ROOT: info 8 | org: 9 | hibernate: 10 | type: trace 11 | 12 | # Local env 13 | --- 14 | spring: 15 | profiles: local 16 | jpa: 17 | database: mysql 18 | properties.hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBDialect 19 | hibernate: 20 | ddl-auto: create 21 | show-sql: true 22 | properties: 23 | hibernate.format_sql: true 24 | datasource: 25 | url: jdbc:mysql://localhost:3306/oauth2?useSSL=false&serverTimezone=UTC 26 | username: root 27 | password: 28 | driver-class-name: com.mysql.cj.jdbc.Driver 29 | 30 | # Init 31 | --- 32 | spring: 33 | profiles: init 34 | jpa: 35 | database: mysql 36 | properties.hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBDialect 37 | hibernate: 38 | ddl-auto: create 39 | show-sql: true 40 | properties: 41 | hibernate.format_sql: true 42 | datasource: 43 | url: jdbc:mysql://localhost:3306/oauth2?useSSL=false&serverTimezone=UTC 44 | username: root 45 | password: 46 | driver-class-name: com.mysql.cj.jdbc.Driver 47 | initialization-mode: always 48 | platform: oauth2 49 | -------------------------------------------------------------------------------- /src/main/resources/data-oauth2.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `oauth_client_details`( 2 | `client_id`, 3 | `resource_ids`, 4 | `client_secret`, 5 | `scope`, 6 | `authorized_grant_types`, 7 | `web_server_redirect_uri`, 8 | `authorities`, 9 | `access_token_validity`, 10 | `refresh_token_validity`, 11 | `additional_information`, 12 | `autoapprove` 13 | ) 14 | 15 | VALUES( 16 | 'client', 17 | null, 18 | '{bcrypt}$2a$10$iP9ejueOGXO29.Yio7rqeuW9.yOC4YaV8fJp3eIWbP45eZSHFEwMG', 19 | 'read_profile,read_posts', 20 | 'authorization_code,implicit,password,client_credentials,refresh_token', 21 | 'http://localhost:9000/callback', 22 | null, 23 | 3000, 24 | 6000, 25 | null , 26 | 'false' 27 | ); -------------------------------------------------------------------------------- /src/main/resources/schema-oauth2.sql: -------------------------------------------------------------------------------- 1 | create table IF NOT EXISTS oauth_client_details ( 2 | client_id VARCHAR(256) PRIMARY KEY, 3 | resource_ids VARCHAR(256), 4 | client_secret VARCHAR(256), 5 | scope VARCHAR(256), 6 | authorized_grant_types VARCHAR(256), 7 | web_server_redirect_uri VARCHAR(256), 8 | authorities VARCHAR(256), 9 | access_token_validity INTEGER, 10 | refresh_token_validity INTEGER, 11 | additional_information VARCHAR(4096), 12 | autoapprove VARCHAR(256) 13 | ); 14 | 15 | create table IF NOT EXISTS oauth_client_token ( 16 | token_id VARCHAR(256), 17 | token LONG VARBINARY, 18 | authentication_id VARCHAR(256) PRIMARY KEY, 19 | user_name VARCHAR(256), 20 | client_id VARCHAR(256) 21 | ); 22 | 23 | create table IF NOT EXISTS oauth_access_token ( 24 | token_id VARCHAR(256), 25 | token LONG VARBINARY, 26 | authentication_id VARCHAR(256) PRIMARY KEY, 27 | user_name VARCHAR(256), 28 | client_id VARCHAR(256), 29 | authentication LONG VARBINARY, 30 | refresh_token VARCHAR(256) 31 | ); 32 | 33 | create table IF NOT EXISTS oauth_refresh_token ( 34 | token_id VARCHAR(256), 35 | token LONG VARBINARY, 36 | authentication LONG VARBINARY 37 | ); 38 | 39 | create table IF NOT EXISTS oauth_code ( 40 | code VARCHAR(256), authentication LONG VARBINARY 41 | ); 42 | 43 | create table IF NOT EXISTS oauth_approvals ( 44 | userId VARCHAR(256), 45 | clientId VARCHAR(256), 46 | scope VARCHAR(256), 47 | status VARCHAR(10), 48 | expiresAt TIMESTAMP, 49 | lastModifiedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 50 | ); 51 | 52 | -- customized oauth_client_details table 53 | create table IF NOT EXISTS ClientDetails ( 54 | appId VARCHAR(256) PRIMARY KEY, 55 | resourceIds VARCHAR(256), 56 | appSecret VARCHAR(256), 57 | scope VARCHAR(256), 58 | grantTypes VARCHAR(256), 59 | redirectUrl VARCHAR(256), 60 | authorities VARCHAR(256), 61 | access_token_validity INTEGER, 62 | refresh_token_validity INTEGER, 63 | additionalInformation VARCHAR(4096), 64 | autoApproveScopes VARCHAR(256) 65 | ); -------------------------------------------------------------------------------- /src/test/java/com/example/oauth2/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.oauth2; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------