写在前面

前面我们搭建了一个完整的授权码模式实例,接下来就尝试搭建另一个完整的模式实例—简化模式。考虑到后续学习的方便性,这里选择从零开始搭建,而非在此前授权码模式的基础上进行修改,这样能更加直观感受到简化模式和授权码模式的区别。注意所有加粗的地方均表示简化模式与授权码模式的区别,也就是不同之处。

实例架构

简化模式又称为隐藏式模式,当某些Web应用是纯前端应用没有后端时,就可以使用简化模式。简化模式允许授权服务器直接向前端颁发令牌,这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)。它的特点就是通过客户端的服务器与授权服务器进行交互,涉及到资源所有者(用户)、客户端(第三方应用)、授权服务器和资源服务器这四个角色。由于用户就是笔者,因此无需提供项目实例,而其他三者这里就提供各自的项目实例,各实例项目名称、角色名称和端口如下表所示:

项目名称 角色名称 端口
auth-server 授权服务器 8080
user-server 资源服务器 8081
client-app 客户端(第三方应用) 8082

空Maven父工程搭建

使用Maven新建一个空白的父工程,名称为implicit,之后我们将在这个父工程中搭建子项目。

授权服务器搭建

implicit父工程中新建一个子模块,名称为auth-server,在选择依赖的时候选择如下三个依赖:Web、Spring Cloud Security和Spring Cloud OAuth2依赖:

第一步,将父工程implicit项目的pom.xml依赖文件修改为如下所示配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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.envy</groupId>
<artifactId>implicit</artifactId>
<version>1.0-SNAPSHOT</version>
<name>implicit</name>
<description>OAuth2.0简化模式实例</description>
<packaging>pom</packaging>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<modules>
<module>auth-server</module>
</modules>
</project>

第二步,回到子模块auth-server中,在其项目目录下新建一个config包,之后在该类中新建一个SecurityConfig类,注意这个类需要继承WebSecurityConfigurerAdapter类,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(10);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("envy").password(new BCryptPasswordEncoder().encode("1234")).roles("admin")
.and()
.withUser("hello").password(new BCryptPasswordEncoder().encode("1234")).roles("user");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().formLogin();
}
}

注意由于本系列笔记主要是学习如何使用OAuth2.0,因此不会详细介绍Spring Security的相关知识。这里出于简单起见,并没有将用户存入数据库中,而是直接存在内存中。此处首先提供了一个密码加密PasswordEncoder类的实例,之后在其中定义了两个用户,并定义了他们的用户名、密码和对应角色。接着还使用了系统默认的登录表单,这样便于后续用户登录。举个例子,开发者想让微信登录第三方网站,那么这有一个前提就是得让用户先登录微信,而登录微信需要用户名和密码,因此此处配置的其实就是用户登录所需的信息。同时对其他页面访问我们没有进行权限控制。

第三步,在完成了用户的基本信息配置后,接下来开始配置授权服务器。在config包内新建一个AccessTokenConfig类,其中的代码如下所示:

1
2
3
4
5
6
7
@Configuration
public class AccessTokenConfig {
@Bean
TokenStore tokenStore(){
return new InMemoryTokenStore();
}
}

可以看到这里我们需要提供一个TokenStore实例,该实例表示将生成的token存放在何处,可以将其存在Redis中,内存中,也可以将其存储在数据库中。其实这个TokenStore是一个接口,它有很多实现类,如下所示:

出于简单考虑,这里依旧将其存入内存中,故选择使用InMemoryTokenStore这一实现类。

接着在config包内新建一个AuthorizationServerConfig类,注意这个类需要继承AuthorizationServerConfigurerAdapter类,里面的代码如下所示:

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
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;

@Bean
AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService);
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
services.setAccessTokenValiditySeconds(60*60*2);
services.setRefreshTokenValiditySeconds(60*60*24*3);
return services;
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("envythink").secret(new BCryptPasswordEncoder().encode("1234"))
.resourceIds("res1").authorizedGrantTypes("refresh_token","implicit")
.scopes("all").redirectUris("http://127.0.0.1:8082/index.html");
}

@Bean
AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}

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

