- PC端:
- 1、向服务器请求,获取二维码
- 3、显示二维码,并且循环向服务器请求,查询此ID的登录状态
- 移动端:
- 4、扫描二维码(ID)、向服务改变此二维码(唯一的ID)的登录状态
- 服务器端:
- 2、生成一个唯一原ID,以它作为key,保存到redis里,值为false(表示未登录意思),返回此ID的二维码图片和ID
- 5、服务器检查此请求是否有登录,如果没有登录,返回没有登录的结果,让APP登录。如果已经登录了,修改此ID对应的登录状态为true(已经登录的状态),这个ID已经登录了。
- 6、当下一次PC端检查登录状态的时候,发现所携带的ID已经登录了,那就生成PC端的token相关信息,并且返回登录成功.
细节:
二维码的过期时间,也就是ID的有效期(3分钟),如果太久的话浪费资源。
多久轮询一次,多久查询一登录状态。可以用阻塞状态,30秒阻塞,如果30秒还没扫描,就返回等待扫描的提示。
有没有涉及到安全问题
二维码生成:
依赖
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
工具类
public class QrCodeUtils {
public static int QRCODE_SIZE = 300;
public static final String format = "png";
public static final String RESPONSE_CONTENT_TYPE = "image/png";
public static byte[] encodeQRCode(String text) {
try {
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
//容错率越高,可存储的信息越少;但是对二维码清晰对要求越小
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
//还可以设置logo之类的
// 生成二维码
BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE, hints);
bitMatrix = deleteWhite(bitMatrix);
BufferedImage bufferedImage = toBufferedImage(bitMatrix);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, out);
return out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static final int BORDER_WIDTH = 4;
public static BitMatrix deleteWhite(BitMatrix matrix) {
int[] rec = matrix.getEnclosingRectangle();
int resWidth = rec[2] + BORDER_WIDTH;
int resHeight = rec[3] + BORDER_WIDTH;
BitMatrix resMatrix = new BitMatrix(resWidth, resHeight);
resMatrix.clear();
for (int i = BORDER_WIDTH; i < resWidth; i++) {
for (int j = BORDER_WIDTH; j < resHeight; j++) {
if (matrix.get(i + rec[0], j + rec[1])) resMatrix.set(i, j);
}
}
return resMatrix;
}
public static BufferedImage toBufferedImage(BitMatrix matrix) {
int width = matrix.getWidth();
int height = matrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
int onColor = 0xFF000000;
int offColor = 0xFFFFFFFF;
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, matrix.get(x, y) ? onColor : offColor);
}
}
return image;
}
}
生成二维码接口
/***
* 获取二维码:
* 二维码的图片路径
* 二维码的内容字符串
* 要防止太频繁的请求
* @return
*/
@GetMapping("/pc-login-qr-code")
public ResponseResult getPcLoginQrCode() {
return userService.getPcLoginQrCodeInfo();
}
实现
@Override
public ResponseResult getPcLoginQrCodeInfo() {
//尝试取出上一次的loginId
String lastLoginId = CookieUtils.getCookie(getRequest(), Constants.User.LAST_REQUEST_LOGIN_ID);
if (!TextUtils.isEmpty(lastLoginId)) {
//先把redis里的删除
redisUtils.del(Constants.User.KEY_PC_LOGIN_ID + lastLoginId);
//检查上次的请求时间,如果太频繁,则直接骂人
Object lastGetTime = redisUtils.get(Constants.User.LAST_REQUEST_LOGIN_ID + lastLoginId);
if (lastGetTime != null) {
return ResponseResult.FAILED("服务器繁忙,请稍后重试.");
}
}
// 1、生成一个唯一的ID
long code = idWorker.nextId();
// 2、保存到redis里,值为false,时间为5分钟(二维码的有效期)
redisUtils.set(Constants.User.KEY_PC_LOGIN_ID + code,
Constants.User.KEY_PC_LOGIN_STATE_FALSE,
Constants.TimeValueInSecond.MIN_5);
Map<String, Object> result = new HashMap<>();
String originalDomain = TextUtils.getDomain(getRequest());
result.put("code", code);
result.put("url", originalDomain + "/portal/image/qr-code/" + code);
CookieUtils.setUpCookie(getResponse(), Constants.User.LAST_REQUEST_LOGIN_ID, String.valueOf(code));
redisUtils.set(Constants.User.LAST_REQUEST_LOGIN_ID + String.valueOf(code),
"true", Constants.TimeValueInSecond.SECOND_10);
// 返回结果
return ResponseResult.SUCCESS("获取成功.").setData(result);
}
改变登录状态接口
@PutMapping("/qr-code-state/{loginId}")
public ResponseResult updateQrCodeLoginState(@PathVariable("loginId") String loginId) {
return userService.updateQrCodeLoginState(loginId);
}
实现
/**
* 更新二维码的登录状态
*
* @param loginId
* @return
*/
@Override
public ResponseResult updateQrCodeLoginState(String loginId) {
//1、检查用户是否登录
SobUser sobUser = checkSobUser();
if (sobUser == null) {
return ResponseResult.ACCOUNT_NOT_LOGIN();
}
//2、改变loginId对应的值=true
redisUtils.set(Constants.User.KEY_PC_LOGIN_ID + loginId, sobUser.getId());
//2.1、通知正在等待的扫描任务
countDownLatchManager.onPhoneDoLogin(loginId);
//3、返回结果
return ResponseResult.SUCCESS("登录成功.");
}
检查登录状态接口
/**
* 检查二维码的登录状态
*
* @return
*/
@GetMapping("/qr-code-state/{loginId}")
public ResponseResult checkQrCodeLoginState(@PathVariable("loginId") String loginId) {
return userService.checkQrCodeLoginState(loginId);
}
实现
/**
* 检查二维码的登录状态
* 结果有:
* 1、登录成功(loginId对应的值为有ID内容)
* 2、等待扫描(loginId对应的值为false)
* 3、二维码已经过期了 loginId对应的值为null
* <p>
* 是被PC端轮询调用的
*
* @param loginId
* @return
*/
@Override
public ResponseResult checkQrCodeLoginState(String loginId) {
//从redis里取值出来
ResponseResult result = checkLoginIdState(loginId);
if (result != null) return result;
//先等待一段时间,再去检查
//如果超出了这个时间,我就们就返回等待扫码
Callable<ResponseResult> callable = new Callable<ResponseResult>() {
@Override
public ResponseResult call() throws Exception {
try {
log.info("start waiting for scan...");
//先阻塞
countDownLatchManager.getLatch(loginId).await(Constants.User.QR_CODE_STATE_CHECK_WAITING_TIME,
TimeUnit.SECONDS);
//收到状态更新的通知,我们就检查loginId对应的状态
log.info("start check login state...");
ResponseResult checkResult = checkLoginIdState(loginId);
if (checkResult != null) return checkResult;
//超时则返回等待扫描
//完事后,删除对应的latch
return ResponseResult.WAiTING_FOR_SCAN();
} finally {
log.info("delete latch...");
countDownLatchManager.deleteLatch(loginId);
}
}
};
try {
return callable.call();
} catch (Exception e) {
e.printStackTrace();
}
return ResponseResult.WAiTING_FOR_SCAN();
}
CountDownLatch管理器
/**
* 管理CountDownLatch
* 获取
* 删除
*/
@Component
public class CountDownLatchManager {
Map<String, CountDownLatch> latches = new HashMap<>();
public void onPhoneDoLogin(String loginId) {
CountDownLatch countDownLatch = latches.get(loginId);
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
public CountDownLatch getLatch(String loginId) {
CountDownLatch countDownLatch = latches.get(loginId);
if (countDownLatch == null) {
countDownLatch = new CountDownLatch(1);
latches.put(loginId, countDownLatch);
}
return countDownLatch;
}
public void deleteLatch(String loginId) {
latches.remove(loginId);
}
}