【若依RuoYi短信验证码登录】汇总

遇到一个场景,需要同时支持手机号或者邮箱和密码或者验证码进行登录的场景,故来记录一下。

说明:此流程主要是基于若依框架集成的多种方式登录,主要演示登录业务逻辑和前端登录密码和验证码切换组件和配置Security

一:后端登录业务逻辑代码:

因为有多个端,多个语言共享登录接口,所以,接口定义尽量简单,接口内的逻辑判断尽量全面,判断手机号还是邮箱登录,再判断密码还是验证码登录,验证完了之后,再去验证用户是否存在数据库中,如果是密码登录的,则需要对比密码,然后再创建一个登录的token,返回。

 public AjaxResult login(LoginBody loginBody){
        //验证手机号和邮箱是否符合格式或者是否为空
        boolean isPhone = false;
        //先判断是手机号还是邮箱登录
        if(StringUtils.isNotEmpty(loginBody.getTel()) && Pattern.compile("^[1][1,2,3,4,5,6,7,8,9][0-9]{9}$").matcher(loginBody.getTel()).matches()){
            isPhone = true;
        }else if(StringUtils.isNotEmpty(loginBody.getEmail()) && loginBody.getEmail().matches("\\w{1,30}@[a-zA-Z0-9]{2,20}(\\.[a-zA-Z0-9]{2,20}){1,2}")){
            isPhone = false;
        }else{
            return AjaxResult.error("登录失败,邮箱和手机号不能同时为空!");
        }

        //在判断是密码还是验证码登录
        boolean isPassword = false;
        if(StringUtils.isNotEmpty(loginBody.getPassword())){
            isPassword = true;
        }else if(StringUtils.isNotEmpty(loginBody.getCode())){
            isPassword = false;
        }else{
            return AjaxResult.error("登录失败,密码和验证码不能同时为空!");
        }
		//验证码验证
        if(!isPassword){
            String codeKey = "0:" + isPhone? loginBody.getTel(): loginBody.getEmail());
            String value = redisCache.getCacheObject(codeKey);
            if (StringUtils.isNotEmpty(value)) {
                if (!value.equals(loginBody.getCode())) {
                    return AjaxResult.error("验证码错误!");
                }
            }else{
                return AjaxResult.error("验证码超时!");
            }
        }
  		// 用户验证
        Authentication authentication = null;
        try
        {
			if(isPassword){
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(isPhone? loginBody.getTel(): loginBody.getEmail(), loginBody.getPassword());
                AuthenticationContextHolder.setContext(authenticationToken);
                // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
                authentication = authenticationManager.authenticate(authenticationToken);
            }else{
                // 该方法会去调用UsernamePhoneUserDetailsServiceImpl.loadUserByUsername
                authentication = authenticationManager.authenticate(new UsernamePhoneAuthenticationToken(isPhone? loginBody.getTel(): loginBody.getEmail()));
            }        
		}
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
        finally
        {
            AuthenticationContextHolder.clearContext();
        }

        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUserId());
        // 生成token
        return tokenService.createToken(loginUser);
}
二:前端登录密码和验证码切换组件:
<template>
  <div class="login">
    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">后台管理系统</h3>
      <el-form-item prop="loginP1" v-if="!isSmsLogin">
        <el-input v-model="loginForm.loginP1" type="text" auto-complete="off" placeholder="请输入手机号/邮箱" >
          <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
        </el-input>
      </el-form-item>
      <el-form-item prop="password" v-if="!isSmsLogin">
        <el-input
          v-model="loginForm.password"
          type="password"
          auto-complete="off"
          placeholder="请输入密码"
          @keyup.enter.native="handleLogin"
        >
          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
        </el-input>
      </el-form-item>
      <el-form-item prop="loginP2" v-if="isSmsLogin">
        <el-input v-model="loginForm.loginP2" type="text" auto-complete="off" placeholder="请输入手机号/邮箱">
          <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
        </el-input>
      </el-form-item>
      <el-form-item prop="code" v-if="isSmsLogin">
        <el-input
          v-model="loginForm.code"
          auto-complete="off"
          placeholder="验证码"
          style="width: 63%"
          @keyup.enter.native="handleLogin"
        >
          <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
        </el-input>
        <div class="login-code">
          <el-button round @click.native.prevent="getSmsCode">{{computeTime>0 ? `(${computeTime}s)已发送` : '获取验证码'}}</el-button>
        </div>
      </el-form-item>
      <el-row>
        <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">{{isSmsLogin ? '记住手机号/邮箱' : '记住密码'}}</el-checkbox>
        <div class="sms-login">
          <el-button
            size="mini"
            type="text"
            @click.native.prevent="loginMethod"
          >
            <span v-if="isSmsLogin">账号密码登录</span>
            <span v-else>验证码登录</span>
          </el-button>
        </div>
      </el-row>
      <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          size="medium"
          type="primary"
          style="width:100%;"
          @click.native.prevent="handleLogin"
        >
          <span v-if="!loading">登 录</span>
          <span v-else>登 录 中...</span>
        </el-button>
        <div style="float: right;" v-if="register">
          <router-link class="link-type" :to="'/register'">立即注册</router-link>
        </div>
      </el-form-item>
    </el-form>
    <!--  底部  -->
    <div class="el-login-footer">
      <span>Copyright © 2018-2022 xiaoqiang All Rights Reserved.</span>
    </div>
  </div>
