SpringCloud Alibaba微服务实战十八

Easter79
• 阅读 469

SpringCloud Alibaba微服务实战十八

概述

大家都知道在oauth2认证体系中有四种授权模式:

  • 授权码模式(authorization code)

  • 简化模式(implicit)

  • 客户端模式(client credentials)

  • 密码模式(password)

那么如何新增一个自定义的授权模式,比如像下面这样根据手机号和短信验证码进行登录呢?SpringCloud Alibaba微服务实战十八

要自定义授权模式我们得先了解下oauth2.0的整体认证过程,认证入口在 org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#postAccessToken 方法中

@RequestMapping(         value = {"/oauth/token"},         method = {RequestMethod.POST}     ) public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {    ... }

通过阅读源码可以梳理出核心认证逻辑代码的执行顺序(password模式):SpringCloud Alibaba微服务实战十八

核心源码解读

  • TokenEndpoint#postAccessToken(...) 主入口

OAuth2AccessToken token =  getTokenGranter().grant(tokenRequest.getGrantType(),  tokenRequest);

  • CompositeTokenGranter#grant(String grantType,TokenRequest tokenRequest ) 负责从所有的TokenGranter中根据授权类型找到具体的TokenGranter

public class CompositeTokenGranter implements TokenGranter {     private final List<TokenGranter> tokenGranters;  ...  public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {   for (TokenGranter granter : tokenGranters) {    OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);    if (grant!=null) {     return grant;    }   }   return null;  }  ... }

那么这里的tokenGranters又是从哪来的呢?答案是oauth2认证服务器端点配置类 AuthorizationServerEndpointsConfigurer

`public final class AuthorizationServerEndpointsConfigurer {
 ...
 private TokenGranter tokenGranter;
 public TokenGranter getTokenGranter() {
  return tokenGranter();
 }

 //默认的四种授权模式+Refresh令牌模式
 private List getDefaultTokenGranters() {
  ClientDetailsService clientDetails = clientDetailsService();
  AuthorizationServerTokenServices tokenServices = tokenServices();
  AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
  OAuth2RequestFactory requestFactory = requestFactory();

  List tokenGranters = new ArrayList();
  tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
    requestFactory));
  tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
  ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
  tokenGranters.add(implicit);
  tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
  if (authenticationManager != null) {
   tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
     clientDetails, requestFactory));
  }
  return tokenGranters;
 }

 private TokenGranter tokenGranter() {
  if (tokenGranter == null) {
   tokenGranter = new TokenGranter() {
    private CompositeTokenGranter delegate;

    @Override
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
     if (delegate == null) {
      delegate = new CompositeTokenGranter(getDefaultTokenGranters());
     }
     return delegate.grant(grantType, tokenRequest);
    }
   };
  }
  return tokenGranter;
 }
 ...
}
`

可以看到Spring已经把把默认的四种授权模式+刷新令牌的模式的配置在代码中写死了,那么如何让Spring能识别我们自定义的授权模式呢?

我们可以通过配置类覆盖TokenGranter,在里面注入我们自定义的授权模式!

  • ProviderManager#authenticate(Authentication authentication)这个类是提供了认证的实现逻辑和流程,他负责从所有的AuthenticationProvider中找出具体的Provider进行认证

`public class ProviderManager implements AuthenticationManager, MessageSourceAware,
  InitializingBean {
 ...
 public Authentication authenticate(Authentication authentication)
   throws AuthenticationException {
  Class<? extends Authentication> toTest = authentication.getClass();
  AuthenticationException lastException = null;
  AuthenticationException parentException = null;
  Authentication result = null;
  Authentication parentResult = null;
  boolean debug = logger.isDebugEnabled();
  //遍历所有的providers使用supports方法判断该provider是否支持当前的认证类型
  for (AuthenticationProvider provider : getProviders()) {
   if (!provider.supports(toTest)) {
    continue;
   }

   try {
   //找到具体的provider进行认证
    result = provider.authenticate(authentication);
    if (result != null) {
     copyDetails(authentication, result);
     break;
    }
   }
   catch (AccountStatusException | InternalAuthenticationServiceException e) {
    prepareException(e, authentication);
    throw e;
   } catch (AuthenticationException e) {
    lastException = e;
   }
  }
  throw lastException;
 }
 ...
}
`

代码实现(核心代码)

SpringCloud Alibaba微服务实战十八

image.png

使用手机号登录时先在表单中输入正确的手机号码,请求后端获取验证码。(此时后台服务一般会将手机号码和验证码进行关联,并设置一个较短时间的有效期)

手机获取到验证码后将其输入到表单中即可登录,后端框架将手机号与用户进行关联认证。

短信验证需要两个基础表单数据:手机号码,短信验证码。

