【JAVA】微服务中使用Spring Security OAuth2集成短信验证码登录和自定义登录(2)

2021/12/7 17:17:57

本文主要是介绍【JAVA】微服务中使用Spring Security OAuth2集成短信验证码登录和自定义登录(2),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

hey-girl东拼西凑原创文章,若有歧义可留言,若需转载需标明出处。

hey有话说: 之前的文章中已经详细的介绍了登录认证流程。建议搞不清的兄弟可以先去瞄瞄。在往下看。项目中集成auth2,给了我们内置的4种授权模式+一个令牌刷新的模式,用grant_type区分。
但是,仔细琢磨。这4种肯定是不能适合各种美好的需求的。不得整个洋气的手机登录或者验证码登录或者第三方微信啥的。

采用自定义grant_type来增加授权模式

注意:如果只是看实现,会很懵的。还是了解下内置的授权模式流程,在看自定义的。我们的小脑袋瓜才能收获满满。了解原理请看我之前的文章。

还有这是在原有的4种模式上新增,而不是说新增这一个,覆盖原有的。要是是并存

废话说完,开始整活。

  1. 大体思路分析。
    内置的5种模式TokenGranter 实现类,是不是都在CompositeTokenGranter这个类里面通过tokenGranters 管理在一起了。然后循环遍历对比grant_type,找到对应的实现类来处理授权
    而每种授权的方式对应一种AuthenticationProvider 实现类来实现。所有 AuthenticationProvider 实现类都通过 ProviderManager 中的 providers 集合存起来
    ,TokenGranter的实现类会整一个AuthenticationToken实现类给Manager(认证管理类),认证管理类里面也管理了很多Providers(认证具体实现类),那不又得遍历下,通过supports方法 ,看看AuthenticationToken 和谁匹配 。找到具体的那个AuthenticationProvider 去干活。
    大致的形容了下,是不是就知道我们自定义的时候要干啥了。

  2. 我们从头来,一步步捋清楚。做一个手机号+密码登录的模式练习下
    首先用老演员postman发起一个post请求。client_id和client_secret可以放参数中或者头部,随意就好了。
    手机号+密码
    动动我们小脑瓜想想。我们的授权模式grant_type:phone。这个模式是我们自己定义的,CompositeTokenGranter里面是没有管理的。我们想要程序认识新的模式。是不是得给安排个实现类。交给CompositeTokenGranter管理起来。

那有小可爱就犯糊涂了。我们怎么写实现类了。
来来来,看看下其他模式是怎么搞的,我们不能超越,但是可以先模仿是不是。
AbstractTokenGranter
那我们就挑个常用的,照着ResourceOwnerPasswordTokenGranter写。

  1. 创建一个PhoneCustomTokenGranter 继承 AbstractTokenGranter
    属性GRANT_TYPE 等于 phone
package com.hey.girl.auth.grant;
/**
 * @Description: 自定义grant_type颁发令牌
 * @author: heihei
 * @date: 2021年12月03日 11:39
 */
public class PhoneCustomTokenGranter extends AbstractTokenGranter {
    private static final String GRANT_TYPE = "phone";
    private final AuthenticationManager authenticationManager;

    public PhoneCustomTokenGranter(AuthenticationManager authenticationManager,
                                   AuthorizationServerTokenServices tokenServices,
                                   ClientDetailsService clientDetailsService,
                                   OAuth2RequestFactory requestFactory
                                  ) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, "phone");
    }

    protected PhoneCustomTokenGranter(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 phone = (String)parameters.get("phone");
        String password = (String)parameters.get("password");
        parameters.remove("password");
        Authentication userAuth = new PhoneAuthenticationToken(phone, password);
        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);
        try{
            userAuth = authenticationManager.authenticate(userAuth);
        }catch (Exception e){
            throw new InvalidGrantException("Could not authenticate mobile: " + phone);
        }
        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + phone);
        }
    }
}

Q1- 代码解析kankan
A1- GRANT_TYPE:授权模式;authenticationManager 认证管理类

