关于java:Java-语言实现简单扫码登录

31次阅读

共计 6231 个字符,预计需要花费 16 分钟才能阅读完成。

扫码登录

目前手机扫描二维码登录已成为一种支流的登录形式,尤其是在 PC 网页端。最近学习了一下扫码登录的原理,感觉蛮乏味的,所以借鉴了网上的一些示例,实现了一个简略的扫码登录的 demo,以此记录一下学习过程。

原理解析

流程简述
  1. PC 端关上二维码登录页面 login.html;
  2. login.html 调用后端接口 createQrCodeImg,该接口生成一个随机的 uuid,uuid 可看做是本页面的惟一标识,同时该接口还会创立一个 LoginTicket 对象,该对象中封装了如下信息:

    • uuid:页面的惟一标识;
    • userId:用户 id;
    • status:扫码状态,0 示意期待扫码,1 示意期待确认,2 示意已确认。
  3. 将上述 uuid 作为 key、LoginTicket 对象作为 value 存储在 Redis 服务器中(或其余数据库),设置其过期工夫为 5 分钟,示意 5 分钟后二维码生效。
  4. 生成二维码图片,二维码中封装的信息为一个 URL,相似于 http://localhost:8080/login/s…。
  5. PC 端显示二维码;
  6. PC 端页面一直轮询(多久轮询一次自行设置)查看扫码的进度,即 LoginTicket 对象的状态。如果为 0 或为 1,持续轮询;如果为 2,进行轮询(已确认登录);
  7. 手机端扫描二维码;
  8. 手机端(携带用户的 token,该 token 为手机端 token)拜访二维码中的指标网址,手机端服务器首先验证 token 是否无效,如果无效则将 LoginTicket 对象的 status 更新为 1;
  9. 手机端服务器询问用户是否确认登录;
  10. 用户抉择确认登录,手机端服务器将 LoginTicket 对象的 status 更新为 2,并将 userId 设置为以后用户的 id;
  11. PC 端检测到用户确认登录后,为用户生成 token(此 token 为 PC 端的 token),并将 token 返回给前端;
  12. 前端获取到 token 后就能够执行其余操作。
流程图

总体流程如下:

实现

环境筹备
  1. JDK 1.8;
  2. maven 3.3.6;
  3. Springboot 2.xx;
  4. Redis。
实体对象

LoginTicket 类定义如下:

@Data
public class LoginTicket {

   private String userId;

   private String uuid;

   private int status;
}

User 类简略封装用户的 id 和 name:

@Data
public class User {

   private String userId;

   private String userName;
}
登录接口
  1. 获取二维码
@RequestMapping(path = "/getQrCodeImg", method = RequestMethod.GET)
public String createQrCodeImg(Model model) {
   
    // 生成 uuid 和 loginTicket 对象并存入 Redis
    String uuid = loginService.createQrImg();
    // 应用 QrCodeUtil 生成二维码
    String QrCode = Base64.encodeBase64String(QrCodeUtil.generatePng(loginURL + uuid, 300, 300));
    // 返回 uuid 和二维码
    model.addAttribute("uuid", uuid);
    model.addAttribute("QrCode", QrCode);
    return "login";
}

当拜访 “localhost:8080/login/getQrCodeImg” 时,PC 端服务器会生成一个 uuid 和一个 LoginTicket 对象,而后将 uuid 作为 key,LoginTicket 对象作为 value 存入到 Redis 服务器中(设置其过期工夫为 5 分钟)。接着将该 uuid 拼接到 URL 中(此 URL 即为手机端扫码后所拜访的网址),并应用开源工具类 Hutool 中的 QrCodeUtil 生成二维码图片。

对于 Hutool 的应用能够参考 https://hutool.cn/。

  1. 扫描二维码
@RequestMapping(path = "/scan/{uuid}/{userId}", method = RequestMethod.GET)
public String scanQrCodeImg(Model model, @PathVariable("uuid") String uuid, @PathVariable("userId") String userId) {
   // 判断用户是否胜利扫码
   boolean scanned = loginService.scanQrCodeImg(uuid, userId);
   // 返回扫码信息
   model.addAttribute("scanned", scanned);
   model.addAttribute("uuid", uuid);
   model.addAttribute("userId", userId);
   return "scan";
}

二维码中封装的信息是一个 URL,手机端扫描二维码时,会拜访该 URL 所代表的的网址。此时申请中会携带手机端用户的 token 和 uuid,token 用来确认用户的身份。在上述代码中,咱们简化手机端的操作,间接传入 userId,利用 userId 代替 token 来辨认用户。服务器(此处为手机端服务器,但咱们应用 PC 端服务器模拟手机端服务器)首先依据 userId 查问用户是否曾经登录,如果 Redis 中存在该用户的信息,则示意用户曾经登录。如果用户未登录或二维码曾经过期,则扫码失败,返回 false;否则将 LoginTicket 对象的状态设置为 1,示意曾经扫码,期待确认。

  1. 确认登录
@RequestMapping(path = "/confirm/{uuid}/{userId}", method = RequestMethod.GET)
@ResponseBody
public Response confirmLogin(@PathVariable("uuid") String uuid, @PathVariable("userId") String userId) {
   // 判断用户是否胜利确认
   boolean logged = loginService.confirmLogin(uuid, userId);
   String msg = logged ? "登录胜利!" : "二维码已过期!";
   return Response.createResponse(msg, logged);
}

同扫码申请一样,确认登录时也应用 userId 代替 token 进行身份辨认。手机端(在浏览器中模拟手机端操作)发送确认申请时,服务器首先查看二维码是否过期(按理来说扫码后再确认,二维码应该不会过期)。如果确认胜利,那么将 LoginTicket 对象的状态设置为 2,并将 userId 置为以后用户的 id(或者 userId 在 scan 在扫码申请就应该设置为用户 id?)。

  1. 轮询
