一个完整的授权码模式实例
写在前面
前面我们对OAuth2.0中四种授权模式进行了学习,接下来将通过一个完整的实例来研究授权码模式,深入理解其中的各个流程。
实例架构
授权码模式是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,涉及到资源所有者(用户)、客户端(第三方应用)、授权服务器和资源服务器这四个角色。由于用户就是笔者,因此无需提供项目实例,而其他三者这里就提供各自的项目实例,各实例项目名称、角色名称和端口如下表所示:
项目名称 | 角色名称 | 端口 |
---|---|---|
auth-server | 授权服务器 | 8080 |
user-server | 资源服务器 | 8081 |
client-app | 客户端(第三方应用) | 8082 |
空Maven父工程搭建
使用Maven新建一个空白的父工程,名称为authorization-code
,之后我们将在这个父工程中搭建子项目。
授权服务器搭建
在authorization-code
父工程中新建一个子模块,名称为auth-server
,在选择依赖的时候选择如下三个依赖:Web、Spring Cloud Security和Spring Cloud OAuth2依赖:
第一步,将父工程authorization-code
项目的pom.xml依赖文件修改为如下所示配置:
1 | <?xml version="1.0" encoding="UTF-8"?> |
第二步,回到子模块auth-server
中,在其项目目录下新建一个config包,之后在该类中新建一个SecurityConfig
类,注意这个类需要继承WebSecurityConfigurerAdapter
类,里面的代码如下所示:
1 | @Configuration |
注意由于本系列笔记主要是学习如何使用OAuth2.0,因此不会详细介绍Spring Security的相关知识。这里出于简单起见,并没有将用户存入数据库中,而是直接存在内存中。此处首先提供了一个密码加密PasswordEncoder类的实例,之后在其中定义了两个用户,并定义了他们的用户名、密码和对应角色。接着还使用了系统默认的登录表单,这样便于后续用户登录。举个例子,开发者想让微信登录第三方网站,那么这有一个前提就是得让用户先登录微信,而登录微信需要用户名和密码,因此此处配置的其实就是用户登录所需的信息。
第三步,在完成了用户的基本信息配置后,接下来开始配置授权服务器。在config包内新建一个AccessTokenConfig
类,其中的代码如下所示:
1 | @Configuration |
可以看到这里我们需要提供一个TokenStore
实例,该实例表示将生成的token存放在何处,可以将其存在Redis中,内存中,也可以将其存储在数据库中。其实这个TokenStore
是一个接口,它有很多实现类,如下所示:
出于简单考虑,这里依旧将其存入内存中,故选择使用InMemoryTokenStore
这一实现类。
接着在config包内新建一个AuthorizationServerConfig
类,注意这个类需要继承AuthorizationServerConfigurerAdapter
类,里面的代码如下所示:
1 | @EnableAuthorizationServer |
接下来对上述代码进行分析:
(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。你可能会有疑问,在前面学习四种授权模式的时候,这四种并不包括refresh_token
这种类型,的确,但是在实际开发过程中,我们都将其看做是其中的一种。
(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 |
以上就是授权服务器的搭建工作,那么接下来就启动该授权服务器。
资源服务器搭建
在完成了授权服务器的搭建工作之后,接下来开始搭建资源服务器。如果用户的项目属于中小型时,那么通常都会将资源服务器和授权服务器放在一起,但是如果是大型项目,那么都会将两者进行分离。因此本篇就假设用户正在开发的是大型项目,就将两者进行分离。
资源服务器,顾名思义就是用来存放用户的资源,这里就是用户的基本信息,如使用微信登录,那么就可能是头像、姓名、openid等信息。用户从授权服务器上获取到access_token
之后,接着就会通过access_token
去资源服务器上获取数据。
第一步,在authorization-code
父工程中新建一个子模块,名称为user-server
,在选择依赖的时候选择如下三个依赖:Web、Spring Cloud Security和Spring Cloud OAuth2依赖:
第二步,在父工程authorization-code
项目的pom.xml依赖文件中新增如下所示配置:
1 | <modules> |
第三步,回到子模块user-server
中,在其项目目录下新建一个config包,之后在该类中新建一个ResourceServerConfig
类,注意这个类需要继承ResourceServerConfigurerAdapter
类,里面的代码如下所示:
1 | @Configuration |
接下来对上述代码进行分析:
(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角色才能访问。
第四步,既然是资源服务器,那么我们就需要提供资源,这里提供两个接口。在项目目录下新建一个controller包,之后在该类中新建一个HelloController
类,里面的代码如下所示:
1 | @RestController |
第五步,在项目application.properties
配置文件中新增如下用于配置项目端口的代码:
1 | server.port=8081 |
以上就是资源服务器的搭建工作,那么接下来就启动该资源服务器。
客户端(第三方应用)搭建
请注意,客户端(第三方应用)并非必须的,开发者可以使用诸如Postman等测试工具来进行测试。此处为了案例的完整性,依旧搭建了一个普通的Spring Boot项目。
第一步,在authorization-code
父工程中新建一个子模块,名称为client-app
,在选择依赖的时候选择如下两个依赖:Web和Thymeleaf依赖:
第二步,在父工程authorization-code
项目的pom.xml依赖文件中新增如下所示配置:
1 | <modules> |
第三步,回到子模块client-app
中,在其resources/templates
目录下新建一个index.html
文件,其中的代码如下所示:
1 | <!DOCTYPE html> |
该页面的意思是点击超链接,就可以实现第三方登录,超链接中的参数如下所示:
(1)client_id
表示客户端id,这个需要开发者根据授权服务器中的实际配置来进行设置。
(2)response_type
表示响应类型,此处设置为code,表示响应一个授权码。
(3)redirect_uri
表示授权成功后的重定向地址,这里设置了跳转到第三方应用的首页。
(4)scop
表示授权的范围,此处值为all。
可以看到上述html页面中还存在一个h1标签,注意该标签中的数据来源于资源服务器。当授权服务器通过后,我们就可以拿着access_token
去资源服务器上请求数据,去加载资源,这样加载到的数据就会在h1标签中显示出来。
第四步,回到子模块client-app
中,在其项目目录下新建一个controller包,并在该包内新建一个HelloController
类,其中的代码如下所示:
1 | @Controller |
我们在此处的HelloController类中定义了一个hello方法,该方法用于访问/index.html
接口。首先判断code是否为空,不为空则说明是通过授权服务器重定向到这个地址,那么接下来我们做如下两个操作:
(1)根据拿到的authorization_code
去请求http://127.0.0.1:8080/oauth/token
地址,进而获取到Token,返回的数据如下所示:
1 | { |
其中的access_token
就是我们向资源服务器请求数据时所需要的令牌;refresh_token
则是后期我们刷新token所需要的令牌;expires_in
表示token剩余的有效期时间。
(2)拿着获取到的access_token
令牌,去资源服务器请求资源,注意access_token
是通过请求头来传递的,最后将从资源服务器中获取到的数据返回到Model中进行展示。
第五步,在项目application.properties
配置文件中新增如下用于配置项目端口的代码:
1 | server.port=8082 |
请注意,如果在上面提示缺少一个RestTemplate
对象,那么就需要开发者自行提供一个Bean方法用于返回一个RestTemplate
实例。以上就是客户端(第三方应用)的搭建工作,那么接下来就启动该客户端(第三方应用)。
项目测试
在确认三个项目都已经正确启动之后,接下来打开浏览器,访问http://127.0.0.1:8082/index.html
链接,此时页面显示如下信息:
接着点击第三方登录这一超链接,之后页面会跳转到授权服务器的默认登录页面:
然后用户输入在授权服务器中配置的用户信息,之后点击登录按钮进行登录,登录成功后的页面如下所示:
在上述页面中出现了一个提示,询问是否授权“envythink”这一用户去访问被保护的资源。我们点击Approve也就是批准,之后点击下方的Authorize按钮,此时页面就会自动跳转到第三方页面中:
注意看,可以发现此时的地址栏中多了一个code参数,这个code参数就是授权服务器给的授权码,之后拿着这个授权码就可以去请求access_token
,授权码使用一次就会失效。同时页面显示出一个admin信息,这个就是从资源服务器上获取到的/admin/hello
接口的信息。
当然由于我们在授权服务器中配置了两个用户,因此当大家使用hello/1234这个用户去登录时,该用户不具备admin角色,因此无法获取到/admin/hello
接口的信息,此时页面还会抛出如下所示错误:
这样就说明我们的授权码模式实例就配置成功了。
刷新token
接下来学习四种模式均具有的刷新token功能,这里笔者以授权码模式为例进行学习。
当开发者启动授权码模式下的授权服务器时,可以看到在Endpoints一栏中出现了很多接口,如下所示:
前面一些actuator都是对于项目的健康监测接口,而后面一些oauth接口才是我们需要注意的地方。下面通过一张表格来对oauth相关的接口进行介绍:
接口(Endpoints) | 含义 |
---|---|
/oauth/authorize |
授权的接口 |
/oauth/token |
用于获取令牌的接口 |
/oauth/confirm_access |
用户确认授权提交的接口(也就是auth-server 询问用户是否授权哪个页面的提交地址) |
/oauth/error |
授权出错的接口 |
/oauth/check_token |
校验access_token 的接口 |
/oauth/token_key |
提供公钥的接口 |
需要注意的是,这里的/oauth/token
接口除了用于获取令牌之外,还可以用来刷新令牌。在用户获取令牌的时候,除了用到access_token
之外,还有一个refresh_token
字段,该字段用于刷新令牌。也就是说在刷新令牌的时候,必须携带上refresh_token
字段,当令牌刷新完成之后,之前使用过的access_token
令牌就会失效。