本文并没有实现表单登录方式,是使用postman的方式进行认证。使用上图只是让大家对短信认证过程有个印象。

SmsCodeAuthenticationToken

`/**
 * 


 * SmsAuthenticationToken
 * 


 * Description:
 * 实现手机号登录,参考org.springframework.security.authentication.UsernamePasswordAuthenticationToken
 * @author javadaily
 * @date 2020/7/13 8:44
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 520L;

    /**
     * 账号主体信息,手机号验证码登录体系中代表 手机号码
     */
    private final Object principal;

    /**
     * 构建未授权的 SmsCodeAuthenticationToken
     * @param mobile 手机号码
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    /**
     * 构建已经授权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities){
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @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");
        }else{
            super.setAuthenticated(false);
        }
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}
`

SmsCodeAuthenticationProvider

`/**
 * Description:
 * 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
 * @author javadaily
 * @date 2020/7/13 13:07
 */
@Log4j2
public class SmsCodeAuthenticationProvider implements AuthenticationProvider{

    private IUserService userService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;
        userService = SpringContextHolder.getBean(IUserService.class);

        String mobile = (String) smsCodeAuthenticationToken.getPrincipal();

        //校验手机号验证码
        checkSmsCode(mobile);

        User user = userService.getUserByMobile(mobile);
        if(null == user){
            throw new BadCredentialsException("Invalid mobile!");
        }

        //授权通过
        UserDetails userDetails = buildUserDetails(user);
        return new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
    }

    /**
     * 构建用户认证信息
     * @param user 用户对象
     * @return UserDetails
     */
    private UserDetails buildUserDetails(User user) {
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                AuthorityUtils.createAuthorityList("ADMIN")) ;
    }

    /**
     * 校验手机号与验证码的绑定关系是否正确
     *  todo 需要根据业务逻辑自行处理
     * @author javadaily
     * @date 2020/7/23 17:31
     * @param mobile 手机号码
     */
    private void checkSmsCode(String mobile) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //获取验证码
        String smsCode = request.getParameter("smsCode");
        if(StringUtils.isEmpty(smsCode) || !"666666".equals(smsCode)){
            throw new BadCredentialsException("Incorrect sms code,please check !");
        }
        //todo  手机号与验证码是否匹配
    }

    /**
     * ProviderManager 选择具体Provider时根据此方法判断
     * 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
`

短信验证码模式认证实现类,需要实现AuthenticationProvider,通过 supports方法会被ProviderManager选中成为具体的认证实现类。手机号码与短信的关联关系需要根据自己业务场景实现,这里直接先写死。

配置类SmsCodeSecurityConfig

