Spring Security如何优雅的增加OAuth2协议授权模式

一、什么是OAuth2协议?
OAuth 2.0 是一个关于授权的开放的网络协议,是目前最流行的授权机制。
数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
由于授权的场景众多,OAuth 2.0 协议定义了获取令牌的四种授权方式,分别是:
授权码模式:授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
简化模式:简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
密码模式:密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
客户端模式:客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
四种授权模式分别使用不同的
grant_type来区分
二、为什么要自定义授权类型?
虽然 OAuth2 协议定义了4种标准的授权模式,但是在实际开发过程中还是远远满足不了各种变态的业务场景,需要我们去扩展。
例如增加图形验证码、手机验证码、手机号密码登录等等的场景
而常见的做法都是通过增加 过滤器Filter 的方式来扩展 Spring Security 授权,但是这样的实现方式有两个问题:
- 脱离了
OAuth2的管理 - 不灵活:例如系统使用 密码模式 授权,网页版需要增加图形验证码校验,但是手机端APP又不需要的情况下,使用增加
Filter的方式去实现就比较麻烦了。
所以目前在 Spring Security 中比较优雅和灵活的扩展方式就是通过自定义 grant_type 来增加授权模式。
三、实现思路
在扩展之前首先需要先了解 Spring Security 的整个授权流程,我以 密码模式 为例去展开分析,如下图所示

