關于OAuth 2.0,請參見下面這兩篇文章(牆裂推薦):
《OAuth 2.0》
《Spring Security OAuth 2.0》
紙上得來終覺淺,絕知此事要躬行。理論知識了解以後,最終還是要動手實踐,不親自做一遍永遠不知道裡面有多少坑。本節的重點是用Spring Security實作授權碼模式。
1. maven依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cjs.example</groupId>
<artifactId>cjs-oauth2-code-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cjs-oauth2-code-server</name>
<description></description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 配置Security
package com.cjs.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
// auth.inMemoryAuthentication().withUser("zhangsan").password("$2a$10$qsJ/Oy1RmUxFA.YtDT8RJ.Y2kU3U4z0jvd35YmiMOAPpD.nZUIRMC").roles("USER");
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
User.UserBuilder builder = User.builder();
UserDetails user = builder.username("zhangsan").password("$2a$10$GStfEJEyoSHiSxnoP3SbD.R8XRowP1QKOdi.N6/iFEwEJWTQqlSba").roles("USER").build();
UserDetails admin = builder.username("lisi").password("$2a$10$GStfEJEyoSHiSxnoP3SbD.R8XRowP1QKOdi.N6/iFEwEJWTQqlSba").roles("USER", "ADMIN").build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
System.out.println(bCryptPasswordEncoder.encode("123456"));
System.out.println(bCryptPasswordEncoder.encode("12345678"));
}
}
3. 配置授權伺服器
package com.cjs.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("my-client-1")
.secret("$2a$10$0jyHr4rGRdQw.X9mrLkVROdQI8.qnWJ1Sl8ly.yzK0bp06aaAkL9W")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris("http://www.baidu.com");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
}
public static void main(String[] args) {
System.out.println(new org.apache.tomcat.util.codec.binary.Base64().encodeAsString("my-client-1:12345678".getBytes()));
System.out.println(java.util.Base64.getEncoder().encodeToString("my-client-1:12345678".getBytes()));
}
}
這裡用戶端的secret是12345678,存儲的是加密後的值
4. 啟動類
package com.cjs.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CjsOauth2CodeServerApplication {
public static void main(String[] args) {
SpringApplication.run(CjsOauth2CodeServerApplication.class, args);
}
}
到目前為止,好像我們就做了兩件事情:一、配置使用者;二、注冊用戶端
現在,我們有兩個使用者(zhangsan和lisi)以及一個已注冊的用戶端(my-client-1)
接下來,用postman模拟用戶端請求擷取access_token
5. 協定端點
我們知道,在擷取access_token之前需要使用者授權,然後傳回一個code,最後用code換access_token
在這個過程中涉及到三個伺服器端點:授權端點、重定向端點、令牌端點
通過控制台看看啟動日志可能會加深了解:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.3.RELEASE)
2018-06-26 09:44:09.575 INFO 6528 --- [ main] c.c.e.CjsOauth2CodeServerApplication : Starting CjsOauth2CodeServerApplication on USER-20170302XK with PID 6528 (E:\cjsworkspace\cjs-oauth2-example\cjs-oauth2-code-server\target\classes started by Administrator in E:\cjsworkspace\cjs-oauth2-example\cjs-oauth2-code-server)
2018-06-26 09:44:09.597 INFO 6528 --- [ main] c.c.e.CjsOauth2CodeServerApplication : No active profile set, falling back to default profiles: default
2018-06-26 09:44:09.762 INFO 6528 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@22635ba0: startup date [Tue Jun 26 09:44:09 CST 2018]; root of context hierarchy
2018-06-26 09:44:12.931 INFO 6528 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2018-06-26 09:44:12.997 INFO 6528 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-06-26 09:44:12.998 INFO 6528 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.31
2018-06-26 09:44:13.200 INFO 6528 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-06-26 09:44:13.200 INFO 6528 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 3448 ms
2018-06-26 09:44:13.379 INFO 6528 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-26 09:44:13.379 INFO 6528 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-26 09:44:13.379 INFO 6528 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-26 09:44:13.379 INFO 6528 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-26 09:44:13.379 INFO 6528 --- [ost-startStop-1] .s.DelegatingFilterProxyRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*]
2018-06-26 09:44:13.380 INFO 6528 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/]
2018-06-26 09:44:13.529 INFO 6528 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/authorize]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
2018-06-26 09:44:13.530 INFO 6528 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/authorize],methods=[POST],params=[user_oauth_approval]}" onto public org.springframework.web.servlet.View org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny(java.util.Map<java.lang.String, java.lang.String>,java.util.Map<java.lang.String, ?>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
2018-06-26 09:44:13.530 INFO 6528 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
2018-06-26 09:44:13.531 INFO 6528 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
2018-06-26 09:44:13.531 INFO 6528 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/check_token]}" onto public java.util.Map<java.lang.String, ?> org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.checkToken(java.lang.String)
2018-06-26 09:44:13.531 INFO 6528 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/confirm_access]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map<java.lang.String, java.lang.Object>,javax.servlet.http.HttpServletRequest) throws java.lang.Exception
2018-06-26 09:44:13.531 INFO 6528 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/error]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)
2018-06-26 09:44:13.760 INFO 6528 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-26 09:44:14.019 INFO 6528 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@22635ba0: startup date [Tue Jun 26 09:44:09 CST 2018]; root of context hierarchy
2018-06-26 09:44:14.049 INFO 6528 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-26 09:44:14.049 INFO 6528 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-26 09:44:14.062 INFO 6528 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-26 09:44:14.063 INFO 6528 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-26 09:44:14.103 WARN 6528 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2018-06-26 09:44:14.865 INFO 6528 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-06-26 09:44:14.901 INFO 6528 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2018-06-26 09:44:14.904 INFO 6528 --- [ main] c.c.e.CjsOauth2CodeServerApplication : Started CjsOauth2CodeServerApplication in 6.298 seconds (JVM running for 8.287)
5.1. 授權端點
請求
參數名稱 | 描述 |
response_type | 必填。将其值設定為code表示如果成功的話将收到一個授權碼。 |
client_id | 必填。用戶端辨別。 |
redirect_uri | 可選。重定向URI雖然不是必須的,但是你的服務應該會需要它。而且,這個URL必須和授權服務端注冊的redirect_id一緻。 |
scope | 可選。請求可能會有一個或多個scope值。授權伺服器會把用戶端請求的範圍(scope)展示給使用者看。 |
state | 推薦。state參數用于應用存儲特定的請求資料的可以防止CSRF攻擊。授權伺服器必須原封不動地将這個值傳回給應用。 |
例如:
http://localhost:8080/oauth/authorize?response_type=code&client_id=my-client-1&redirect_uri=http://www.baidu.com&scope=all
響應
code | 授權碼,稍後用此授權碼交換通路令牌 |
請求時帶的state參數 |
https://www.baidu.com/?code=7Zudn6
5.2. 令牌端點
grant_type | 必填。參數值必須是"authorization_code" |
必填。之前收到的授權碼 | |
可能是必填的。如果授權請求的時候有redirect_uri,那麼token請求的時候也必須帶上這個參數,二者的值必須是一樣的。 | |
用戶端辨別,如果沒有其它的用戶端認證存在的話這個參數是必須的。 |
關于用戶端認證補充一點
![]()
OAuth 2.0 授權碼請求
授權伺服器可以通過HTTP Basic Auth方式對用戶端進行認證,也可以通過在請求中加一個client_secret參數來對用戶端進行認證。
不建議将用戶端的secret直接作為參數放到client_secret中,而且這種方式下client_id和client_secret都是必填參數。
特别注意,使用者(資源所有者)的使用者名和密碼跟用戶端的使用者名和密碼(client_id、client_secret)不是一套,它們是兩個東西。
那麼,通過HTTP Basic Auth如何對用戶端進行認證呢?用戶端需要怎樣傳參呢?
需要在請求header中設定Authorization,它的值是Basic + 空格 + Base64加密後的client_id:secret
至于為什麼,那是協定要求的,具體可以參考https://tools.ietf.org/html/rfc2617#page-5,格式如下:
![]()
OAuth 2.0 授權碼請求
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic bXktY2xpZW50LTE6MTIzNDU2Nzg=
cache-control: no-cache
Postman-Token: baf729b9-f030-41c1-b6fb-d740b0c5c573
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: localhost:8080
cookie: JSESSIONID=45EAF00C9829B9F569579DB08533D850; UISESSION=2A65FF6FC8C6BE415DE3C043E3EDAAA4
accept-encoding: gzip, deflate
content-length: 103
Connection: keep-alive
grant_type=authorization_code&code=7Zudn6&redirect_uri=http%3A%2F%2Fwww.baidu.com&client_id=my-client-1
6. 用戶端請求
直接在浏覽器中輸入以下位址,然後會跳到登入授權頁面
上面這兩個頁面都是預設自帶的
授權成功以後重定向
有了授權碼以後就可以換取access_token了
這個請求中唯一需要注意的一個參數就是Authorization,這是用于認證用戶端的,本例中它是這樣算出來的
public static void main(String[] args) {
System.out.println(new org.apache.tomcat.util.codec.binary.Base64().encodeAsString("my-client-1:12345678".getBytes()));
System.out.println(java.util.Base64.getEncoder().encodeToString("my-client-1:12345678".getBytes()));
}
是以,最終是Basic bXktY2xpZW50LTE6MTIzNDU2Nzg=
到此為止,我們隻是獲得了access_token,下一節将講解如何配置資源伺服器,以及用戶端通路受保護的資源。
另外,本例中關于令牌的存儲,以及用戶端注冊都是在記憶體中,實際生産過程中肯定是要存儲到資料庫中的,這一部分以後有時間再寫吧!