@Component public class SmsCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {     /**      * 短信验证码配置器      *  所有的配置都可以移步到WebSecurityConfig      *  builder.authenticationProvider() 相当于 auth.authenticationProvider();      *  使用外部配置必须要在WebSecurityConfig中用http.apply(smsCodeSecurityConfig)将配置注入进去      * @param builder      * @throws Exception      */     @Override     public void configure(HttpSecurity builder) throws Exception {         //注入SmsCodeAuthenticationProvider         SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();         builder.authenticationProvider(smsCodeAuthenticationProvider);     } }

此类主要实现SmsCodeAuthenticationProvider的注入,否则ProviderManager无法选到SmsCodeAuthenticationProvider。

SmsCodeTokenGranter

`/**
 * 扩展认证模式
 * @author javadaily
 * @date 2020/7/14 8:31
 */
public class SmsCodeTokenGranter extends AbstractTokenGranter{

    private static final String GRANT_TYPE = "sms_code";

    private final AuthenticationManager authenticationManager;

    public SmsCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    protected SmsCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String mobile = parameters.get("mobile");

        Authentication userAuth = new SmsCodeAuthenticationToken(mobile);

        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException ex) {
            throw new InvalidGrantException(ex.getMessage());
        } catch (BadCredentialsException ex) {
            throw new InvalidGrantException(ex.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate mobile: " + mobile);
        }
    }
}
`

继承AbstractTokenGranter扩展认证模式sms_code,需要将其添加到Spring中并通过grantType被选中。

配置类TokenGranterConfig

通过前面几步自定义认证的基础逻辑都已实现,接下来需要将我们的短信认证模式添加到Spring中,主要参考 org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultTokenGranters() 实现。

`/**
 *参考实现:org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultTokenGranters()
 * @author javadaily
 * @date 2020/7/14 8:38
 */
@Configuration
public class TokenGranterConfig {
    @Autowired
    private ClientDetailsService clientDetailsService;

    private TokenGranter tokenGranter;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    TokenEnhancer tokenEnhancer;

    @Autowired
    private AuthenticationManager authenticationManager;

    private AuthorizationServerTokenServices tokenServices;

    private boolean reuseRefreshToken = true;

    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public TokenGranter tokenGranter(){
        if(null == tokenGranter){
            tokenGranter = new TokenGranter() {
                private CompositeTokenGranter delegate;

                @Override
                public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
                    if(delegate == null){
                        delegate = new CompositeTokenGranter(getDefaultTokenGranters());
                    }
                    return delegate.grant(grantType,tokenRequest);
                }
            };
        }
        return tokenGranter;
    }

    private List getDefaultTokenGranters() {
        AuthorizationServerTokenServices tokenServices = tokenServices();
        AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
        OAuth2RequestFactory requestFactory = requestFactory();

        List tokenGranters = new ArrayList();
        //授权码模式
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory));
        //refresh模式
        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory));
        //简化模式
        ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory);
        tokenGranters.add(implicit);
        //客户端模式
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory));

        if (authenticationManager != null) {
            //密码模式
            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
            //短信验证码模式
            tokenGranters.add(new SmsCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
        }

        return tokenGranters;
    }

    private AuthorizationServerTokenServices tokenServices() {
        if (tokenServices != null) {
            return tokenServices;
        }
        this.tokenServices = createDefaultTokenServices();
        return tokenServices;
    }

    private AuthorizationServerTokenServices createDefaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore);
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(reuseRefreshToken);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancer);
        addUserDetailsService(tokenServices, this.userDetailsService);
        return tokenServices;
    }

    /**
     * 添加预身份验证
     * @param tokenServices
     * @param userDetailsService
     */
    private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
        if (userDetailsService != null) {
            PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
            provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper(userDetailsService));
            tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
        }
    }

    /**
     * OAuth2RequestFactory的默认实现,它初始化参数映射中的字段,
     * 验证授权类型(grant_type)和范围(scope),并使用客户端的默认值填充范围(scope)(如果缺少这些值)。
     */
    private OAuth2RequestFactory requestFactory() {
        return new DefaultOAuth2RequestFactory(clientDetailsService);
    }

    /**
     * 授权码API
     * @return
     */
    private AuthorizationCodeServices authorizationCodeServices() {
        if (this.authorizationCodeServices == null) {
            this.authorizationCodeServices = new InMemoryAuthorizationCodeServices();
        }
        return this.authorizationCodeServices;
    }
}
`

修改认证服务器配置 AuthorizationServerConfig

在上面的TokenGranterConfig中已经创建了AuthorizationServerTokenServices,所以我们可以将AuthorizationServerConfig中的tokenServices功能删除,然后在方法 configure(AuthorizationServerEndpointsConfigurer endpoints) 中注入 tokenGranter 即可

@Configuration @EnableAuthorizationServer @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {  @Autowired     private  TokenGranter tokenGranter;  ...     @Override     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {         endpoints.tokenGranter(tokenGranter);     }  ... }

测试

  • 正常测试 通过debug模式可以看到SmsCodeTokenGranter已经被加入Spring,并能正常返回jwt token了。 SpringCloud Alibaba微服务实战十八 SpringCloud Alibaba微服务实战十八

  • 输入错误的手机号码进行认证 SpringCloud Alibaba微服务实战十八

  • 输入错误的短信验证码进行认证 SpringCloud Alibaba微服务实战十八

本篇文章是SpringCloud alibaba实战系列文章的第20篇,如果大家对之前的文章感兴趣可以移步 http://javadaily.cn/tags/SpringCloud 查看!

如果本文对你有帮助,

别忘记给我个三连:

点赞,转发,评论

咱们下期见!

收藏 等于白嫖点赞 才是真情!

End

干货分享

这里为大家准备了一份小小的礼物,关注公众号,输入如下代码,即可获得百度网盘地址,无套路领取!

001:《程序员必读书籍》
002:《从无到有搭建中小型互联网公司后台服务架构与运维架构》
003:《互联网企业高并发解决方案》
004:《互联网架构教学视频》
006:《SpringBoot实现点餐系统》
007:《SpringSecurity实战视频》
008:《Hadoop实战教学视频》
009:《腾讯2019Techo开发者大会PPT》

010: 微信交流群

近期热文top

1、关于JWT Token 自动续期的解决方案

2、还不了解ETL,看看这篇文章?

3、数据库优化技巧 - SQL语句优化

4、架构师之路-微服务技术选型

5、RocketMQ进阶 - 事务消息

SpringCloud Alibaba微服务实战十八

我就知道你“在看”

SpringCloud Alibaba微服务实战十八

本文分享自微信公众号 - JAVA日知录(javadaily)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
00_设计模式之语言选择
设计模式之语言选择设计模式简介背景设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。设计模式(Designpattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k