一、什么是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
集合里即可。
参考代码:PwdImgCodeGranter.java
场景二 :新加一种授权方式,如:手机号加密码登录。
该场景需要实现以下内容:
定义一个新的 grantType
类型,并新增对应的 TokenGranter
实现类添加到 CompositeTokenGranter
中的 tokenGranters
集合里
新增一个 AuthenticationToken
实现类,用于存放该授权所需的信息。
新增一个 AuthenticationProvider
实现类 实现授权的逻辑 ,并重写 supports
方法绑定步骤二的 AuthenticationToken
实现类
参考代码:MobilePwdGranter.java
四、代码实现 下面以 场景二 新增手机号加密码授权方式为例,展示核心的代码实现
4.1. 创建 AuthenticationToken 实现类 创建 MobileAuthenticationToken
类,用于存储手机号和密码信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 中的集合里 1 2 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