3.1. 流程分析
整个授权流程关键点分为以下两个部分:
第一部分:关于授权类型 grant_type 的解析
- 每种
grant_type都会有一个对应的TokenGranter实现类。 - 所有
TokenGranter实现类都通过CompositeTokenGranter中的tokenGranters集合存起来。 - 然后通过判断
grantType参数来定位具体使用那个TokenGranter实现类来处理授权。
第二部分:关于授权登录逻辑
- 每种
授权方式都会有一个对应的AuthenticationProvider实现类来实现。 - 所有
AuthenticationProvider实现类都通过ProviderManager中的providers集合存起来。 TokenGranter类会 new 一个AuthenticationToken实现类,如UsernamePasswordAuthenticationToken传给ProviderManager类。- 而
ProviderManager则通过AuthenticationToken来判断具体使用那个AuthenticationProvider实现类来处理授权。 - 具体的登录逻辑由
AuthenticationProvider实现类来实现,如DaoAuthenticationProvider。
3.2. 扩展分析
根据上面的流程,扩展分为以下两种场景
场景一:只对原有的授权逻辑进行增强或者扩展,如:用户名密码登录前增加图形验证码校验。
该场景需要定义一个新的 grantType 类型,并新增对应的 TokenGranter 实现类 添加扩展内容,然后加到 CompositeTokenGranter 中的 tokenGranters 集合里即可。
场景二:新加一种授权方式,如:手机号加密码登录。
该场景需要实现以下内容:
- 定义一个新的
grantType类型,并新增对应的TokenGranter实现类添加到CompositeTokenGranter中的tokenGranters集合里 - 新增一个
AuthenticationToken实现类,用于存放该授权所需的信息。 - 新增一个
AuthenticationProvider实现类 实现授权的逻辑,并重写supports方法绑定步骤二的AuthenticationToken实现类
四、代码实现
下面以 场景二 新增手机号加密码授权方式为例,展示核心的代码实现
4.1. 创建 AuthenticationToken 实现类
创建 MobileAuthenticationToken 类,用于存储手机号和密码信息
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
public MobileAuthenticationToken(String mobile, String password) {
super(null);
this.principal = mobile;
this.credentials = password;
setAuthenticated(false);
}
public MobileAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
4.2. 创建 AuthenticationProvider 实现类
创建 MobileAuthenticationProvider 类,实现登录逻辑,并绑定 MobileAuthenticationToken 类
@Setter
public class MobileAuthenticationProvider implements AuthenticationProvider {
private ZltUserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) {
MobileAuthenticationToken authenticationToken = (MobileAuthenticationToken) authentication;
String mobile = (String) authenticationToken.getPrincipal();
String password = (String) authenticationToken.getCredentials();
UserDetails user = userDetailsService.loadUserByMobile(mobile);
if (user == null) {
throw new InternalAuthenticationServiceException("手机号或密码错误");
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("手机号或密码错误");
}
MobileAuthenticationToken authenticationResult = new MobileAuthenticationToken(user, password, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
}
4.3. 创建 TokenGranter 实现类
创建 MobilePwdGranter 类并定义 grant_type 的值为 mobile_password
public class MobilePwdGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "mobile_password";
private final AuthenticationManager authenticationManager;
public MobilePwdGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices
, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
String mobile = parameters.get("mobile");
String password = parameters.get("password");
parameters.remove("password");
Authentication userAuth = new MobileAuthenticationToken(mobile, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
userAuth = authenticationManager.authenticate(userAuth);
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate mobile: " + mobile);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
4.4. 加到 CompositeTokenGranter 中的集合里
// 添加手机号加密码授权模式
tokenGranters.add(new MobilePwdGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
4.5. 测试
使用以下地址,指定 grant_type 为 mobile_password 进行授权获取 access_token
/oauth/token?grant_type=mobile_password&mobile={mobile}&password={password}

五、参考样例
详细的代码实现可以参考
https://gitee.com/zlt2000/microservices-platform/tree/master/zlt-uaa
扫码关注有惊喜!

Spring Security如何优雅的增加OAuth2协议授权模式的更多相关文章
- Spring Security技术栈开发企业级认证与授权(一)环境搭建
本项目是基于慕课网的Spring Security技术栈开发企业级认证与授权,采用IDEA开发,本文章用来记录该项目的学习过程. 慕课网视频:https://coding.imooc.com/clas ...
- 转 - spring security oauth2 password授权模式
原贴地址: https://segmentfault.com/a/1190000012260914#articleHeader6 序 前面的一篇文章讲了spring security oauth2的c ...
- spring boot:spring security给用户登录增加自动登录及图形验证码功能(spring boot 2.3.1)
一,图形验证码的用途? 1,什么是图形验证码? 验证码(CAPTCHA)是"Completely Automated Public Turing test to tell Computers ...
- Spring Security 实战干货:客户端OAuth2授权请求的入口
1. 前言 在Spring Security 实战干货:OAuth2第三方授权初体验一文中我先对OAuth2.0涉及的一些常用概念进行介绍,然后直接通过一个DEMO来让大家切身感受了OAuth2.0第 ...
- Spring Boot 集成 Swagger2 与配置 OAuth2.0 授权
Spring Boot 集成 Swagger2 很简单,由于接口采用了OAuth2.0 & JWT 协议做了安全验证,使用过程中也遇到了很多小的问题,多次尝试下述配置可以正常使用. Maven ...
- Owin + WebApi + OAuth2 搭建授权模式(授权码模式 Part I)
绪 最近想要整理自己代码封装成库,也十分想把自己的设计思路贴出来让大家指正,奈何时间真的不随人意. 想要使用 OWIN 做中间件服务,该服务中包含 管线.授权 两部分.于是决定使用 webapi .O ...
- Spring Security实现OAuth2.0授权服务 - 基础版
一.OAuth2.0协议 1.OAuth2.0概述 OAuth2.0是一个关于授权的开放网络协议. 该协议在第三方应用与服务提供平台之间设置了一个授权层.第三方应用需要服务资源时,并不是直接使用用户帐 ...
- SpringCloud微服务实战——搭建企业级开发框架(四十):使用Spring Security OAuth2实现单点登录(SSO)系统
一.单点登录SSO介绍 目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的 ...
- Spring Security 解析(五) —— Spring Security Oauth2 开发
Spring Security 解析(五) -- Spring Security Oauth2 开发 在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决 ...
随机推荐
- 4.13 省选模拟赛 传销组织 bitset 强连通分量 分块
考试的时候昏了头 没算空间 这道题我爆零了.值得注意的是 一般认为bitset的空间是 int 的1/w倍 对于那m条边 无论如何构造 这m条关系都是存在的 题目其实是想让我们用这m条关系来计算给出的 ...
- IntelliJ IDEA 控制台输出中文乱码
IntelliJ IDEA 控制台输出中文乱码部分如图所示: 解决方法一: 1.打开IntelliJ IDEA本地安装目录中bin文件夹下的idea.exe.vmoptions和idea64.exe. ...
- Sharding-JDBC实现垂直拆分
参考资料:猿天地 https://mp.weixin.qq.com/s/wl8h6LIQUHztVuVbjfsU3Q 作者:尹吉欢 当一个项目量增大,数据表数量增多时,就需要对数据表进行垂直拆分, ...
- 声明式事务xml Spring
!--JDBC事务管理器--><bean id="tansactionManager" class="org.springframework.jdbc.dat ...
- 强大的输入框-应用快速启动uTools
uTools uTools是一个 极简.插件化.跨平台 的现代桌面软件.通过自由选配丰富的插件,打造你得心应手的工具集合. 当你熟悉它后,能够为你节约大量时间,让你可以更加专注地改变世界. uTool ...
- 秦九韶算法 & 三分法
前言 今天考试出了一个题 郭郭模拟退火骗了75分 于是再次把咕咕了好久的模退提上日程 如果进展顺利 明后天应该会开爬山算法和模退的博客笔记 今天先把今天考试的正解学习一下--三分法 引入 老规矩上板子 ...
- javascript作用域、预解析笔记
1.作用域 一般情况下,一段代码中所用到的名字并不总是有效可用的, 而限定这个名字(变量)的可用性的代码范围就是这个名字的作用域,可用有效的减少变量名冲突 2.js的作用域(e ...
- MySQL回表查询
一.MySQL索引类型 1.普通索引:最基本的索引,没有任何限制 2.唯一索引(unique index):索引列的值必须唯一,但是允许为空 3.主键索引:特殊的唯一索引,但是不允许为空,一般在建表的 ...
- C#LeetCode刷题之#55-跳跃游戏(Jump Game)
问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3674 访问. 给定一个非负整数数组,你最初位于数组的第一个位置. ...
- python设计模式之模板模式
python设计模式之模板模式 编写优秀代码的一个要素是避免冗余.在面向对象编程中,方法和函数是我们用来避免编写冗余代码的重要工具. 现实中,我们没法始终写出100%通用的代码.许多算法都有一些(但并 ...