遇到一个场景,需要同时支持手机号或者邮箱和密码或者验证码进行登录的场景,故来记录一下。 说明:此流程主要是基于若依框架集成的多种方式登录,主要演示登录业务逻辑和前端登录密码和验证码切换组件和配置Sec
说明:此流程主要是基于若依框架集成的多种方式登录,主要演示登录业务逻辑和前端登录密码和验证码切换组件和配置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的流程图可知,实现多种方式登录,只需要重写三个主要的组件,第一个用户认证处理过滤器,第二个用户认证token类,第三个,自定义短信登录身份认证。
参考UsernamePasswordAuthenticationToken类,继承AbstractAuthenticationToken,重写以下几个方法,自定义短信登录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(); }
- 重写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)); }
注意,此时会有两个用户验证的处理类,一个是原来的UserDetailsServiceImpl,另一个是现在的UsernamePhoneUserDetailsServiceImpl,需要去SecurityConfig配置类去配置不同的用户认证业务类,通过@Qualifer指定注入的bean。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)public class SecurityConfig extends WEBSecurityConfigurerAdapter{ @Autowired @Qualifier("userDetailsByPass") private UserDetailsService userDetailsService; @Autowired @Qualifier("userDetailsByPhone") private UserDetailsService userDetailsByPhone;//此处省略若干代码......}
自定义一个短信登录的身份鉴权, UserDetailsService 只负责根据用户名返回用户信息,AuthenticationProvider负责将 UserDetails 组装成 Authentication 向调用者返回。
public class UsernamePhoneAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; public UsernamePhoneAuthenticationProvider(UserDetailsService userDetailsService){ setUserDetailsService(userDetailsService); } @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; } @Override public boolean supports(Class<?> authentication){ return UsernamePhoneAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; }
配置SecurityConfig 的configure方法
@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; @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; //此处省略n行代码...... @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //手机或邮箱的验证码的验证 auth.authenticationProvider(new UsernamePhoneAuthenticationProvider(userDetailsByPhone)); //账号密码的验证 auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); }
大概就这么多,如果有更好的方式,欢迎交流。
来源地址:https://blog.csdn.net/z_xiao_qiang/article/details/130170360
--结束END--
本文标题: 【若依RuoYi短信验证码登录】汇总
本文链接: https://lsjlt.com/news/408399.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0