在文章 OAuth 2.0 概念及授权流程梳理 中我们谈到OAuth 2.0的概念与流程,这里我准备分别记一记这几种授权模式的demo,一方面为自己的最近的学习做个总结,另一方面做下知识输出,如果文中有错误的地方,请评论指正,在此不胜感激。
阅读本文,默认读者已经过Spring Security有一定的了解,对OAuth2.0流程有一定了解。
带领读者对Spring Security OAuth2.0框架的授权码模式有一个比较直观的概念,能使用框架搭建授权码模式授权服务器与资源服务器(前后端分离版本)
授权码模式要求:用户登录并对第三方应用(客户端)进行授权,出示授权码交给客户端,客户端凭授权码换取access_token(访问凭证)
此模式要求授权服务器与用户直接交互,在此过程中,第三方应用是无法获取到用户输入的密码等信息的,这个模式也是OAuth 2.0中最安全的一个
这里主要关注security-authorization-server
与security-resource-server
这两个模块
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.1.18.RELEASEspring-boot.version>
<spring-cloud.version>Greenwich.SR6spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.security.oauthgroupId>
<artifactId>spring-security-oauth2artifactId>
<version>2.3.6.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.security.oauth.bootgroupId>
<artifactId>spring-security-oauth2-autoconfigureartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
文中服务器均使用demo级别配置,请勿直接使用到生产环境
授权服务器结构主体:
WebSecurityConfiguration:
package com.authorization.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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Collections;
/**
* @description:
* @Author C_Y_J
* @create 2022/1/7 14:53
**/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 密码编码解码器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加一个用户、保存在内存中
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("123456"))
.authorities(Collections.emptyList());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有请求都需要通过认证
http.authorizeRequests()
.anyRequest().authenticated();
// 允许Basic登录
http.httpBasic();
// 关跨域保护
http.csrf().disable();
}
}
继承WebSecurityConfigurerAdapter
的方法,实现个性化配置,这里我们使用内存保存一个名为user
、密码为123456
的用户,与授权服务器交互的用户就是他了。
除了配置用户,我们需要对服务的资源进行保护,这里将所有的请求都要求通过认证才可以访问,用户登录需要使用httpBasic形式(就是那种网页弹个窗要求登录的那种)。
Spring Security 5.x版本后,要求显示声明使用的密码器,就是PasswordEncoder
了,常用BCryptPasswordEncoder
,简单的可以认为它是使用时间戳和盐进行加密的一种算法,同一个密码被加密后也不会相同。
AuthorizationServerConfiguration:
package com.authorization.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.AuthorizationServerSecurityConfigurer;
/**
* @description: @EnableAuthorizationServer 实现授权服务器
* @Author C_Y_J
* @create 2022/1/7 11:35
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
// 配置内存中
.inMemory()
// 该客户端id
.withClient("client_1")
// 该客户端密钥
.secret(new BCryptPasswordEncoder().encode("client_1_secret"))
// 该客户端token有效时间 秒
.accessTokenValiditySeconds(3600)
// 该客户端支持的模式
.authorizedGrantTypes("refresh_token", "password", "authorization_code")
// 该客户端允许的授权范围配置
.scopes("userInfo", "server", "all")
// 该客户端资源列表
.resourceIds("resource")
// false 允许跳转到授权页面
.autoApprove(false)
// 该客户端验证回调地址
.redirectUris("http://www.baidu.com");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单提交
security.allowFormAuthenticationForClients();
// 校验token的条件是已经通过身份认证的
security.checkTokenAccess("isAuthenticated()");
}
}
通过@Configuration
和EnableAuthorizationServer
开启授权服务器配置,通过重写AuthorizationServerConfigurerAdapter
的方法来完成自定义授权服务器。
OAuth2授权码模式中,要求不仅仅用户需要登录,还要求客户端也需要登录,这里就需要在configure(ClientDetailsServiceConfigurer clients)
这个方法中配置客户端(第三方应用)的登录信息:
withClient
中配置的是客户端id(client_id)。"authorization_code"
(授权码模式),这里是可以同时支持多种授权模式的。redirectUris校验是否同一个客户端这个,可能说的不是很准确,说下大体流程,我们在授权服务器上配置了这个回调地址,授权服务器在用户授权成功后,返回授权码的地址就是它,另外我们后续申请token时,也需要传递这个回调地址,所以我的理解是校验是否是同一客户端发来的第二次请求(换token时)。
configure(AuthorizationServerSecurityConfigurer security)
这里配置资源客户端(第三方应用)的表单提交权限,类似Spring Security配置的permitAll()
等权限控制标识,如果不配置,客户端将无法换取token。
application.yml:
这里我只配置了
# 服务器端口
server:
port: 6600
资源服务器结构主体:
ResourceServerConfiguration:
package com.resource.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
/**
* @description: @EnableResourceServer 实现资源服务器
* @Author C_Y_J
* @create 2022/1/7 11:33
**/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
// 针对所有的请求
.authorizeRequests()
// 匹配规则:/user/{id} 不需要登录认证
.antMatchers("/user/info/{id}").permitAll()
// 匹配规则:任何请求 需要登录认证
.anyRequest().authenticated();
//允许表单登录
http.formLogin();
// 开启httpBasic认证
http.httpBasic();
// 关闭csrf防护
http.csrf().disable();
}
@Primary
@Bean
public RemoteTokenServices remoteTokenServices() {
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 设置授权服务器check_token端点完整地址
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:6600/oauth/check_token");
// 设置客户端id与secret,注意:client_secret值不能使用passwordEncoder加密!
remoteTokenServices.setClientId("client_1");
remoteTokenServices.setClientSecret("client_1_secret");
return remoteTokenServices;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
// 设置授权服务器的资源列表
resources.resourceId("resource");
}
}
通过@Configuration
和@EnableResourceServer
这两个注解标识服务是一个资源服务器,重写ResourceServerConfigurerAdapter
来实现自定义授权服务器
配置configure(HttpSecurity http)
方法,这里可以代替Spring Security同名方法配置,开启所有请求需要授权才可访问
配置资源相关设置configure(ResourceServerSecurityConfigurer resources)
,这里只设置resourceId
后续的使用redis校验token也在这里设置
校验token的配置,这里使用了远程调用授权服务器帮忙校验token的方式,只需要显示注入RemoteTokenServices remoteTokenServices()
的Bean,就可以调用授权服务器的/oauth/check_token端点,设置客户端配置的值,详见注释
SysUserController:
package com.resource.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description:
* @Author C_Y_J
* @create 2021/12/10 10:49
**/
@RestController
@RequestMapping("/user")
public class SysUserController {
/**
* 通过ID查询用户信息
*
* @param id ID
* @return 用户信息
*/
@GetMapping("/info/{id}")
public String user(@PathVariable Integer id) {
return ("通过" + id + "查询用户信息");
}
/**
* 用户认证信息
*/
@GetMapping(value = "/authentication")
public String info() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else {
username = principal.toString();
}
return username;
}
}
application.yml:
# 服务器端口
server:
port: 7700
获取授权码的流程,一般是由客户端使用自己的client_id+client_secret+response_type=code拼接url,让浏览器跳转完成的,用户的登录与授权过程都需要在浏览器中完成,启动项目后访问下列url
localhost:6600/oauth/authorize?client_id=client_1&client_secret=client_1_secret&response_type=code
登录用户/密码: user/123456,选择Approve表示接受授权,Deny反之,如下动图所示:
最后我们得到了回调地址 www.baidu.com/?code=0XyNpB
这里的code就是授权码,接下来我们使用授权码进行换取token
POST请求,localhost:6600/oauth/token,参数如图:
BasicAuth:这里填的是客户端配置的client_id和client_secret的值,配置后会在Header中添加Authorization:Basic Y2xpZW50XzE6Y2xpZW50XzFfc2VjcmV0,
Basic空格后的是
client_id:client_secret`具体值被Base64后得到的值
请求参数列表:
最后我们获得了授权服务的响应,包含token的json。
code:tbW5VJ
scope:userInfo
redirect_uri:http://www.baidu.com
grant_type:authorization_code
{
"access_token": "2910bbb5-7f99-46ff-97cd-9b6b63124cfc",
"token_type": "bearer",
"refresh_token": "bda3de27-8b28-4278-beeb-ccb3a894e891",
"expires_in": 3599,
"scope": "userInfo"
}
127.0.0.1:7700/user/info/12
127.0.0.1:7700/user/authentication
复制之前获取到的token,添加token访问接口。
Bearer Token相当于在Headers中添加Authorization:Bearer空格access_token
。
授权码模式就先在这里告一段落,写的比较基础,自认为该说到的点都说到了,后续还会写其它模式的文章,如文中有何遗漏,请不吝评论反馈,本人会尽快改正,谢谢!