阅读完需:约 16 分钟
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动。
它的步骤如下: (下面的代码例子和这个流程是一样的)
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
完整例子:
auth-server:
user-server:
client-app:
我们常见的 OAuth2 授权码模式登录中,涉及到的各个角色,我都会自己提供,自己测试,这样可以最大限度的让小伙伴们了解到 OAuth2 的工作原理。
1、授权服务器搭建
首先我们搭建一个名为 auth-server 的授权服务
项目创建完成后,首先提供一个 Spring Security 的基本配置:
SecurityConfig:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//密码编码器
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存身份验证中
auth.inMemoryAuthentication()
.withUser("sang").password(passwordEncoder().encode("123"))
.roles("admin")
.and()
.withUser("xjh").password(passwordEncoder().encode("123"))
.roles("user");
}
//配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().formLogin();
}
/*
* 认证管理器
* */
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
在这段代码中,为了代码简洁,我就不把 Spring Security 用户存到数据库中去了,直接存在内存中。这里我创建了一个名为 sang 的用户,密码是 123,角色是 admin。同时我还配置了一个表单登录。这段配置的目的,实际上就是配置用户。例如你想用微信登录第三方网站,在这个过程中,你得先登录微信,登录微信就要你的用户名/密码信息,那么我们在这里配置的,其实就是用户的用户名/密码/角色信息。
基本的用户信息配置完成后, 接下来我们来配置授权服务器:
AccessTokenConfig
@Configuration
public class AccessTokenConfig {
@Bean
TokenStore tokenStore(){
//在内存中存储令牌
return new InMemoryTokenStore();
}
}
AuthorizationServerConfig:
/**
* @author Lenovo
* 授权服务器配置器适配器
*/
@Configuration
@EnableAuthorizationServer //启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Autowired
PasswordEncoder passwordEncoder;
/*
* 认证管理器 为密码模式准备的
*/
@Autowired
AuthenticationManager authenticationManager;
/**
*客户详情服务
*/
@Autowired
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*7);
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("xjh")
//密码
.secret(passwordEncoder.encode("123"))
//资源编号
.resourceIds("res1")
//授权的赠款类型 authorization_code 授权码模式,refresh_token 刷新Token , implicit 简化模式 , password 密码模式 , 需要哪个模式就配置哪个模式
.authorizedGrantTypes("authorization_code","refresh_token","implicit","password","client_credentials")
//范围
.scopes("all")
//自动批准
.autoApprove(true)
//重定向Uris
.redirectUris("http://localhost:8082/index.html","http://localhost:8082/password.html");
}
//授权服务器端点配置器
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints //认证管理器 为密码模式准备的
.authenticationManager(authenticationManager)
//授权码服务
.authorizationCodeServices(authorizationCodeServices())
.tokenServices(tokenServices());
}
//授权码服务
AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
}
这段代码有点长,我来给大家挨个解释:
- 1. 首先我们提供了一个 TokenStore 的实例,这个是指你生成的 Token 要往哪里存储,我们可以存在 Redis 中,也可以存在内存中,也可以结合 JWT 等等,这里,我们就先把它存在内存中,所以提供一个 InMemoryTokenStore 的实例即可。
- 2. 接下来我们创建 AuthorizationServer 类继承自 AuthorizationServerConfigurerAdapter,来对授权服务器做进一步的详细配置,AuthorizationServer 类记得加上@EnableAuthorizationServer 注解,表示开启授权服务器的自动化配置。
- 3. 在 AuthorizationServer 类中,我们其实主要重写三个 configure 方法。
- 4. AuthorizationServerSecurityConfigurer 用来配置令牌端点的安全约束,也就是这个端点谁能访问,谁不能访问。checkTokenAccess 是指一个 Token 校验的端点,这个端点我们设置为可以直接访问(在后面,当资源服务器收到 Token 之后,需要去校验 Token 的合法性,就会访问这个端点)。
- 5. ClientDetailsServiceConfigurer 用来配置客户端的详细信息,授权服务器要做两方面的检验,一方面是校验客户端,另一方面则是校验用户,校验用户,我们前面已经配置了,这里就是配置校验客户端。客户端的信息我们可以存在数据库中,这其实也是比较容易的,和用户信息存到数据库中类似,但是这里为了简化代码,我还是将客户端信息存在内存中,这里我们分别配置了客户端的 id,secret、资源 id、授权类型、授权范围以及重定向 uri。授权类型一共讲了四种,四种之中不包含 refresh_token 这种类型,但是在实际操作中,refresh_token 也被算作一种。
- 6. AuthorizationServerEndpointsConfigurer 这里用来配置令牌的访问端点和令牌服务。authorizationCodeServices用来配置授权码的存储,这里我们是存在在内存中,tokenServices 用来配置令牌的存储,即 access_token 的存储位置,这里我们也先存储在内存中。授权码和令牌有什么区别?授权码是用来获取令牌的,使用一次就失效,令牌则是用来获取资 源
- 7. tokenServices 这个 Bean 主要用来配置 Token 的一些基本信息,例如 Token 是否支持刷新、Token 的存储位置、Token 的有效期以及刷新 Token 的有效期等等。Token 有效期这个好理解,刷新 Token 的有效期我说一下,当 Token 快要过期的时候,我们需要获取一个新的 Token,在获取新的 Token 时候,需要有一个凭证信息,这个凭证信息不是旧的 Token,而是另外一个refresh_token,这个 refresh_token 也是有有效期的。
2、资源服务器搭建
接下来我们搭建一个资源服务器。大家网上看到的例子,资源服务器大多都是和授权服务器放在一起的,如果项目比较小的话,这样做是没问题的,但是如果是一个大项目,这种做法就不合适了。资源服务器就是用来存放用户的资源,例如你在微信上的图像、openid 等信息,用户从授权服务器上拿到 access_token 之后,接下来就可以通过 access_token 来资源服务器请求数据。我们创建一个新的 Spring Boot 项目,叫做 user-server ,作为我们的资源服务器,创建时,添加如下依赖:
项目创建成功之后,添加如下配置:
ResourceServerConfig :
/**
* @author Lenovo
* 资源服务器配置适配器
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
RemoteTokenServices tokenServices(){
//远程令牌服务
RemoteTokenServices services=new RemoteTokenServices();
//设置检查令牌端点网址
services.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
//设定客户编号
services.setClientId("xjh");
//设置客户机密
services.setClientSecret("123");
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. tokenServices 我们配置了一个 RemoteTokenServices 的实例,这是因为资源服务器和授权服务器是分开的,资源服务器和授权服务器是放在一起的,就不需要配置 RemoteTokenServices 了。
- 2. RemoteTokenServices 中我们配置了 access_token 的校验地址、client_id、client_secret 这三个信息,当用户来资源服务器请求资源时,会携带上一个 access_token,通过这里的配置,就能够校验出 token 是否正确等。
- 3. 最后配置一下资源的拦截规则,这就是 Spring Security 中的基本写法,我就不再赘述。接下来我们再来配置两个测试接口:
HelloController:
@RestController
@CrossOrigin(value = "*")
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello";
}
@GetMapping("/admin/hello")
public String admin(){
return "admin";
}
}
如此之后,我们的资源服务器就算配置成功了。
3、 第三方应用搭建
第三方应用就是一个普通的 Spring Boot 工程,创建时加入 Thymeleaf 依赖和 Web 依赖:
在 resources/templates 目录下,创建 index.html :
index.html :
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="http://localhost:8080/oauth/authorize?client_id=xjh&response_type=code&scope=all
&redirect_uri=http://localhost:8082/index.html">
第三方登录
</a>
<h1 th:text="${msg}"></h1>
</body>
</html>
这是一段 Thymeleaf 模版,点击超链接就可以实现第三方登录,超链接的参数如下:
- client_id 客户端 ID,根据我们在授权服务器中的实际配置填写。
- response_type 表示响应类型,这里是 code 表示响应一个授权码。
- redirect_uri 表示授权成功后的重定向地址,这里表示回到第三方应用的首页。
- scope 表示授权范围。
h1 标签中的数据是来自资源服务器的,当授权服务器通过后,我们拿着 access_token 去资源服务器加载数据,加载到的数据就在 h1 标签中显示出来。
接下来我们来定义一个 HelloController:
HelloController :
@Controller
public class HelloController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/index.html")
public String index(String code , Model model){
if(code!=null){
System.out.println(code);
//一个key对应多个value,通常我们会将多个value放到一个集合中
MultiValueMap<String,String> map=new LinkedMultiValueMap();
map.add("code",code);
map.add("client_id","xjh");
map.add("client_secret","123");
map.add("redirect_uri","http://localhost:8082/index.html");
map.add("grant_type","authorization_code");
Map<String,String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
System.out.println("resp="+resp);
HttpHeaders headers=new HttpHeaders();
headers.add("Authorization","Bearer"+resp.get("access_token"));
HttpEntity<?> httpEntity=new HttpEntity<>(headers);
ResponseEntity<String> responseEntity = restTemplate.exchange("http://localhost:8081/hello", HttpMethod.GET, httpEntity, String.class);
model.addAttribute("msg",responseEntity.getBody());
}
return "index";
}
}
在这个 HelloController 中,我们定义出 /index.html 的地址。
- code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
- state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
- grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code“。
- code:表示上一步获得的授权码,必选项。
- redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。(必须和一开始发送给认证服务器的地址一样)
- client_id:表示客户端ID,必选项。
如果 code 不为 null,也就是如果是通过授权服务器重定向到这个地址来的,那么我们做如下两个操作:
1. 根据拿到的 code,去请求http://localhost:8080/oauth/token地址去获取 Token,返回的数据结构如下:
{
"access_token": "e7f223c4-7543-43c0-b5a6-5011743b5af4",
"token_type": "bearer",
"refresh_token": "aafc167b-a112-456e-bbd8-58cb56d915dd",
"expires_in": 7199,
"scope": "all"
}
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
- scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
access_token 就是我们请求数据所需要的令牌,refresh_token 则是我们刷新 token 所需要的令牌,expires_in 表示 token 有效期还剩多久。
2. 接下来,根据我们拿到的 access_token,去请求资源服务器,注意 access_token 通过请求头传递,最后将资源服务器返回的数据放到 model 中。
这里我只是举一个简单的例子,目的是和把这个流程走通,正常来说,access_token 我们可能需要一个定时任务去维护,不用每次请求页面都去获取,定期去获取最新的 access_token 即可。
5、测试
首先我们去访问http://localhost:8082/index.html页面,结果如下:
然后我们点击第三方登录这个超链接,点完之后,会进入到授权服务器的默认登录页面:
接下来我们输入在授权服务器中配置的用户信息来登录,登录成功后,会看到如下页面:
在这个页面中,我们可以看到一个提示,询问是否授权 xjh 这个用户去访问被保护的资源,我们选择 approve(批准),然后点击下方的 Authorize 按钮,点完之后,页面会自动跳转回我的第三方应用中:
这个时候地址栏多了一个 code 参数,这就是授权服务器给出的授权码,拿着这个授权码,我们就可以去请求 access_token,授权码使用一次就会失效。同时大家注意到页面多了一个 Hello,这个 Hello 就是从资源服务器请求到的数据。