Spring Security OAuth2.0
概述
介绍在 Spring Security 如何实现 OAuth2.0 实现 授权 的功能。
另外,阮一峰提供了几篇关于 OAuth2.0 非常不错的文章,推荐去从瞅瞅。
OAuth2.0 是什么?
FROM 《维基百科 —— 开放授权》
OAuth(Open Authorization)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
- 旁白君:很多团队,内部会采用 OAuth2.0 实现一个 授权 服务,避免每个上层应用或者服务重复开发。
OAuth 允许用户提供一个 令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。
每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问 特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
- 旁白君:如果胖友对接过 微信网页授权 功能,就会发现分成两种方式:静默 授权、手动 授权。前者只能获取到用户的 openid,而后者可以获取到用户的 基本信息。
OAuth2.0 是用于授权的 行业标准协议。OAuth2.0 为简化客户端开发提供了特定的授权流,包括 Web 应用、桌面应用、移动端应用等。
- 旁白君:OAuth 1.0 协议体系本身存在一些问题,现已被各大开发平台逐渐废弃。
OAuth2.0 角色解释
- 在 OAuth2.0 中,有如下角色:
- Authorization Server:认证 服务器,用于认证用户。如果客户端认证通过,则发放访问资源服务器的 令牌。
- Resource Server:资源 服务器,拥有受保护资源。如果请求包含正确的 访问令牌,则可以访问资源。
- 友情提示:提供管理后台、客户端 API 的服务,都可以认为是 Resource Server。
- Client:客户端。它请求资源服务器时,会带上 访问令牌,从而成功访问资源。
- 友情提示:Client 可以是浏览器、客户端,也可以是内部服务。
- Resource Owner:资源拥有者。最终用户,他有访问资源的 账号 与 密码。
- 友情提示:可以简单把 Resource Owner 理解成人,她在使用 Client 访问资源。
OAuth 2.0 运行流程
如下是 OAuth 2.0 的 授权码模式 的运行流程:
- (A)用户打开客户端以后,客户端要求用户给予授权。
- (B)用户同意给予客户端授权。
- (C)客户端使用上一步获得的授权,向认证服务器申请令牌。
- (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
- (E)客户端使用令牌,向资源服务器申请获取资源。
- (F)资源服务器确认令牌无误,同意向客户端开放资源。
上述的六个步骤,B 是关键,即用户如何给客户端进行 授权。有了授权之,客户端就可以获取 令牌,进而凭令牌获取 资源。
友情提示:如果有对接过三方开放平台,例如说微信、QQ、微博等三方登录,就会很容易理解这个步骤过程。这个时候的资源,资源主要指的是三方开放平台的用户资料等等。
OAuth 2.0 授权模式
客户端必须得到用户的授权(Authorization Grant),才能获得访问令牌(Access Token)。
OAuth2.0 定义了四种授权方式:
授权码模式(Authorization Code)
密码模式(Resource Owner Password Credentials)
简化模式(Implicit)
客户端模式(Client Credentials)
其中,密码模式 和 授权码模式 比较常用。至于如何选择,后续慢慢细品。
-
当然,对于 黄框 部分,对于笔者还是比较困惑的。笔者认为,第三方的单页应用 SPA ,也是适合采用 Authorization Code Grant 授权模式的。例如,《微信网页授权》 :
具体而言,网页授权流程分为四步:
- 1、引导用户进入授权页面同意授权,获取 code
- 2、通过 code 换取网页授权 access_token(与基础支持中的 access_toke n 不同)
- 3、如果需要,开发者可以刷新网页授权 access_token,避免过期
- 4、通过网页授权 access_token 和 openid 获取用户基本信息(支持 UnionID 机制)
所以猜测,之所以图中画的是 Implicit Grant 的原因是,受 Google 的 《OAuth 2.0 for Client-side Web Applications》 一文中,推荐使用了 Implicit Grant 。
当然,具体使用 Implicit Grant 还是 Authorization Code Grant 授权模式,没有定论。笔者,偏向于使用 Authorization Code Grant,对于第三方客户端的场景。
密码模式
学习 密码模式(Resource Owner Password Credentials Grant),用户向客户端提供自己的 用户名和密码。客户端使用这些信息,向 授权服务器 索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
旁白君:如果客户端和授权服务器都是自己公司的,显然符合。
- (A)用户向客户端提供用户名和密码。
- (B)客户端将 用户名和密码 发给授权服务器,向后者 请求令牌。
- (C)授权服务器确认无误后,向客户端提供访问令牌。
使用示例
- 我们来新建两个项目,搭建一个密码模式的示例
- authorization-server-with-resource-owner-password-credentials:授权服务器。
- resource-server:资源服务器。
搭建授权服务器
- 创建 authorization-server-with-resource-owner-password-credentials 项目,搭建授权服务器。
引入依赖
创建 pom.xml 文件,引入 Spring Security OAuth 依赖。
添加 spring-security-oauth2-autoconfigure 依赖,引入 Spring Security OAuth 并实现自动配置。同时,它也引入了 Spring Security 依赖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<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">
<parent>
<artifactId>oauth</artifactId>
<groupId>pers.fulsun.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>authorization-server-with-resource-owner-password-credentials</artifactId>
<description>授权服务器</description>
<properties>
<!-- 依赖相关配置 -->
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<!-- 插件相关配置 -->
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 实现对 Spring MVC 的自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对 Spring Security OAuth2 的自动配置 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>
</project>
SecurityConfig
创建 SecurityConfig 配置类,提供一个账号密码为「admin/admin」的用户。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44/**
* 创建 SecurityConfig 配置类
*/
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份验证管理器
*
*/
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
public AdminUserDetailsService userDetailsService() {
return new AdminUserDetailsService();
}
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()// 关闭 csrf
.authorizeRequests()
.antMatchers("/oauth/**", "/login/**", "/logout/**")
.permitAll() // 放行
.anyRequest()
.authenticated() // 需要认证
.and()
.formLogin()
.permitAll();
}
}
AuthorizationServerConfig
创建 AuthorizationServerConfig 配置类,进行 授权 服务器。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66/**
* 进行授权服务器的配置。
*/
// 声明开启 OAuth 授权服务器的功能。
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager;
UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
/**
* 配置使用 AuthenticationManager 实现用户认证的功能 对应在 {@link SecurityConfig#authenticationManagerBean()}
* 配置使用 userDetailsService 获取用户信息 对应在 {@link SecurityConfig#userDetailsService()}
*
* @param endpoints
* @throws Exception
*/
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
/**
* 设置 /oauth/check_token 端点,通过认证后可访问。
* 这里的认证,指的是使用 client-id + client-secret 进行的客户端认证,不要和用户认证混淆。
*
* @param oauthServer
* @throws Exception
*/
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// Spring Security OAuth2 会公开了两个端点,用于检查令牌(/oauth/check_token 和/oauth/token_key),
// 这些端点默认受保护 denyAll()。tokenKeyAccess()和 checkTokenAccess()方法会打开这些端点以供使用。
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()") //isAuthenticated 授权
.allowFormAuthenticationForClients();
}
/**
* 进行 Client 客户端的配置。
*
* @param clients
* @throws Exception
*/
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 设置使用基于内存的 Client 存储器。实际情况下,最好放入数据库中,方便管理。
clients.inMemory()
// 创建一个 Client 配置。
// Client 账号、密码
.withClient("clientapp").secret(passwordEncoder.encode("112233"))
// 密码模式
.authorizedGrantTypes("password")
// 可授权的 Scope
.scopes("read_userinfo", "read_contacts")
// 如果要继续添加另外的 Client 配置,可以使用 #and() 方法继续拼接。
// .and().withClient()
;
}
}在类上添加
@EnableAuthorizationServer
注解,声明开启 OAuth 授权 服务器的功能。同时,继承AuthorizationServerConfigurerAdapter
类,进行 OAuth 授权 服务器的配置。#configure(AuthorizationServerEndpointsConfigurer endpoints)
方法,配置使用的 AuthenticationManager 实现 用户认证 的功能。其中,authenticationManager
是由 SecurityConfig 创建,Spring Security 的配置类。#configure(AuthorizationServerSecurityConfigurer oauthServer)
方法,设置/oauth/check_token
端点,通过认证后可访问。其中,
/oauth/check_token
端点对应 CheckTokenEndpoint 类,用于校验访问令牌的有效性。在客户端访问资源服务器时,会在请求中带上 访问令牌。
在资源服务器收到客户端的请求时,会使用请求中的 访问令牌,找授权服务器确认该 访问令牌 的有效性。
为什么要创建 Client 的
client-id
和client-secret
呢?- 通过
client-id
编号和client-secret
,授权服务器可以知道调用的来源以及正确性。这样,即使“坏人”拿到 Access Token ,但是没有client-id
编号和client-secret
,也不能和授权服务器发生 有效 的交互。
- 通过
AuthServerApplication
创建 AuthorizationServerApplication 类,授权服务器的启动类。代码如下:
1
2
3
4
5
6
7
8
public class AuthorizationServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthorizationServerApplication.class, args);
}
}
简单测试
执行 AuthorizationServerApplication 启动授权服务器。下面,我们使用 Postman 模拟一个 Client。
POST
请求 http://localhost: 8080/oauth/token 地址,使用密码模式进行 授权。如下图所示:请求说明:
通过 Basic Auth 的方式,填写
client-id
+client-secret
作为用户名与密码,实现 Client 客户端有效性的认证。请求参数
grant_type
为"password"
,表示使用 密码模式。请求参数
username
和password
,表示 用户 的用户名与密码。
响应说明:
- 响应字段
access_token
为 访问令牌,后续客户端在访问资源服务器时,通过它作为身份的标识。 - 响应字段
token_type
为 令牌类型,一般是bearer
或是mac
类型。 - 响应字段
expires_in
为访问令牌的 过期时间,单位为秒。 - 响应字段
scope
为 权限范围。
- 响应字段
POST
请求 http://localhost: 8080/oauth/check_token 地址,校验访问令牌的有效性。如下图所示:
搭建资源服务器
- 创建 resource-server 项目,搭建资源服务器。
引入依赖
创建 pom.xml 文件,引入 Spring Security OAuth 依赖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<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">
<parent>
<artifactId>oauth</artifactId>
<groupId>pers.fulsun.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>resource-server</artifactId>
<description>资源服务器</description>
<properties>
<!-- 依赖相关配置 -->
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<!-- 插件相关配置 -->
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 实现对 Spring MVC 的自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对 Spring Security OAuth2 的自动配置 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>
</project>
配置文件
创建 application.yml 配置文件,添加 Spring Security OAuth 相关配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14server:
port: 9090
security:
oauth2:
# OAuth2 Client 配置,对应 OAuth2ClientProperties 类
client:
client-id: clientapp
client-secret: 112233
# OAuth2 Resource 配置,对应 ResourceServerProperties 类
resource:
token-info-uri: http://127.0.0.1:8080/oauth/check_token # 获得 Token 信息的 URL
# 访问令牌获取 URL,自定义的
access-token-uri: http://127.0.0.1:8080/oauth/token
security.oauth2.client
配置项,OAuth2 Client 配置,对应 OAuth2ClientProperties 类。在这个配置项中,我们添加了客户端的client-id
和client-secret
。为什么要添加这个配置项呢?因为资源服务器会调用授权服务器的
/oauth/check_token
接口,而考虑到安全性,我们配置了该接口需要进过 客户端认证。友情提示:其实 单独 给资源服务器配置一个 Client 的
client-id
和client-secret
。我们可以把资源服务器理解成授权服务器的 一个特殊的客户端。
security.oauth2.resource
配置项,OAuth2 Resource 配置,对应 ResourceServerProperties 类。- 这里,我们通过
token-info-uri
配置项,设置使用授权服务器的/oauth/check_token
接口,校验访问令牌的有效性。
- 这里,我们通过
security.oauth2.access-token-uri
配置项,是 我们自定义 的,设置授权服务器的oauth/token
接口,获取访问令牌。因为稍后我们将在 LoginController 中,实现一个/login
登录接口。
ResourceServerConfig
创建 OAuth2ResourceServerConfig 类,进行 资源 服务器。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 设置 /login 无需权限访问
.antMatchers("/login").permitAll()
// 设置其它请求,需要认证后访问
.anyRequest().authenticated()
;
}
}
- 在类上添加
@EnableResourceServer
注解,声明开启 OAuth 资源 服务器的功能。同时,继承 ResourceServerConfigurerAdapter 类,进行 OAuth 资源服务器 的配置。 #configure(HttpSecurity http)
方法,设置 HTTP 权限。这里,我们设置/login
接口 无需 权限访问,其它接口 认证 后可访问, 客户端在访问资源服务器时,其请求中的 访问令牌 会被 资源 服务器调用 授权 服务器的/oauth/check_token
接口,进行校验访问令牌的正确性。
ExampleController
创建 ExampleController 类,提供
/api/example/hello
接口,表示一个资源。代码如下:1
2
3
4
5
6
7
8
9
10
public class ExampleController {
public String hello() {
return "world";
}
}
ResourceServerApplication
创建 ResourceServerApplication 类,资源服务器的启动类。代码如下:
1
class ResourceServerApplication { public static void main(String[] args) { SpringApplication.run(ResourceServerApplication.class, args); }}
测试
执行 ResourceServerApplication 启动资源服务器。
首先,请求
127.0.0.1:9090/api/example/hello
接口,不带 访问令牌,则请求会被 拦截。如下图所示:然后,请求
127.0.0.1:9090/api/example/hello
接口,带上 错误 的访问令牌,则请求会被 拦截。如下图所示:友情提示:访问令牌需要在请求头
"Authorization"
上设置,并且以"Bearer "
开头。
最后,请求
127.0.0.1:9090/api/example/hello
接口,带上 正确 的访问令牌,则请求会被 通过。如下图所示:
LoginController
创建 LoginController 类,提供
/login
登录接口。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42package pers.fulsun.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
public class LoginController {
private OAuth2ClientProperties oauth2ClientProperties;
private String accessTokenUri;
public OAuth2AccessToken login( String username,
{ String password)
// <1> 创建 ResourceOwnerPasswordResourceDetails 对象
ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
resourceDetails.setAccessTokenUri(accessTokenUri);
resourceDetails.setClientId(oauth2ClientProperties.getClientId());
resourceDetails.setClientSecret(oauth2ClientProperties.getClientSecret());
resourceDetails.setUsername(username);
resourceDetails.setPassword(password);
// <2> 创建 OAuth2RestTemplate 对象
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails);
restTemplate.setAccessTokenProvider(new ResourceOwnerPasswordAccessTokenProvider());
// <3> 获取访问令牌
return restTemplate.getAccessToken();
}
}在 /login 接口中,资源服务器扮演的是一个 OAuth 客户端的角色,调用授权服务器的 /oauth/token 接口,使用密码模式进行授权,获得访问令牌。
<1> 处,创建 ResourceOwnerPasswordResourceDetails 对象,填写密码模式授权需要的请求参数。
<2> 处,创建 OAuth2RestTemplate 对象,它是 Spring Security OAuth 封装的工具类,用于请求授权服务器。同时,将 ResourceOwnerPasswordAccessTokenProvider 设置到其中(友情提示:这一步非常重要),表示使用密码模式授权。
<3> 处,调用 OAuth2RestTemplate 的 #getAccessToken() 方法,调用授权服务器的 /oauth/token 接口,进行密码模式的授权。
注意,OAuth2RestTemplate 是有状态的工具类,所以需要每次都重新创建。
测试 2
- 重新执行 ResourceServerApplication 启动资源服务器。下面,我们来进行
/login
接口的测试。
首先,请求 http://127.0.0.1: 9090/login 接口,使用 用户 的 用户名 与 密码 进行登录,获得访问令牌。如下图所示:
响应结果和授权服务器的
/oauth/token
接口是一致的,因为就是调用它,嘿嘿~
然后,请求 <127.0.0.1:9090/api/example/hello> 接口,带 刚刚的 访问令牌,则请求会被通过。如下图所示:
授权码模式
授权码模式,是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的 后台 服务器,与 授权 务器进行互动。
旁白君:一般情况下,在有客户端 的情况下,我们与第三方平台常常采用这种方式。
(A)用户访问客户端,后者将前者跳转到到 授权 服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,授权服务器将跳转到客户端事先指定的”重定向 URI”(Redirection URI),同时附上一个 授权码。
(D)客户端收到授权码,附上早先的”重定向 URI”,向认证服务器申请 令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了 授权码 和 重定向 URI,确认无误后,向客户端发送 访问令牌。
搭建授权服务器
复制出 authorization-server-with-resource-owner-password-credentials 项目,修改 搭建授权服务器。
仅仅需要修改 OAuth2AuthorizationServerConfig 类,设置使用
"authorization_code"
授权码模式,并设置回调地址。1
/** * 进行 Client 客户端的配置。 * * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 设置使用基于内存的 Client 存储器。实际情况下,最好放入数据库中,方便管理。 clients.inMemory() // 创建一个 Client 配置。 // Client 账号、密码 .withClient("clientapp").secret(passwordEncoder.encode("112233"))- .authorizedGrantTypes("password") // 密码模式+ .authorizedGrantTypes("authorization_code") // 授权码模式+ .redirectUris("http://www.baidu.com") // 可授权的 Scope .scopes("read_userinfo", "read_contacts") // 如果要继续添加另外的 Client 配置,可以使用 #and() 方法继续拼接。 // .and().withClient() ; }
测试
执行 AuthorizationServerApplication 启动授权服务器。
使用 浏览器,访问 http://127.0.0.1: 8080/oauth/authorize?client_id = clientapp&redirect_uri = http://www.baidu.com&response_type = code&scope = read_userinfo 地址,获取 授权。请求参数说明如下:
- 友情提示:
/oauth/authorize
对应 AuthorizationEndpoint 端点。 client_id
参数,必传,为我们在 OAuth2AuthorizationServer 中配置的 Client 的编号。redirect_uri
参数,可选,回调地址。当然,如果client_id
对应的 Client 未配置redirectUris
属性,会报错。response_type
参数,必传,返回结果为code
授权码。scope
参数,可选,申请授权的 Scope 。如果多个,使用逗号分隔。state
参数,可选,表示客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值。友情提示:state
参数,未在上述 URL 中体现出来。
- 友情提示:
因为我们并未 登录 授权服务器,所以被拦截跳转到登录界面。如下图所示:
输入用户的账号密码「admin/admin」进行登录。登录完成后,进入授权界面。如下图所示:
旁白君:和我们日常使用的腾讯 QQ、微信、微博等等三方登录,是一模一样的,除了丑了点,嘿嘿~
选择
scope.read_userinfo
为 Approve 允许,点击「Authorize」按钮,完成 授权 操作。浏览器自动重定向到 Redirection URI 地址,并且在 URI 上可以看到code
授权码。如下图所示:
使用 Postman 模拟请求
我们先使用 Postman 模拟请求 http://localhost: 8080/oauth/token 地址,使用 授权码 获取到 访问令牌。如下图所示:
请求说明:
通过 Basic Auth 的方式,填写
client-id
+client-secret
作为用户名与密码,实现 Client 客户端有效性的认证。请求参数
grant_type
为"authorization_code"
,表示使用 授权码模式。请求参数
code
,从授权服务器获取到的 授权码。请求参数
redirect_uri
,Client 客户端的 Redirection URI 地址。
注意,授权码 仅能使用一次,重复请求会报
Invalid authorization code:
错误。如下图所示:
搭建资源服务器
复用 resource-server 项目,主要是提供回调地址。
新建 CallbackController 类,提供
/callback
回调地址。需要注意的是 设置请求授权服务器需要的
code
和redirect_uri
参数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44package pers.fulsun.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
public class CallbackController {
private OAuth2ClientProperties oauth2ClientProperties;
private String accessTokenUri;
/**
* 提供 /callback 回调地址,在获取到授权码时,请求授权服务器,通过授权码获取访问令牌。
*/
public OAuth2AccessToken login( { String code)
// 创建 AuthorizationCodeResourceDetails 对象
AuthorizationCodeResourceDetails resourceDetails = new AuthorizationCodeResourceDetails();
resourceDetails.setAccessTokenUri(accessTokenUri);
resourceDetails.setClientId(oauth2ClientProperties.getClientId());
resourceDetails.setClientSecret(oauth2ClientProperties.getClientSecret());
// 创建 OAuth2RestTemplate 对象
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails);
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().setAuthorizationCode(code); // 设置 code
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().setPreservedState("http://127.0.0.1:9090/callback"); // 通过这个方式,设置 redirect_uri 参数
restTemplate.setAccessTokenProvider(new AuthorizationCodeAccessTokenProvider());
// 获取访问令牌
return restTemplate.getAccessToken();
}
}在 OAuth2ResourceServerConfig 配置类中,设置
/callback
回调地址无需权限验证,不然回调都跳转不过来哈。1
//声明开启 OAuth 资源服务器的功能 public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { // 设置 /login 接口无需权限访问,其它接口认证后可访问。 http.authorizeRequests() // 设置 /login 无需权限访问 .antMatchers("/login").permitAll() // 设置 /callback 回调地址无需权限验证 .antMatchers("/callback").permitAll() // 设置其它请求,需要认证后访问 .anyRequest().authenticated(); }}
简单测试
修改授权服务器的 clientDetail 的跳转路径为
http://127.0.0.1:9090/callback
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 设置使用基于内存的 Client 存储器。实际情况下,最好放入数据库中,方便管理。
clients.inMemory()
// 创建一个 Client 配置。
// Client 账号、密码
.withClient("clientapp").secret(passwordEncoder.encode("112233"))
// .authorizedGrantTypes("password") // 密码模式
.authorizedGrantTypes("authorization_code") // 授权码模式
.redirectUris("http://127.0.0.1:9090/callback")
// 可授权的 Scope
.scopes("read_userinfo", "read_contacts")
// 如果要继续添加另外的 Client 配置,可以使用 #and() 方法继续拼接。
// .and().withClient()
;
}执行 ResourceServerApplication 启动资源服务器。
访问 http://127.0.0.1: 8080/oauth/authorize?client_id = clientapp&redirect_uri = http://127.0.0.1: 9090/callback&response_type = code&scope = read_userinfo,成功授权后获取到 访问令牌。如下图所示:
简化模式
简化模式,不通过第三方应用程序的服务器,直接在浏览器中向 授权 服务器申请令牌,跳过 了“授权码”这个步骤,因此得名。
所有步骤在浏览器中完成,令牌对访问者是 可见 的,且客户端不需要授权。
- (A)用户访问客户端,后者将前者跳转到到 授权 服务器。
- (B)用户选择是否给予客户端授权。
- (C)假设用户给予授权,授权服务器将用户导向客户端指定的”重定向 URI”,并在 URI 的 Hash 部分 包含了 访问令牌。
- (D)浏览器向资源服务器发出请求,其中不包括上一步收到的 Hash 值。
- (E)资源服务器返回一个网页,其中包含的代码可以获取 Hash 值中的令牌。
- (F)浏览器执行上一步获得的脚本,提取出令牌。
- (G)浏览器将令牌发给客户端。
搭建授权服务器
复制出
authorization-server-with-implicit
项目,修改 搭建授权服务器。仅仅需要修改 OAuth2AuthorizationServerConfig 类,设置使用
"implicit"
简化模式,并设置回调地址。- 注意,这里设置的回调地址,稍后我们会在搭建资源服务器 中实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 设置使用基于内存的 Client 存储器。实际情况下,最好放入数据库中,方便管理。
clients.inMemory()
// 创建一个 Client 配置。
// Client 账号、密码
.withClient("clientapp").secret(passwordEncoder.encode("112233"))
// .authorizedGrantTypes("password") // 密码模式
// .authorizedGrantTypes("authorization_code") // 授权码模式
+ .authorizedGrantTypes("implicit") // 简化模式
+ .redirectUris("http://127.0.0.1:9090/callback2")
// 可授权的 Scope
.scopes("read_userinfo", "read_contacts")
// 如果要继续添加另外的 Client 配置,可以使用 #and() 方法继续拼接。
// .and().withClient()
;
}
搭建资源服务器
复用 resource-server 项目,主要是提供回调地址。
新建 Callback2Controller 类,提供
/callback2
回调地址。代码如下:1
2
3
4
5
6
7
8
9
10
public class Callback2Controller {
public String login() {
return "假装这里有一个页面,嘿嘿。";
}
}在 OAuth2ResourceServerConfig 配置类中,设置
/callback02
回调地址无需权限验证,不然回调都跳转不过来哈。1
2
3
4
5
6
7
8
9
10
11
12
13@Override
public void configure(HttpSecurity http) throws Exception {
// 设置 /login 接口无需权限访问,其它接口认证后可访问。
http.authorizeRequests()
// 设置 /login 无需权限访问
.antMatchers("/login").permitAll()
// 设置 /callback回调地址无需权限验证
.antMatchers("/callback").permitAll()
+ .antMatchers("/callback2").permitAll()
// 设置其它请求,需要认证后访问
.anyRequest().authenticated();
}
简单测试
执行 AuthorizationServerApplication 启动授权服务器。
执行 ResourceServerApplication 启动资源服务器。
使用 浏览器,访问 http://127.0.0.1: 8080/oauth/authorize?client_id = clientapp&redirect_uri = http://127.0.0.1: 9090/callback2&response_type = token&scope = read_userinfo 地址,获取 授权。请求参数说明如下:
client_id
参数,必传,为我们在 OAuth2AuthorizationServer 中配置的 Client 的编号。redirect_uri
参数,可选,回调地址。当然,如果client_id
对应的 Client 未配置redirectUris
属性,会报错。response_type
参数,必传,返回结果为token
访问令牌。scope
参数,可选,申请授权的 Scope 。如果多个,使用逗号分隔。- state` 参数,可选,表示客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值。
因为我们并未 登录 授权服务器,所以被拦截跳转到登录界面。登录完成后,进入授权界面。如下图所示:
选择
scope.read_userinfo
为 Approve 允许,点击「Authorize」按钮,完成 授权 操作。浏览器自动重定向到 Redirection URI 地址,并且在 URI 上的 Hash 部分 可以看到access_token
访问令牌。如下图所示:
客户端模式
客户端模式,指客户端以自己的名义,而不是以用户的名义,向授权服务器进行认证。
严格地说,客户端模式并不属于 OAuth 框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求授权服务器提供服务,其实不存在授权问题。
旁白君:我们对接微信公众号时,就采用的客户端模式。我们的后端服务器就扮演“客户端”的角色,与微信公众号的后端服务器进行交互。
- (A)客户端向授权服务器进行身份认证,并要求一个 访问令牌。
- (B)授权服务器确认无误后,向客户端提供访问令牌。
搭建授权服务器
复制出 authorization-server-with-client-credentials 项目,修改 搭建授权服务器。
删除 SecurityConfig 配置类,因为客户端模式下,无需 Spring Security 提供用户的认证功能。
但是,Spring Security OAuth 需要一个 PasswordEncoder Bean,否则会报错,因此我们在 OAuth2AuthorizationServerConfig 类的
#passwordEncoder()
方法进行创建。修改 OAuth2AuthorizationServerConfig 类,设置使用
"client_credentials"
客户端模式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 进行 Client 客户端的配置。
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 设置使用基于内存的 Client 存储器。实际情况下,最好放入数据库中,方便管理。
clients.inMemory()
// 创建一个 Client 配置。
// Client 账号、密码
.withClient("clientapp").secret(passwordEncoder.encode("112233"))
// .authorizedGrantTypes("password") // 密码模式
// .authorizedGrantTypes("authorization_code") // 授权码模式
// .authorizedGrantTypes("implicit") // 简化模式
+ .authorizedGrantTypes("client_credentials") // 客户端模式
// 可授权的 Scope
.scopes("read_userinfo", "read_contacts")
// 如果要继续添加另外的 Client 配置,可以使用 #and() 方法继续拼接。
// .and().withClient()
;
}
简单测试
执行 AuthorizationServerApplication 启动授权服务器。下面,我们使用 Postman 模拟一个 Client。
POST
请求 http://localhost: 8080/oauth/token 地址,使用客户端模式进行 授权。如下图所示:请求说明:
通过 Basic Auth 的方式,填写
client-id
+client-secret
作为用户名与密码,实现 Client 客户端有效性的认证。请求参数
grant_type
为"client_credentials"
,表示使用 客户端模式。响应就是访问令牌
搭建资源服务器
复用 resource-server 项目
新建 ClientLoginController 类,提供
/client-login
接口,实现调用 授权 服务器,进行 客户端 模式的授权,获得访问令牌。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42package pers.fulsun.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
public class ClientLoginController {
private OAuth2ClientProperties oauth2ClientProperties;
private String accessTokenUri;
/**
* 使用 OAuth2RestTemplate 进行请求授权服务器
* @return
*/
public OAuth2AccessToken login() {
// 创建 ClientCredentialsResourceDetails 对象
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setAccessTokenUri(accessTokenUri);
resourceDetails.setClientId(oauth2ClientProperties.getClientId());
resourceDetails.setClientSecret(oauth2ClientProperties.getClientSecret());
// 创建 OAuth2RestTemplate 对象
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails);
restTemplate.setAccessTokenProvider(new ClientCredentialsAccessTokenProvider());
// 获取访问令牌
return restTemplate.getAccessToken();
}
}在 OAuth2ResourceServerConfig 配置类中,设置
/client-login
接口无需权限验证,不然无法调用。
1 |
|
简单测试
执行 ResourceServerApplication 启动资源服务器。
请求 http://127.0.0.1: 9090/client-login 接口,使用 客户端模式 进行授权,获得访问令牌。如下图所示:
使用授权服务器获得的 访问令牌,请求 <127.0.0.1:9090/api/example/hello> 接口时 带上,则请求会被 通过。如下图所示:
刷新令牌
在 OAuth2.0 中,一共有 两类 令牌:
- 访问 令牌(Access Token)
- 刷新 令牌(Refresh Token)
在 访问 令牌过期时,我们可以使用 刷新 令牌向 授权 服务器获取一个 新 的访问令牌。
可能会有疑惑,为什么会有 刷新 令牌呢?
- 每次请求资源服务器时,都会在请求上带上 访问 令牌,这样它的泄露风险是 相对 高的。因此,出于 安全性 的考虑,访问令牌的过期时间 比较短,刷新令牌的过期时间 比较长。
- 这样,如果访问令牌即使被盗用走,那么在一定的时间后,访问令牌也能在较短的时间内过期。当然,安全也是相对的,如果使用刷新令牌后,获取到新的访问令牌,访问令牌 后续 又 可能 被盗用。
大家常用开放平台的令牌过期时间,让大家更好的理解:
开放平台 Access Token 有效期 Refresh Token 有效期 微信开放平台 2 小时 未知 腾讯开放平台 90 天 未知 小米开放平台 90 天 10 年
授权 服务器
- 复制 passwordd 方式,创建 authorization-server-with-refresh-credentials 项目,搭建 提供访问令牌 的 授权 服务器。改动点如下图所示:
在 OAuth2AuthorizationServerConfig 的
#configure(ClientDetailsServiceConfigurer clients)
方法中,在配置的 Client 的授权模式中,额外新增"refresh_token"
刷新令牌。通过
#accessTokenValiditySeconds(int accessTokenValiditySeconds)
方法,设置 访问 令牌的有效期。通过
#refreshTokenValiditySeconds(int refreshTokenValiditySeconds)
方法,设置 刷新 令牌的有效期。1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 设置使用基于内存的 Client 存储器。实际情况下,最好放入数据库中,方便管理。
clients.inMemory()
// 创建一个 Client 配置。
// Client 账号、密码
.withClient("clientapp").secret(passwordEncoder.encode("112233"))
.authorizedGrantTypes("refresh_token", "password") // 密码模式
// 可授权的 Scope
.scopes("read_userinfo", "read_contacts")
.accessTokenValiditySeconds(3600) // 访问令牌有效期 3600 s = 1 小时
.refreshTokenValiditySeconds(864000) // 访问令牌有效期 864000 s = 10 天
;
}
在 OAuth2AuthorizationServerConfig 的
#configure(AuthorizationServerEndpointsConfigurer endpoints)
方法中,设置使用的userDetailsService
用户详情 Service。- 而该
userDetailsService
是在 SecurityConfig 的#userDetailsServiceBean()
方法创建的 UserDetailsService Bean。 - 友情提示:如果不进行 UserDetailsService 的设置,在使用 刷新 令牌获取新的 访问 令牌时,会抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43/**
* 进行授权服务器的配置。
*/
// 声明开启 OAuth 授权服务器的功能。
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager;
UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
/**
* 配置使用 AuthenticationManager 实现用户认证的功能 对应在 {@link SecurityConfig#authenticationManagerBean()}
* 配置使用 userDetailsService 获取用户信息 对应在 {@link SecurityConfig#userDetailsService()}
*
* @param endpoints
* @throws Exception
*/
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
endpoints.userDetailsService(userDetailsService);
}
/**
* 设置 /oauth/check_token 端点,通过认证后可访问。
* 这里的认证,指的是使用 client-id + client-secret 进行的客户端认证,不要和用户认证混淆。
*
* @param oauthServer
* @throws Exception
*/
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// Spring Security OAuth2 会公开了两个端点,用于检查令牌(/oauth/check_token 和/oauth/token_key),
// 这些端点默认受保护 denyAll()。tokenKeyAccess()和 checkTokenAccess()方法会打开这些端点以供使用。
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}- 而该
简单测试
执行 AuthorizationServerApplication 启动授权服务器。下面,我们使用 Postman 模拟一个 Client。
POST
请求 http://localhost: 8080/oauth/token 地址,使用 密码 模式进行 授权。如下图所示:额外多返回了
refresh_token
刷新令牌,校验token有效性。POST
请求 http://localhost: 8080/oauth/token 地址,使用 刷新令牌 模式进行 授权。如下图所示:- 请求说明:
- 通过 Basic Auth 的方式,填写
client-id
+client-secret
作为用户名与密码,实现 Client 客户端有效性的认证。 - 请求参数
grant_type
为"refresh_token"
,表示使用刷新令牌模式。 - 请求参数
refresh_token
,表示刷新令牌。
- 通过 Basic Auth 的方式,填写
- 请求说明:
在响应中,返回了新的
access_token
访问令牌。注意,老的access_token
访问令牌会失效,无法继续使用。
删除令牌
在用户登出系统时,我们会有删除令牌的需求。虽然说,可以通过客户端本地删除令牌的方式实现。但是,考虑到真正的彻底的实现删除令牌,必然服务端自身需要删除令牌。
友情提示:客户端本地删除令牌的方式实现,指的是清楚本地 Cookie、localStorage 的令牌缓存。
在 Spring Security OAuth2 中,并没有提供内置的接口,所以需要自己去实现。
授权服务器
直接在授权服务器 authorization-server-with-resource-owner-password-credentials 项目上修改。
创建TokenDemoController. 接入删除令牌的功能。
提供
/token/demo/revoke
接口,调用 ConsumerTokenServices 的#revokeToken(String tokenValue)
方法,删除访问令牌和刷新令牌。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
public class TokenDemoController {
private ConsumerTokenServices tokenServices;
public boolean revokeToken( { String token)
return tokenServices.revokeToken(token);
}
}在 SecurityConfig 配置类,设置
/token/demo/revoke
接口无需授权,方便测试。代码如下:1
2
3
4
5
6
7
8
9
10
11// SecurityConfig.java
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// 设置 /token/demo/revoke 无需授权
.mvcMatchers("/token/demo/revoke").permitAll()
// 设置其它接口需要授权
.anyRequest().authenticated();
}
简单测试
执行 AuthorizationServerApplication 启动授权服务器。下面,我们使用 Postman 模拟一个 Client。
POST
请求 http://localhost:8080/oauth/token 地址,使用密码模式进行授权。如下图所示:请求 http://localhost:8080/token/demo/revoke 地址,删除令牌。如下图所示:
删除成功,调用授权服务器的
oauth/check_token
接口,测试访问令牌是否已经被删除。