接下来对上述代码进行分析:
(1)自定义AuthorizationServerConfig类,并继承AuthorizationServerConfigurerAdapter类,用来对授权服务器做更为详细的配置。请注意,此时需要在该类上添加@EnableAuthorizationServer注解,表示开启授权服务器的自动化配置。
(2)在自定义的AuthorizationServerConfig类中重写三个方法,这些方法分别用于对令牌端点安全、客户端信息、令牌访问端点和服务等内容进行详细配置。
(3)重写configure(AuthorizationServerSecurityConfigurer security)方法,该方法用于配置令牌端点的安全约束,即这个端点谁能访问,谁不能访问。接着我们调用checkTokenAccess()方法,里面设置值为permitAll(),表示该端点可以直接访问(后面当资源服务器收到token之后,需要校验token是否合法,此时就会访问这个端点)。查看源码可以知道该值默认为denyAll()表示都拒绝。
(4)重写configure(ClientDetailsServiceConfigurer clients)方法,该方法用于配置客户端的详细信息。在前面我们说过,授权服务器会进行两个方面的校验,一是校验客户端;而是校验用户。我们知道Spring Security与用户存储相关的类是UserDetailsService,由于我们将用户直接保持在内存中,因此系统默认就会通过这个类去校验用户。那么接下来就是校验客户端,需要注入ClientDetailsService对象。这里其实就是配置客户端的信息,同样出于简单考虑,这里依旧将其配置到内存中。此处配置了客户端的id、secret、资源id、授权类型、授权范围以及重定向URL。可以看到此处的authorizedGrantTypes参数的值已经变成了implicit

(5)重写configure(AuthorizationServerEndpointsConfigurer endpoints)方法,该方法用于配置令牌的访问端点和令牌服务。首先调用authorizationCodeServices方法来配置授权码的存储位置,由于我们是存储在内存中,因此需要提供一个返回AuthorizationCodeServices实例的方法,当然了更准确的说我们是返回一个InMemoryAuthorizationCodeServices对象。接着调用tokenServices()方法,来配置token的存储位置。授权码(authorization_code)和令牌(token)是有区别的,授权码用于获取令牌,使用一次就会失效;而令牌则用来获取资源。
(6)请注意,前面我们定义了tokenStore()方法,该方法仅仅是配置了token的存储位置,但是对于token并没有进行设置。接下来需要提供一个AuthorizationServerTokenServices实例,开发者可以在该实例中配置token的基本信息,如token是否支持刷新、token的存储位置、token的过期时间以及刷新token的有效期。所谓的“刷新token的有效期”是指当token快要过期的时候,我们肯定是需要获取一个新的token,而在获取新的token的时候,需要有一个凭证信息,注意这个凭证信息不是旧的token,而是另外一个refresh_token,而这个refresh_token也是有有效期的。

第四步,在项目application.properties配置文件中新增如下用于配置项目端口的代码:

1
server.port=8080

以上就是授权服务器的搭建工作,那么接下来就启动该授权服务器。

资源服务器搭建

由于简化模式不存在服务端,因此我们只能通过使用js来请求资源服务器上的数据,故资源服务器需要支持跨域。

第一步,在implicit父工程中新建一个子模块,名称为user-server,在选择依赖的时候选择如下三个依赖:Web、Spring Cloud Security和Spring Cloud OAuth2依赖:

第二步,在父工程implicit项目的pom.xml依赖文件中新增如下所示配置:

1
2
3
4
<modules>
<module>auth-server</module>
<module>user-server</module>
</modules>

第三步,既然是资源服务器,那么我们就需要提供资源,这里提供两个接口。回到子模块user-server中,在其项目目录下新建一个controller包,之后在该类中新建一个HelloController类,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@CrossOrigin(value = "*")
public class HelloController {

@GetMapping("/hello")
public String hello(){
return "hello";
}

@GetMapping("admin/hello")
public String admin(){
return "admin";
}
}

可以看到此处在HelloController类上添加了@CrossOrigin(value = "*")注解,表示在Spring Boot中对访问该类下所有接口都允许跨域,注意允许任何域访问。
第四步,其项目目录下新建一个config包,之后在该类中新建一个ResourceServerConfig类,注意这个类需要继承ResourceServerConfigurerAdapter类,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
RemoteTokenServices tokenServices(){
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://127.0.0.1:8080/oauth/check_token");
services.setClientId("envythink");
services.setClientSecret("1234");
return services;
}

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

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated()
.and().cors();
}
}

接下来对上述代码进行分析:
(1)自定义ResourceServerConfig类,并继承ResourceServerConfigurerAdapter类,用来对资源服务器做更为详细的配置。请注意,此时需要在该类上添加@EnableResourceServer注解,表示开启资源服务器的自动化配置。
(2)在自定义的ResourceServerConfig类中重写两个方法,这些方法分别用于对资源信息和页面访问等内容进行详细配置。
(3)重写configure(ResourceServerSecurityConfigurer resources)方法,该方法用于对资源进行配置。这里配置了资源的id,同时调用tokenServices()来配置token的存储位置。由于此处将资源服务器和授权服务器进行了分离,因此需要提供一个RemoteTokenServices对象,言外之意就是当资源服务器和授权服务器放在一起时,就不必提供一个RemoteTokenServices对象。
(4)我们定义了一个tokenServices()方法,该方法用于返回(3)中所需要的RemoteTokenServices对象,在RemoteTokenServices对象中,我们配置了access_token的的校验地址、客户端id,客户端秘钥等,其实这就是授权服务器的地址信息。这样当用户来资源服务器请求资源时,会携带一个access_token,通过此处的配置,它就能检验这个token是否正确。
(5)重写configure(HttpSecurity http)方法,该方法用于对访问页面进行配置。其实就是对资源进行拦截,这里就是判断当用户访问URL是以/admin开头的时候,需要用户具备admin角色才能访问。同时添加了.cors()选项用于开始Spring Security对于跨域的支持。(此处的SpringSecurity跨域配置是在Spring Boot支持跨域的基础上进行的。)
第五步,在项目application.properties配置文件中新增如下用于配置项目端口的代码:

1
server.port=8081

以上就是资源服务器的搭建工作,那么接下来就启动该资源服务器。

客户端(第三方应用)搭建

请注意,客户端(第三方应用)并非必须的,开发者可以使用诸如Postman等测试工具来进行测试。此处为了案例的完整性,依旧搭建了一个普通的Spring Boot项目。

前面也说了由于简化模式不存在服务端,因此第三方应用就只能通过使用js来请求资源服务器上的数据。

第一步,在implicit父工程中新建一个子模块,名称为client-app,在选择依赖的时候选择Web依赖:

第二步,在父工程implicit项目的pom.xml依赖文件中新增如下所示配置:

1
2
3
4
5
<modules>
<module>auth-server</module>
<module>user-server</module>
<module>client-app</module>
</modules>

第三步,回到子模块client-app中,在其resources/static目录下新建一个index.html文件,其中的代码如下所示:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script src="/jquery-3.5.0.min.js"></script>
</head>
<body>
<h1>欢迎来到OAth2.0 简化模式实例</h1>
<a href="http://127.0.0.1:8080/oauth/authorize?client_id=envythink&response_type=token&scope=all&redirect_uri=http://127.0.0.1:8082/index.html">第三方登录(简化模式)</a>
<div id="app"></div>
<script>
var hash = window.location.hash;//提取出参数,类似这种格式#access_token=b1740984-3d4a-4b64-8509-9891a4d6a582&token_type=bearer&expires_in=7199
if(hash && hash.length>0){
var params = hash.substring(1).split("&");//#access_token=b1740984-3d4a-4b64-8509-9891a4d6a582
var token = params[0].split("=");//[access_token,b1740984-3d4a-4b64-8509-9891a4d6a582]
$.ajax({
type: 'get',
headers: {
'Authorization': 'Bearer'+ token[1]
},
url: 'http://127.0.0.1:8081/admin/hello',
success: function (msg){
$("#app").html(msg)
}
})
}
</script>
</body>
</html>

请注意resources/static目录下的文件是不需要经过控制器就能访问的。接下来解释该页面代码的含义:
(1)client_id表示客户端id,这个需要开发者根据授权服务器中的实际配置来进行设置。
(2)response_type表示响应类型,此处设置为token,表示响应一个令牌。
(3)redirect_uri表示授权成功后的重定向地址,这里设置了跳转到第三方应用的首页。
(4)scope表示授权的范围,此处值为all。
可以看到上述html页面中还存在一个id为app的标签,注意该标签中的数据来源于资源服务器。当授权服务器通过后,我们就可以通过ajax拿着access_token去资源服务器上请求数据,去加载资源,这样加载到的数据就会在id为app的标签中显示出来。

也就是说当用户登录成功后,会自动重定向到http://127.0.0.1:8082/index.html页面,并在该链接中添加一个锚点参数,类似于如下所示的URL:

1
http://127.0.0.1:8082/index.html#access_token=b1740984-3d4a-4b64-8509-9891a4d6a582&token_type=bearer&expires_in=7199

因此我们需要使用js来提取出#后面的参数,并进一步解析出access_token参数的值。之后我们将access_token参数放在请求头中,并发起一个ajax请求,并将获取到的数据在id为app的标签中显示出来。

第四步,在项目application.properties配置文件中新增如下用于配置项目端口的代码:

1
server.port=8082

以上就是客户端(第三方应用)的搭建工作,那么接下来就启动该客户端(第三方应用)。

项目测试

在确认三个项目都已经正确启动之后,接下来打开浏览器,访问http://127.0.0.1:8082/index.html链接,此时页面显示如下信息:

接着点击第三方登录这一超链接,之后页面会跳转到授权服务器的默认登录页面:

然后用户输入在授权服务器中配置的用户信息,之后点击登录按钮进行登录,登录成功后的页面如下所示:

在上述页面中出现了一个提示,询问是否授权“envythink”这一用户去访问被保护的资源。我们点击Approve也就是批准,之后点击下方的Authorize按钮,此时页面就会自动跳转到第三方页面中:

注意看,可以发现此时的地址栏中多了锚点参数,里面包含了access_token,之后就携带这个access_token参数向资源服务器发起ajax请求来获取/admin/hello接口的信息,页面就会显示出一个admin信息。

(完)