</template>
三:配置Security:

按照Security的流程图可知,实现多种方式登录,只需要重写三个主要的组件,第一个用户认证处理过滤器,第二个用户认证token类,第三个,自定义短信登录身份认证。
在这里插入图片描述

1.参考UsernamePasswordAuthenticationToken类,继承AbstractAuthenticationToken,重写以下几个方法,自定义短信登录token验证。

/**
 * 自定义短信登录token验证
 */
public class UsernamePhoneAuthenticationToken extends AbstractAuthenticationToken {

    /**
     * 手机号
     */
    private final Object principal;

    public UsernamePhoneAuthenticationToken(Object principals){
        super(null);
        this.principal = principals;
        setAuthenticated(false);
    }

    public UsernamePhoneAuthenticationToken(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) throws IllegalArgumentException{
        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();
    }
  1. 重写UserDetailsService类的loadUserByUsername方法,实现用户验证处理。
/**
 * 用户验证处理
 */
@Service("userDetailsByPhone")
public class UsernamePhoneUserDetailsServiceImpl implements UserDetailsService {

    private static final Logger logger = LoggerFactory.getLogger(UsernamePhoneUserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser;
        if(Pattern.compile("^[1][1,2,3,4,5,6,7,8,9][0-9]{9}$").matcher(username).matches()){
            sysUser = sysUserMapper.selectUserByTel(username);
        }else if(username.matches("\\w{1,30}@[a-zA-Z0-9]{2,20}(\\.[a-zA-Z0-9]{2,20}){1,2}")){
            sysUser = sysUserMapper.selectUserByEmail(username);
        }else{
            throw new ServiceException("请使用手机号或者邮箱进行登录!");
        }

        if(StringUtils.isNull(sysUser)){
            logger.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户:" + username+ " 不存在");
        }
        return createLoginUser(sysUser);
    }

    public UserDetails createLoginUser(SysUser user){
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }

3.注意,此时会有两个用户验证的处理类,一个是原来的UserDetailsServiceImpl,另一个是现在的UsernamePhoneUserDetailsServiceImpl,需要去SecurityConfig配置类去配置不同的用户认证业务类,通过@Qualifer指定注入的bean。

/**
 * spring security配置
 * 
 * @author victor_zhang
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑(账号密码)
     */
    @Autowired
    @Qualifier("userDetailsByPass")
    private UserDetailsService userDetailsService;

    /**
     * 自定义用户认证逻辑(手机号验证码)
     */
    @Autowired
    @Qualifier("userDetailsByPhone")
    private UserDetailsService userDetailsByPhone;

	//此处省略若干代码......

}

4.自定义一个短信登录的身份鉴权, UserDetailsService 只负责根据用户名返回用户信息,AuthenticationProvider负责将 UserDetails 组装成 Authentication 向调用者返回。

/**
 * 自定义短信登录身份认证
 */
public class UsernamePhoneAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public UsernamePhoneAuthenticationProvider(UserDetailsService userDetailsService){
        setUserDetailsService(userDetailsService);
    }

    /**
     * 重写authentication方法,实现身份验证逻辑
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePhoneAuthenticationToken authenticationToken = (UsernamePhoneAuthenticationToken) authentication;
        String phone = (String) authenticationToken.getPrincipal();
        //委托 UserDetailsService 查找系统用户
        UserDetails userDetails = userDetailsService.loadUserByUsername(phone);
        //鉴权成功,返回一个拥有鉴权的AbstractAuthenticationToken
        UsernamePhoneAuthenticationToken authenticationTokenRes = new UsernamePhoneAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationTokenRes.setDetails(authenticationToken.getDetails());
        return authenticationTokenRes;
    }

    /**
     * 重写supports方法,指定此AuthenticationProvider 仅支持短信验证码身份验证
     */
    @Override
    public boolean supports(Class<?> authentication){
        return UsernamePhoneAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

5.配置SecurityConfig 的configure方法

/**
 * spring security配置
 * 
 * @author victor_zhang
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑(账号密码)
     */
    @Autowired
    @Qualifier("userDetailsByPass")
    private UserDetailsService userDetailsService;

    /**
     * 自定义用户认证逻辑(手机号验证码)
     */
    @Autowired
    @Qualifier("userDetailsByPhone")
    private UserDetailsService userDetailsByPhone;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;
    
    //此处省略n行代码......

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        //手机或邮箱的验证码的验证
        auth.authenticationProvider(new UsernamePhoneAuthenticationProvider(userDetailsByPhone));
        //账号密码的验证       
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

大概就这么多,如果有更好的方式,欢迎交流。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值