Q2- getOAuth2Authentication方法解析
A2- 这个方法是在生成令牌的时候被调用的。 OAuth2Request(等于TokenRequests+ClientDetails的整合)和Authorization(当前授权用户)合在一起生成一个OAuth2Authentication。
在这方法中,主要就是获取请求参数。手机号和密码登录。然后生成一个PhoneAuthenticationToken,传递给manager管理类,通过provider的supports方法。根据不同类型的token,去找对应的Provider做具体认证。认证完以后拿到Authentication。就可以创建OAuth2Authentication对象了。

  1. 在上面的逻辑中需要生成PhoneAuthenticationToken。所以是不是又可以照着我们常用的UsernamePasswordAuthenticationToken抄袭下了。
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 1L;
    /**
     * 身份
     */
    private final Object principal;
    /**
     * 凭证
     */
    private Object credentials;
    public PhoneAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }
    public PhoneAuthenticationToken( 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 void setAuthenticated(boolean authenticated) throws IllegalArgumentException{
        Assert.isTrue(!authenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }
    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
    @Override
    public Object getPrincipal() {
        return this.principal;
    }
}
  1. 上面的逻辑还得要个Provider,所以照着DaoAuthenticationProvider整一个
    PhoneAuthenticationProvider
/**
 1. @Description: 认证具体实现类
 2. @author: heihei
 3. @date: 2021年12月03日 10:34
 */
@Setter
public class PhoneAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;
    /**
     * 认证具体方法
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 断言所提供的对象是所提供类的实例
        Assert.isInstanceOf(PhoneAuthenticationToken.class,authentication,()->{
            return "Only PhoneAuthenticationToken is supported";
        });
        PhoneAuthenticationToken  authenticationToken = (PhoneAuthenticationToken) authentication;
        String mobile = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        UserDetails user =userDetailsService.loadUserByUsername(mobile);
        PhoneAuthenticationToken authenticationResult = new PhoneAuthenticationToken(user, password,user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    /**
     * support方法来表示自己支持那种Token的认证
     */
    @Override
    public boolean supports(Class<?> aClass) {
        // 也就是判断当前的Class对象所表示的类,是不是参数中传递的Class对象所表示的类的父类,超接口,
        // 或者是相同的类型。是则返回true,否则返回false。
        return PhoneAuthenticationToken.class.isAssignableFrom(aClass);
    }
}

写到这里,是不是还有个疑问,UserDetailsService是不是得写个。之前写了个public class GirlUserDetailsServiceImpl implements UserDetailsService。在这个类里loadUserByUsername方法是去调取远程服务查询数据库等操作。但是我这里变了。我的需求可能是通过手机号或者邮箱等字段去查询用户。那么我就写个GirlPhoneUserDetailsService 具体业务逻辑,可根据实际改。主要是明白这里的作用

