遇到一个场景,需要同时支持手机号或者邮箱和密码或者验证码进行登录的场景,故来记录一下。
说明:此流程主要是基于若依框架集成的多种方式登录,主要演示登录业务逻辑和前端登录密码和验证码切换组件和配置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();
}
- 重写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方法文章来源:https://www.toymoban.com/news/detail-680212.html
/**
* 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());
}
大概就这么多,如果有更好的方式,欢迎交流。文章来源地址https://www.toymoban.com/news/detail-680212.html
到了这里,关于【若依RuoYi短信验证码登录】汇总的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!