@RequestMapping(path = "/getQrCodeState/{uuid}", method = RequestMethod.GET)
@ResponseBody
public Response getQrCodeState(@PathVariable("uuid") String uuid) throws InterruptedException {JSONObject data = new JSONObject();
   // 查看二维码是否过期
   String redisKey = CommonUtil.getTicketKey(uuid);
   LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
   if (loginTicket == null) {data.put("status", -1);
      return Response.createResponse("二维码已过期!", data);
   }
   // 查看 status
   int status = loginTicket.getStatus();
   data.put("status", status);
   if (status == 2) {
      // 用户已确认登录
      String userId = loginTicket.getUserId();
      User user = userService.getLoggedUser(userId);
      if (user != null) {
         // 生成 token
         String token = TokenUtil.buildToken(userId, user.getUserName());
         data.put("token", token);
         return Response.createResponse(null, data);
      }
      return Response.createErrorResponse("无用户信息!");
   }
   // 2s 轮询一次
   Thread.sleep(2000);
   String msg = status == 0 ? null : "已扫描, 期待确认";
   return Response.createResponse(msg, data);
}

轮询的逻辑其实就是依据 uuid 查看 LoginTicket 对象的状态,如果 LoginTicket 对象为空,示意二维码曾经过期;如果 status 为 0,示意期待扫码;如果 status 为 1,示意已扫码,期待确认;如果 status 为 2,示意已确认登录。当检测到用户确认登录后,服务器为用户生成 token(此 token 用于 PC 端服务器辨认用户身份),而后将 token 返回给前端。留神,上述代码生成 token 之前,调用了 UserService 中的 getLoggedUser 办法来查问用户的身份信息,在此 demo 中,为了简化操作,但凡须要获取用户信息的中央咱们都应用该办法去获取,如后面手机端服务器(其实也是在 PC 端模仿)依据 token(为了简化,实际上为 userId)查问用户信息时也调用了该办法。还有一点须要留神,最初一步的 token 也能够应用 cookie 来代替,这样兴许会更加简略,这里学习了一下 JWT,所以采纳 token(应用 token 拜访时,token 应该怎么保留呢,苦恼!!!!!+10086)。getLoggedUser 办法其实就是检测 Redis 中有无用户的身份信息,代码如下:

public User getLoggedUser(String userId) {String redisKey = CommonUtil.getUserKey(userId);
   return (User) redisTemplate.opsForValue().get(redisKey);
}
Service 层

Service 层对应的代码如下:

@Service
public class LoginService {

   private final int WAIT_EXPIRED_SECONDS = 60 * 5;

   private final int LOGIN_EXPIRED_SECONDS = 3600 * 24;

   @Autowired
   private RedisTemplate redisTemplate;

   public String createQrImg() {
      // 生成 loginTicket
      String uuid = CommonUtil.generateUUID();
      LoginTicket loginTicket = new LoginTicket();
      loginTicket.setUuid(uuid);
      loginTicket.setStatus(0);
      // 存入 redis
      String redisKey = CommonUtil.getTicketKey(loginTicket.getUuid());
      redisTemplate.opsForValue().set(redisKey, loginTicket, WAIT_EXPIRED_SECONDS, TimeUnit.SECONDS);
      return uuid;
   }

   public boolean scanQrCodeImg(String uuid, String userId) {String ticketKey = CommonUtil.getTicketKey(uuid);
      String userKey = CommonUtil.getUserKey(userId);
      LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(ticketKey);
      User user = (User) redisTemplate.opsForValue().get(userKey);
      // 检测用户是否登录以及二维码是否过期
      if (user == null || loginTicket == null) {return false;} else {
         // 将 status 置为 1
         loginTicket.setStatus(1);
         redisTemplate.opsForValue().set(ticketKey, loginTicket, redisTemplate.getExpire(ticketKey, TimeUnit.SECONDS), TimeUnit.SECONDS);
      }
      return true;
   }

   public boolean confirmLogin(String uuid, String userId) {String redisKey = CommonUtil.getTicketKey(uuid);
      LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
      boolean logged = true;
      if (loginTicket == null) {logged = false;} else {
         // 将 userId 置为用户 id, 并将 status 置为 2
         loginTicket.setUserId(userId);
         loginTicket.setStatus(2);
         redisTemplate.opsForValue().set(redisKey, loginTicket, LOGIN_EXPIRED_SECONDS, TimeUnit.SECONDS);
      }
      return logged;
   }
}

前端的几个 xx.html 文件的代码写得不太好,大家间接看源码吧,源码我会放在文末。

成果演示

执行程序前,咱们须要在 Redis 中存储以后用户的信息,示意用户在手机端曾经登录,其中 key 的格局为 user:userId,value 为 User 对象。比方在演示前,咱们在 Redis 中存储了 userId 为 “1” 的用户 “Join 同学 ”。

演示动图如下:

待改良

  1. 整个流程中应该存在手机端服务器和 PC 端服务器,但为了简化操作,咱们利用 PC 端模拟手机端,比方扫码和确认申请应该由手机端服务器解决,而程序中咱们间接在 PC 端拜访对应的 Controller;
  2. 查看扫码状态时采纳了轮询的形式,或者能够采纳 Websocket;
  3. “ 手机端 ” 验证 “token” 时,咱们应用 userId 来简化操作;
  4. 最初一步咱们将 token 返回给了前端,前端发送申请时,须要在 header 中寄存 token,但问题是 token 应该如何保留呢?之后须要解决此问题;
  5. 未学习过前端,所以代码不怎么标准。
欢送批评指正,源码见扫码登录

正文完
 0