扫码登录功能实现,基于ruoyi-vue分离版举例集成
·
一、扫码登录pc端功能实现逻辑
1、pc生成二维码(包含一个二维码token,后端生成token并存入redis和初始状态为等待,设置二维码过期时间)
2、打开二维码页面时,生成二维码,并开始不断向后端轮询检查redis中二维码token状态
3、手机端扫码获取二维码的token,并向后端更新token状态(更新redis中的token状态为已扫码,存入移动端登录的用户名)
4、轮询还在继续,在轮询中判断获取到的token状态是否是已扫码,是的话提取其中用户名并中断轮询,再后端发起系统的登入请求(额外写一个只需用户名登录的接口,获取token,这一步可以和状态方法合并,避免免密登录接口暴露)成功后再重定向到系统首页
二、java后端实现
1.扫码登录业务服务 Service
@Service
public class ScanLoginService {
@Autowired
private RedisCache redisCache;
private static final String LOGIN_TOKEN_PREFIX = "scan_token:";
//创建token并记录到redis
public String createLoginToken() {
String token = UUID.randomUUID().toString();
redisCache.setCacheObject(LOGIN_TOKEN_PREFIX + token, "waiting", 10, TimeUnit.MINUTES);
return token;
}
//扫码后调用更新redis中token状态
public boolean scanToken(String token, String username) {
String key = LOGIN_TOKEN_PREFIX + token;
String status = redisCache.getCacheObject(key);
if (status == null || !"waiting".equals(status)) {
return false;
}
// 标记为已扫码,存用户名(后续可以自动登录)
redisCache.setCacheObject(key, "scanned:" + username, 10, TimeUnit.MINUTES);
return true;
}
//检查redis中token状态
public String checkStatus(String token) {
return redisCache.getCacheObject(LOGIN_TOKEN_PREFIX + token);
}
}
2.新建扫码登录 Controller
主要接口:二维码token、移动端扫码后调用、查询扫码状态(合并登录判断,避免用户名登入接口对外开放),同时在放开接口权限:在SecurityConfig的configure(HttpSecurity httpSecurity)方法中.antMatchers("/scanLogin/**").permitAll()
@RestController
@RequestMapping("/scanLogin")
public class ScanLoginController {
@Autowired
private ScanLoginService scanLoginService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenService tokenService;
@Autowired
private ISysUserService userService;
// 生成二维码token
@GetMapping("/token")
public AjaxResult getToken() {
String token = scanLoginService.createLoginToken();
return AjaxResult.success("token", token);
}
// 扫码接口,扫码端调用(微信扫码确认登录)
@PostMapping("/scan")
public AjaxResult scan(@RequestBody Map<String, String> params) {
String token = params.get("token");
String username = params.get("username");
boolean success = scanLoginService.scanToken(token, username);
if (success) {
return AjaxResult.success("扫码成功");
}
return AjaxResult.error("扫码失败,token无效或超时");
}
// 查询扫码状态
@GetMapping("/status")
public AjaxResult status(@RequestParam String token) {
String status = scanLoginService.checkStatus(token);
if (status == null) {
return AjaxResult.error("token无效或过期");
}
if(status.startsWith("scanned:")){
String[] split = status.split(":");
String username = split[1];
SysUser user = userService.selectUserByUserName(username);
if (user == null) {
return AjaxResult.error("用户不存在");
}
try {
// 不用密码 用自定义AuthenticationProvider认证
Authentication authentication = authenticationManager.authenticate(new ScanAuthenticationToken(username));
SecurityContextHolder.getContext().setAuthentication(authentication);
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成JWT token返回
String jwtToken = tokenService.createToken(loginUser, CacheConstants.LOGIN_PC);
String res="token:"+jwtToken;
return AjaxResult.success("status",res);
} catch (AuthenticationException e) {
return AjaxResult.error("认证失败:" + e.getMessage());
}
}
return AjaxResult.success("status",status);
}
}
(中场休息)前面接口某些类会报红不存在,下面处理这个问题,处理新增扫码的认证实现:在com.ruoyi.framework.security下建包scan,然后加入下面三个自定义内容
3.自定义扫码登录认证 Provider
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* @author Administrator
*/
public class ScanAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public ScanAuthenticationProvider(UserDetailsService userDetailsService){
setUserDetailsService(userDetailsService);
}
/**
* 重写 authenticate方法,实现身份验证逻辑。
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ScanAuthenticationToken authenticationToken = (ScanAuthenticationToken) authentication;
String username = (String) authenticationToken.getPrincipal();
// 委托 UserDetailsService 查找系统用户
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 鉴权成功,返回一个拥有鉴权的 AbstractAuthenticationToken
ScanAuthenticationToken authenticationResult = new ScanAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/**
* 重写supports方法,指定此 AuthenticationProvider 仅支持短信验证码身份验证。
*/
@Override
public boolean supports(Class<?> authentication) {
return ScanAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
4.自定义扫码登录token验证
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 自定义扫码登录token验证
*/
public class ScanAuthenticationToken extends AbstractAuthenticationToken {
/**
* 存储用户id
*/
private final Object principal;
/**
* 构建一个没有鉴权的构造函数
*/
public ScanAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
* 构建一个拥有鉴权的构造函数
*/
public ScanAuthenticationToken(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();
}
}
5.自定义扫码登录用户验证处理
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* 用户验证处理
*
* @author Administrator
*/
@Service("userDetailsByScan")
public class UserDetailsByScan implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserDetailsByScan.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 防止漏查角色等所需的信息,尽量模仿根据用户名查用户的方法开发:ISysUserService.selectUserByUserName(username)
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username+ " 不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号:" + username+ " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("对不起,您的账号:" + username+ " 已停用");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user) {
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
}
6.Security配置中注册自定义Provider
找到SecurityConfig,在configure加入ScanAuthenticationProvider
Autowired
@Qualifier("userDetailsByScan")
private UserDetailsByScan userDetailsByScan;
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
if (!casProperties.isCasEnable()) {
//扫码登录provider
auth.authenticationProvider(new ScanAuthenticationProvider(userDetailsByScan));
// 用户密码登录
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}else{
super.configure(auth);
auth.authenticationProvider(casAuthenticationProvider());
}
}
三、前端逻辑实现
1、接口API的封装js
import request from '@/utils/request'
// 获取扫码token
export function getScanToken() {
return request({
url: '/scanLogin/token',
method: 'get'
})
}
// 检查扫码token状态
export function getScanTokenStatus(token) {
return request({
url: '/scanLogin/status',
method: 'get',
params: { token }
});
}
2、登录页面
关注实现逻辑,一些前端样式可能会有问题
<template>
<div class="login">
///////////其他/////////////////
<div v-if="currentLoginMethod === 'scan'" class="scan-login">
<h2 style="margin-top: 50px;">扫码登录</h2>
<div class="qr-code-container" style="position: relative; width: 240px; height: 240px;">
<img
v-if="qrCodeUrl"
:src="qrCodeUrl"
class="qr-code-img"
alt="QR Code"
style="width: 240px; height: 240px;"
/>
<div v-if="qrCodeExpired">
<p class="expired-text" style="color:red;font-weight:bold;">二维码已失效</p>
<button class="button1" @click="refreshQRCode">请点击刷新</button>
</div>
</div>
<p style="margin-bottom: 52px;">请使用手机扫描二维码进行登录</p>
</div>
</div>
</template>
<script>
import { getScanToken, getScanTokenStatus} from "@/api/scanLogin";
import QRCode from "qrcodejs2"; // 你需要安装 qrcode:npm install
import { setToken } from '@/utils/auth';
export default {
name: "Login",
data() {
return {
currentLoginMethod: 'form', // 'form'或'scan'
qrCodeExpired: false,
scanToken: '',
pollingInterval: null,
qrCodeUrl:'',
};
},
watch: {
currentLoginMethod(newVal) {
if (newVal === 'scan') {
this.initScanLogin();
} else {
this.clearScanLogin();
}
}
},
mounted() {
if (this.currentLoginMethod === 'scan') {
this.initScanLogin();
}
},
beforeDestroy() {
this.clearScanLogin();
},
methods: {
toggleLoginMethod() {
this.currentLoginMethod = this.currentLoginMethod === 'form' ? 'scan' : 'form';
},
//初始化二维码相关方法
async initScanLogin() {
this.qrCodeExpired = false;
await this.createQRCode();
this.startPolling();
this.startQRCodeTimer();
},
clearScanLogin() {
this.qrCodeExpired = false;
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
}
this.stopQRCodeTimer();
this.scanToken = '';
if (this.$refs.qrCodeContainer) {
this.$refs.qrCodeContainer.innerHTML = '';
}
},
//创建二维码
async createQRCode() {
try {
const res = await getScanToken();
this.scanToken = res.token;
//看业务情况自定义内容,不一定要放链接,可以直接给token也行,具体看移动端实现
// 二维码内容,扫码端打开确认页面URL,携带token
const qrContent = `${window.location.origin}/scanConfirm?token=${this.scanToken}`;
// 清空之前二维码
if (this.$refs.qrCodeContainer) {
this.$refs.qrCodeContainer.innerHTML = '';
// 生成二维码
new QRCode(this.$refs.qrCodeContainer, {
text: qrContent,
width: 240,
height: 240,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}
} catch (error) {
this.$message.error('获取扫码二维码失败,请稍后重试');
console.error(error);
}
},
//轮询获取扫码状态
startPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
this.pollingInterval = setInterval(async () => {
if (!this.scanToken) return;
try {
const res = await getScanTokenStatus(this.scanToken);
if (res.code!=200) {
// token过期或无效
this.qrCodeExpired = true;
clearInterval(this.pollingInterval);
this.pollingInterval = null;
return;
}
const status = res.data || '';
if(status.startsWith('token:')){
const jwtToken = status.split(':')[1];
clearInterval(this.pollingInterval);
this.pollingInterval = null;
this.qrCodeExpired = true; // 防止继续轮询
// 调用扫码登录接口,自动登录
await this.scanLogin(jwtToken);
}
} catch (error) {
console.error('扫码状态轮询异常', error);
}
}, 2000);
},
//二维码登录跳转
async scanLogin(jwtToken) {
try {
setToken(jwtToken);
this.$message.success('扫码登录成功');
// 登录成功后跳转首页或重定向页面
this.$router.push(this.redirect || '/');
} catch (error) {
this.$message.error('扫码登录异常,请重试');
console.error(error);
}
},
//启动定时器
startQRCodeTimer() {
this.qrCodeExpired = false;
if (this.qrCodeTimeout) {
clearTimeout(this.qrCodeTimeout);
}
this.qrCodeTimeout = setTimeout(() => {
this.qrCodeExpired = true;
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
}
}, 30000); // 30秒二维码过期
},
//定时器停止
stopQRCodeTimer() {
if (this.qrCodeTimeout) {
clearTimeout(this.qrCodeTimeout);
}
},
//刷新二维码
refreshQRCode() {
this.qrCodeExpired = false;
this.createQRCode();
this.startPolling();
this.startQRCodeTimer();
}
}
};
</script>
四、总结
关注实现的具体逻辑思路就行。
更多推荐




所有评论(0)