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
扫码关注有惊喜!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
Java与C++中处理子类与父类同名函数的区别
Java中子类和父类的同名方法会处理为重载,若方法名和参数都相同,则覆盖父类方法。 例如: public class Test { public void func(int value) { Log.d("Test", "func(int value):" + value); } public void func(float value) { Log.d("Test", "func(float value):" + value); } } class ChildTest extends Test { public void func(char value) { Log.d("ChildTest", "func(char value):" + value); } // public void func(int value) { // Log.d("ChildTest", "func(int value):" + value); // } }; ChildTest test = new ChildTest(); test.func(3); ...
-
下一篇
【二进制安全】printf之任意读与写
前言 新手入门pwn,对于各位表哥在writeup中写的格式化字符串漏洞不是很理解,查阅网上资料发现大多都是以printf来深入讲解格式化字符串漏洞的原理,故作此文来探讨printf使用不当产生的漏洞,有不正确的地方,望大佬可以指出来。 printf剖析 printf是C语言中的输出函数,包含在头文件stdio.h文件中,功能是按规定格式向输出设备(一般为显示器)输出数据,并返回实际输出的字符数,若出错,则返回负数。printf函数的原型为: # include <stdio.h>intprintf(constchar*format,...); printf的一般格式有: 1) printf("字符串\n"); 2) printf("输出控制符",输出参数); 3) printf("输出控制符1 输出控制符2…", 输出参数1, 输出参数2, …); 4) printf("输出控制符 非输出控制符",输出参数); 由函数原型我们可以知道printf函数的第一个字符指向一个format字符串(格式化字符串),后面再不定的跟着一些参数。常见的格式符如下: %c:字符%d:十进制...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL数据库在高并发下的优化方案
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- 设置Eclipse缩进为4个空格,增强代码规范
- Docker容器配置,解决镜像无法拉取问题