一、实现原理
- PC 端发送 “扫码登录” 请求,服务端生成二维码 uuid,并存储二维码的过期时间、状态等信息。
- PC 端获取二维码并显示。
- PC 端开始轮询检查二维码的状态(2s一次),二维码最初为 “待扫描” 状态。
- 手机端扫描二维码,获取二维码 uuid。
- 手机端向服务端发送 “扫码” 请求,请求中携带二维码 uuid、手机端 access_token。
- 服务端验证手机端用户的合法性,验证通过后将二维码状态置为 “待确认”,并将用户信息与二维码关联在一起。
- PC 端轮询时检测到二维码状态为 “待确认”。
- 手机端向服务端发送 “确认登录” 请求,请求中携带着二维码 uuid、 access_token。
- 服务端验证 access_token和uuid绑定的access_token是否一致,验证通过后将二维码状态置为 “已确认”。
- PC 端轮询时检测到二维码状态为 “已确认”,并获取到了 PC 端 access_token,之后 PC 端不再轮询。
- PC 端通过 PC 端 access_token 访问服务端。
二、PC端接口
1.二维码状态
public enum QrCodeStatusEnum {
WAITING(10000,"待扫描"),
SCANNED(20000,"待确认"),
CONFIRMED(30000,"已确认"),
INVALID(40000,"二维码无效"),
CANCEL(50000,"已取消");
private Integer statusCode;
private String statusValue;
QrCodeStatusEnum(Integer statusCode, String statusValue) {
this.statusCode = statusCode;
this.statusValue = statusValue;
}
public Integer getStatusCode() {
return statusCode;
}
public String getStatusValue() {
return statusValue;
}
2.获取二维码
public CommonResult createQrImg() {
Map<Object, Object> resultMap = new HashMap<>();
// uuid
String uuid = UUID.randomUUID().toString();
LoginTicket loginTicket = new LoginTicket();
// 二维码最初为 WAITING 状态
loginTicket.setStatusCode(QrCodeStatusEnum.WAITING.getStatusCode());
loginTicket.setUuid(uuid);
//存入 redis 过期时间5分钟
redisUtils.set(uuid, loginTicket, UUID_EXPIRE_TIME);
QrConfig config = new QrConfig(300, 300);
//附带logo
//config.setImg(file);
// 设置边距,既二维码和背景之间的边距
//config.setMargin(3);
// 高纠错级别
//config.setErrorCorrection(ErrorCorrectionLevel.H);
// 设置前景色,既二维码颜色(青色)
//config.setForeColor(new Color(0, 60, 130).getRGB());
// 设置背景色(灰色)
//config.setBackColor(new Color(242, 242, 242).getRGB());
//二维码内容
Map<Object, Object> values = new HashMap<>();
values.put("uuid", uuid);
byte[] bytes = QrCodeUtil.generatePng(JSON.toJSONString(values), config);
String qrCode = Base64.getEncoder().encodeToString(bytes);
resultMap.put("uuid", uuid);
resultMap.put("QrCode", qrCode);
resultMap.put("statusCode", QrCodeStatusEnum.WAITING.getStatusCode());
return CommonResult.ok(resultMap);
}
3.扫描二维码
public Map<String, Object> scanQrCodeImg(String uuid) {
// 避免多个移动端同时扫描同一个二维码
lock.lock();
Map<String, Object> data = new HashMap<>();
try {
LoginTicket loginTicket = (LoginTicket) redisUtils.get(uuid);
// redis 中 key 过期后也可能不会立即删除
Long expired = redisUtils.getExpireForSeconds(uuid);
boolean valid = loginTicket != null &&
QrCodeStatusEnum.WAITING.getStatusCode().equals(loginTicket.getStatusCode()) &&
expired != null &&
expired >= 0;
if (valid) {
User user = UserUtil.getCurrentUser();
if (user == null) {
throw new RuntimeException("用户未登录");
}
// 修改扫码状态
loginTicket.setStatusCode(QrCodeStatusEnum.SCANNED.getStatusCode());
// 将二维码与用户进行关联
loginTicket.setUserId(user.getUserid());
redisUtils.set(uuid, loginTicket, expired);
data.put("statusCode", QrCodeStatusEnum.SCANNED.getStatusCode());
} else {
data.put("statusCode", QrCodeStatusEnum.INVALID.getStatusCode());
}
return data;
} finally {
lock.unlock();
}
}
4.确认登录
//operationType:操作类型 confirm:确认 ;cancel:取消登录
public CommonResult confirmLogin(String uuid, String operationType) {
Map<String, Object> data = new HashMap<>();
User currentUser = UserUtil.getCurrentUser();
//校验"确认登录"和"扫码登录"是同一个人
if (currentUser == null) {
throw new RuntimeException("请先登录");
}
Long expired = redisUtils.getExpireForSeconds(uuid);
if (expired == null || expired == 0 || expired ==-2) {
data.put("statusCode", QrCodeStatusEnum.INVALID.getStatusCode());
return CommonResult.ok(data);
} else {
LoginTicket loginTicket = (LoginTicket) redisUtils.get(uuid);
if (!(currentUser.getUserid()).equals(loginTicket.getUserId())) {
throw new RuntimeException("非法操作!");
}
if (operationType.equals("confirm")) {
lock.lock();
try {
loginTicket.setStatusCode(QrCodeStatusEnum.CONFIRMED.getStatusCode());
redisUtils.set(uuid, loginTicket, expired);
data.put("statusCode", QrCodeStatusEnum.CONFIRMED.getStatusCode());
} finally {
lock.unlock();
}
} else if (operationType.equals("cancel")) {
lock.lock();
try {
loginTicket.setStatusCode(QrCodeStatusEnum.CANCEL.getStatusCode());
redisUtils.set(uuid, loginTicket, expired);
data.put("statusCode", QrCodeStatusEnum.CANCEL.getStatusCode());
} finally {
lock.unlock();
}
}
}
return CommonResult.ok(data);
}
5.PC 端轮询获取二维码状态(2s一次)
//currentStatus: 当前二维码状态
public CommonResult getQrCodeStatus(HttpServletRequest request, HttpServletResponse response,@RequestParam String uuid ,@RequestParam Integer currentStatus) {
Map<Object, Object> resultMap = new HashMap<>();
LoginTicket loginTicket = (LoginTicket) redisUtils.get(uuid);
if (loginTicket ==null) {
resultMap.put("statusCode", QrCodeStatusEnum.INVALID.getStatusCode());
return CommonResult.ok(resultMap);
}
Integer statusCode = loginTicket.getStatusCode();
//二维码状态没有更新
if ((currentStatus).equals(statusCode)) {
resultMap.put("statusCode", statusCode);
}
User user = userService.selectByUserId(loginTicket.getUserId());
//二维码状态为待扫描
if ((statusCode).equals(QrCodeStatusEnum.WAITING.getStatusCode())) {
resultMap.put("statusCode", statusCode);
}
//二维码状态为已扫描
if ((statusCode).equals(QrCodeStatusEnum.SCANNED.getStatusCode())) {
resultMap.put("statusCode", statusCode);
}
//二维码状态为已取消
if ((statusCode).equals(QrCodeStatusEnum.CANCEL.getStatusCode())) {
resultMap.put("statusCode", statusCode);
redisUtils.delete(uuid);
}
//二维码状态为已确定 用户确认后为 PC 端生成 access_token
if ((statusCode).equals(QrCodeStatusEnum.CONFIRMED.getStatusCode())) {
Map<String, String> parameters=new HashMap<>();
parameters.put("client_id","android");
parameters.put("grant_type","password");
parameters.put("password",user.getPassword());
if (user.getIsEnterprise() != null && user.getIsEnterprise()==1) {
//企业主用户
parameters.put("userType","enterprise");
}else if (user.getIsEnterprise() != null && user.getIsEnterprise()==0){
//普通用户
parameters.put("userType","user");
}
parameters.put("username",user.getUsername());
parameters.put("client_id","android");
//调用登录接口生成access_token
CommonResult<?> loginResult = usersController.login(request, response, parameters);
if (loginResult != null && loginResult.getCode()==0) {
//获取access_token成功
DefaultOAuth2AccessToken data = (DefaultOAuth2AccessToken) loginResult.getData();
resultMap.put("access_token",data.getValue());
resultMap.put("token_type",data.getTokenType());
resultMap.put("refresh_token",data.getRefreshToken());
resultMap.put("expires_in",data.getExpiresIn());
resultMap.put("scope",data.getScope());
resultMap.put("username",data.getAdditionalInformation().get("username"));
resultMap.put("statusCode", statusCode);
}else {
//获取access_token失败
throw new RuntimeException("获取accessToken失败");
}
}
return CommonResult.ok(resultMap);
}
本文含有隐藏内容,请 开通VIP 后查看