@Slf4j
@Service()
@RequiredArgsConstructor
public class GirlPhoneUserDetailsService implements UserDetailsService {
    /**
     * feign调用远程服务
     */
    private final RemoteUserService remoteUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);
        UserDetails userDetails = getUserDetails(result);
        return userDetails;
    }
    /**
     * 构建user details
     * @param result 用户信息
     * @return
     */
    private UserDetails getUserDetails(R<UserInfo> result) {
        if(result == null || result.getData() == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        UserInfo userInfo = result.getData();
        Set<String> dbAuthsSet = new HashSet<>();
        /* 获取角色
         * ArrayUtil是糊涂的工具
         */
        if (ArrayUtil.isNotEmpty(userInfo.getRoles())) {
            // 获取角色
            Arrays.stream(userInfo.getRoles()).forEach(role -> dbAuthsSet.add(SecurityConstants.ROLE + role));
            // 获取资源
            dbAuthsSet.addAll(Arrays.asList(userInfo.getPermissions()));
        }
        // 注意toArray(new Object[0])与toArray()在函数上是相同的
        Collection<? extends GrantedAuthority> authorities = AuthorityUtils
                .createAuthorityList(dbAuthsSet.toArray(new String[0]));
        SysUser user = userInfo.getSysUser();
        // 构造security用户
        return new GirlUser(user.getUserId(), user.getDeptId(), user.getUsername(),
                SecurityConstants.BCRYPT + user.getPassword(),
                StrUtil.equals(user.getLockFlag(), CommonConstants.STATUS_NORMAL),
                true,true, true, authorities );
    }
}
  1. 基本改写的写完了,我们是不是就可以跑起来了。No!!! 小脑袋瓜想想。这些新增的我们不得配置配置么。
  2. 配置PhoneCustomTokenGranter,你说你新增了模式。如何被管理。我们先看看程序本身的几种授权模式怎么被加载的
    目光聚焦到AuthorizationServerEndpointsConfigurer这里。调用了getDefaultTokenGranters()方法,并且创建了 CompositeTokenGranter的实例对象,进行初始化。程序默认写死了这些模式。
    AuthorizationServerEndpointsConfigurer
    getDefaultTokenGranters方法
    话说这份上,灵感是不是一下来了。我把我的新模式往后加,不就完事了么。所以只要要在AuthorizationServerConfig里面配置下。
    // 贴部分重要代码
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                .tokenStore(tokenStore())
                .tokenEnhancer(tokenEnhancer())
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)
                .reuseRefreshTokens(false)
                .pathMapping("/oauth/confirm_access", "/token/confirm_access")
                .exceptionTranslator(new GirlWebResponseExceptionTranslator());
        List<TokenGranter>  tokenGranters =   getDefaultTokenGranters(
                endpoints.getTokenServices(),
                endpoints.getClientDetailsService(),
                endpoints.getAuthorizationCodeServices(),
                endpoints.getOAuth2RequestFactory()
                );
        endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters));
    }
    private List<TokenGranter> getDefaultTokenGranters(
            AuthorizationServerTokenServices tokenServices,
            ClientDetailsService clientDetails,
            AuthorizationCodeServices authorizationCodeServices,
            OAuth2RequestFactory requestFactory
                                                       ) {
        List<TokenGranter> 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 (this.authenticationManager != null) {
            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(this.authenticationManager, tokenServices, clientDetails, requestFactory));
        }
        tokenGranters.add(new PhoneCustomTokenGranter(
                authenticationManager,tokenServices,
                clientDetails,
                requestFactory
        ));

        return tokenGranters;
    }

这这过程中遇到的问题。也是很常见的一种问题,就是我自己新增的模式覆盖程序提供的5种模式。可以看看这位作者写的添加自定义授权模式遇到的问题,和这位作者一样,我也想着用CompositeTokenGranter里的add方法,但是感觉不太知道哪里用比较合适。

  1. 是不是还感觉差点啥。是得没错。你新增的provider。你是不是得管理起来。
    这个配置就写WebSecurityConfigurerAdapter配置类中就好
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(phoneAuthenticationProvider());
        auth.authenticationProvider(daoAuthenticationProvider());
    }

    @Bean
    public PhoneAuthenticationProvider phoneAuthenticationProvider() {
        PhoneAuthenticationProvider phoneAuthenticationProvider= new PhoneAuthenticationProvider();
        phoneAuthenticationProvider.setUserDetailsService(userPhoneDetailsService);
        phoneAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return phoneAuthenticationProvider;
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return daoAuthenticationProvider;
    }

这里也是auth.authenticationProvider必须加你要用的provider,不然程序之前有的就GG被覆盖了。

最后一步,你新增一个授权模式,你的数据库表得加对应的。不然会匹配不上的。在这里插入图片描述
测试:
在这里插入图片描述
总结:不知道有没有其他更好的方法,如果有欢迎留言。
还是那句话。先了解登录的一些流程。才能得心应手的扩展



这篇关于【JAVA】微服务中使用Spring Security OAuth2集成短信验证码登录和自定义登录(2)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程