一文带你了解二维码扫码的全部用途
扫描二维码是如今非常常见的一个功能。
用户可以通过扫描二维码,来实现登录、网页跳转、信息获取、授权等功能。
下面这个文章来详细的说一些跟二维码扫码有关的实例。
二维码扫码登录(本地代码实现)
扫码登录,我们首先来梳理一下具体的逻辑。
- 移动端发起扫码请求,服务端生成二维码,缓存二维码ID及初始状态,并将二维码以Base64格式返回给前端用于展示。
- PC端页面定时轮询服务端,检查二维码状态。
- 用户使用移动端扫码后,调用扫码接口,将移动端Token与二维码ID一并提交至服务端。服务端校验后生成临时Token,并将二维码状态更新为"已扫码"。
- PC端继续轮询,检测二维码状态是否变为"已扫码"。
- 用户在移动端确认登录,将临时Token与二维码ID提交至服务端进行确认。服务端校验临时Token无误后,将二维码状态更新为"已确认",并生成正式的有效期Token。
- PC端检测到二维码状态为"已确认" ,获取有效Token后完成登录流程。
我们来看流程图

之后我们直接来设计。
首先来设计数据库
sql
CREATE TABLE u_qrlogin (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
uuid VARCHAR(64) NOT NULL UNIQUE COMMENT '唯一标识',
device VARCHAR(64) COMMENT '设备号',
token TEXT COMMENT 'JWT令牌',
status VARCHAR(32) COMMENT '扫码状态',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
);
之后就是对应的dto
swift
package com.xiaou.qrcodelogin.dto;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 对应数据库表:u_qrlogin
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfoDTO {
/**
* 自增主键
*/
@TableId
private Long id;
/**
* 唯一标识
*/
private String uuid;
/**
* 设备号
*/
private String device;
/**
* JWT令牌
*/
private String token;
/**
* 扫码状态
*/
private String status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
之后是我们要向前端返回的vo
arduino
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseVO {
/**
* 唯一标识
*/
private String uuid;
/**
* 登录二维码
*/
private String qrcode;
/**
* jwt令牌
*/
private String token;
/**
* 扫码状态
*/
private String status;
}
之后我们需要一个枚举类来记录二维码的状态
scss
@Getter
public enum LoginStatusEnum {
UNSCANNED("未扫描"),
SCANNED("已扫描"),
CONFIRMED("已确认");
private String desc;
LoginStatusEnum(String desc) {
this.desc = desc;
}
}
之后需要一些工具类的辅助。
这里用到了jjwt以及google.zxing来进行二维码的辅助生成。
java
package com.xiaou.qrcodelogin.utils;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
public class JwtUtil {
private static final String SECRET_KEY = "9dad5e7e-bcb7-438f-b39e-ad8c67814915";
public static String generateAuthToken(String uuid) {
// 生成JWT或其他形式令牌
return Jwts.builder()
.setSubject(uuid)
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期
.signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes(StandardCharsets.UTF_8))
.compact();
}
}
java
package com.xiaou.qrcodelogin.utils;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Hashtable;
public class QRCodeUtil {
public static String generateQRCode(String content, int width, int height)
throws WriterException, IOException {
Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix matrix = new MultiFormatWriter().encode(
content, BarcodeFormat.QR_CODE, width, height, hints);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(matrix, "PNG", outputStream);
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
}
}
之后来具体实现。
首先来看二维码的生成接口
scss
/**
* 生成二维码
*/
@GetMapping("/generate")
public ResponseEntity<ResponseVO> generateQRCode() throws Exception {
String uuid = UUID.randomUUID().toString();
String base64QR = QRCodeUtil.generateQRCode(uuid, 200, 200);
LocalDateTime now = LocalDateTime.now();
LoginInfoDTO loginInfoDTO = LoginInfoDTO.builder()
.uuid(uuid)
.status(LoginStatusEnum.UNSCANNED.name())
.createTime(now)
.updateTime(now)
.build();
cache(loginInfoDTO);
ResponseVO vo = ResponseVO.builder()
.uuid(uuid)
.qrcode("data:image/png;base64," + base64QR)
.build();
log.info("✅ 生成二维码成功 uuid: {}", uuid);
return ResponseEntity.ok(vo);
}
相当于我们构建了一个LoginInfoDTO的对象,放到了redis里面。之后进行了一个生成。返回的就是一个二维码。
typescript
private void cache(LoginInfoDTO loginInfoDTO) {
stringRedisTemplate.opsForValue().set(
cacheKey(loginInfoDTO.getUuid()),
JSONObject.toJSONString(loginInfoDTO),
2,
TimeUnit.MINUTES
);
}
之后我们根据这个二维码,要有以下的一些接口。
首先是检测状态的接口:
scss
/**
* 检查扫码状态
*/
@GetMapping("/check/{uuid}")
public ResponseEntity<?> checkStatus(@PathVariable String uuid) {
LoginInfoDTO loginInfoDTO = getCache(uuid);
if (loginInfoDTO == null) {
return ResponseEntity.status(410).body("二维码已过期");
}
String token = "";
if (LoginStatusEnum.CONFIRMED.name().equals(loginInfoDTO.getStatus())) {
token = JwtUtil.generateAuthToken(uuid);
loginInfoDTO.setToken(token);
loginInfoDTO.setUpdateTime(LocalDateTime.now());
cache(loginInfoDTO);
}
ResponseVO vo = ResponseVO.builder()
.token(token)
.status(loginInfoDTO.getStatus())
.build();
log.info("🧐 校验二维码状态 uuid: {}, 状态: {}", uuid, loginInfoDTO.getStatus());
return ResponseEntity.ok(vo);
}
我们从redis里面获得之前创建的loginInfoDTO。这里是如果状态是已经确认的我们就设置他的token以及UpdateTime
typescript
private LoginInfoDTO getCache(String uuid) {
String s = stringRedisTemplate.opsForValue().get(cacheKey(uuid));
return s == null ? null : JSONObject.parseObject(s, LoginInfoDTO.class);
}
之后是手机端扫码的接口
less
/**
* 扫码(手机端)
*/
@PostMapping("/scan/{uuid}")
public ResponseEntity<?> scanQrCode(@PathVariable String uuid) {
LoginInfoDTO loginInfoDTO = getCache(uuid);
if (loginInfoDTO == null) {
return ResponseEntity.status(410).body("二维码已过期");
}
loginInfoDTO.setStatus(LoginStatusEnum.SCANNED.name());
loginInfoDTO.setUpdateTime(LocalDateTime.now());
cache(loginInfoDTO);
log.info("📱 扫码成功 uuid: {}", uuid);
return ResponseEntity.ok().build();
}
就是当我们调用这个接口的时候,给他一个已扫描的信息。
同理登录也是这样
less
/**
* 确认登录(手机端)
*/
@PostMapping("/confirm/{uuid}")
public ResponseEntity<?> confirm(@PathVariable String uuid) {
LoginInfoDTO loginInfoDTO = getCache(uuid);
if (loginInfoDTO == null) {
return ResponseEntity.status(410).body("二维码已过期");
}
loginInfoDTO.setStatus(LoginStatusEnum.CONFIRMED.name());
loginInfoDTO.setUpdateTime(LocalDateTime.now());
cache(loginInfoDTO);
log.info("🔒 确认登录成功 uuid: {}", uuid);
return ResponseEntity.ok().build();
}
在经过扫描和确认登录后。我们在去检测checkStatus她的状态的时候,就已经是登录成功的状态了。
至此我们的流程就结束了。下面来看具体的演示。
首先是生成二维码的接口

我们首先不扫码的时候,检查一下状态。

之后我们来进行一个扫码模拟操作

继续检查可以看到是已经扫码的状态

之后我们确认登录

状态就是登录的状态,并且有token了

这就是一个简单的模拟。
至此最简单的一个二维码扫码登录就完成了。我们可以根据实际开发来对这些接口进行一个更详细的补充
二维码扫码登录(第三方登录实现微信)
当然,如今的项目,大多数的项目已经不会自己去开发一个扫码的功能,而是去借助第三方来实现。最常见的有微信扫一扫登录,qq扫一扫登录等。
下面我讲用代码来演示如何去对接第三方的登录。
为什么要用第三方登录?首先,例如微信,qq这种应用,是每一个用户基本上都有的功能。并且通过第三方登录可以直接去获取一些信息,方便用户进行一个快速的注册。
对接微信公众号流程
-
在微信公众平台填写服务器接口配置
- 配置服务器地址(URL)、Token、消息加解密密钥(EncodingAESKey)等信息。
-
服务器提供两个接口
- GET接口:用于微信服务器进行首次验证(验签)。
- POST接口:用于接收用户发送的消息或事件推送。
-
微信服务器调用 GET 接口进行验签操作
- 验证是否是合法的服务器地址,根据
signature
,timestamp
,nonce
进行校验。
- 验证是否是合法的服务器地址,根据
-
微信服务器调用 POST 接口进行消息推送/事件回调
- 用户操作触发事件(如关注、扫码等)后,微信服务器将消息以POST方式发送给你的服务器。
-
通过调用微信接口获取二维码
- 例如使用 生成带参数二维码接口。
-
获取二维码需要提供 ticket 凭证
- 微信返回二维码
ticket
,可根据该ticket
获取二维码图片。
- 微信返回二维码
-
获取 ticket 凭证前需先获取 accessToken
accessToken
是调用所有微信开放接口的全局唯一凭证,有效期通常为 2 小时。
-
用户使用微信扫码
- 用户扫码后,微信将扫码事件通过 POST 接口推送给你的服务器,可进行后续业务处理。
下面我们来分步来讲
首先先看官方的一个流程图

之后我们来实操。
首先是原理讲解的实操。
第一我们要
注册应用:在微信开放平台注册应用,获取AppID 和AppSecret。
配置回调域名:设置授权后的回调地址(redirect_uri),需通过微信审核
第二:
前面已经提到过获取微信二维码需要先获取到 accessToken 全局唯一接口调用凭据,再使用accessToken才能获取到 ticket,最后使用 ticket 获取二维码扫码登录
所以需要获取 accessToken 全局唯一接口调用凭据
https请求方式: GET api.weixin.qq.com/cgi-bin/tok...
开始开发 / 获取 Access token (qq.com)
该接口会返回 accessToken,并返回 accessToken 的有效期
第三:
获取 ticket 凭证
微信获取 ticket 以及使用 ticket 获取二维码文档
临时二维码请求说明:http请求方式: POST URL: api.weixin.qq.com/cgi-bin/qrc... POST数据格式:json POST数据例子:{"expire_seconds": 604800, "action_name": "QR_SCENE", "action_info": {"scene": {"scene_id": 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数:{"expire_seconds": 604800, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
永久二维码请求说明:http请求方式: POST URL: api.weixin.qq.com/cgi-bin/qrc... POST数据格式:json POST数据例子:{"action_name": "QR_LIMIT_SCENE", "action_info": {"scene": {"scene_id": 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数: {"action_name": "QR_LIMIT_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
前面我们已经获取了 accessToken 唯一接口调用凭据,现在即可使用accessToken根据不同的场景(例如需要获取临时二维码或者永久二维码的不同请求)获取得到可以获取二维码的ticket
第四就是去请求二维码了
获取微信二维码
HTTP GET请求(请使用https协议)mp.weixin.qq.com/cgi-bin/sho...

成功扫码后微信会调用前面配置的回调接口 ,回调接口会传递给服务器openid参数,openid唯一标识一个用户,在回调接口中可以将ticket和openid进行绑定缓存,后续可通过ticket获取openid来判断用户是否登录
vue实现完整版
之后我们来看一种vue来实现的完整版。
首先引入插件
javascript
// 安装
npm install vue-wxlogin --save-dev
// js引入
import wxlogin from 'vue-wxlogin'
components: { wxlogin }
使用插件
ini
<wxlogin
:appid="appid"
:scope="'snsapi_login'" // 网页固定的
:theme="'black'"
:redirect_uri="redirect_uri"
:href='bast64css'
>
</wxlogin>
// data
<wxlogin
:appid="appid"
:scope="'snsapi_login'" // 网页固定的
:theme="'black'"
:redirect_uri="redirect_uri"
:href='bast64css'
>
</wxlogin>
// data
bast64css: 'data:text/css;base64,LmltcG93ZXJCb3ggLnFyY29kZSB7d2lkdGg6IDIwMHB4O2hlaWdodDoyMDBweH0NCi5pbXBvd2VyQm94IC5pbmZvIHt3aWR0aDogMjAwcHh9DQouc3RhdHVzX2ljb24ge2Rpc3BsYXk6IG5vbmV9DQouaW1wb3dlckJveCAuc3RhdHVzIHt0ZXh0LWFsaWduOiBjZW50ZXI7fQ0KLmltcG93ZXJCb3ggLnRpdGxlIHtkaXNwbGF5OiBub25lfQ0KaWZyYW1lIHtoZWlnaHQ6IDMyMnB4O30NCg==',
appid: 'xxx', // 后端提供
redirect_uri: 'xxx', // 后端提供
结果:这样微信二维码就会显示在自己写的网页上
扫描后,页面的url会给一个带code的地址 ,去获取code
下面是一些参数的说明

ini
if (window.location.href.indexOf('code') >= 0) {
let code = window.location.href.split('?')[1];
code = code.substring(5, code.indexOf('&'));
this.wechatcode = code
this.wechatLogin()
}
这个是用插件,如果不用插件我们可以
xml
<!--微信授权登录按钮-->
<img src="@/assets/images/weixin.png" /><a style="line-height: 60px;height: 60px; margin: 0 5px;" type="text" @click="handLoginByWx">微信扫码登录</a>
配置登录相关参数,跳转微信登录二维码授权页面
ini
// 跳转微信登录二维码授权页面
handLoginByWx() {
// 重定向地址重定到当前页面,在路径获取code
const hrefUrl = window.location.href
// 判断是否已存在code
if (!this.code) {
// 不存在,配置相关微信登录参数(主要是授权页面地址,appID,回调地址)
window.location.href = `
https://open.weixin.qq.com/connect/qrconnect
?appid=APPID
&redirect_uri=${encodeURIComponent(hrefUrl)}
&response_type=code
&scope=snsapi_login
`
}
}
java实现完整版
这里我们用到了微信测试公众号,如果实际开发需要对自己的公众号进行配置审核。微信测试号:mp.weixin.qq.com/debug/cgi-b...
首先是代码架构
bash
├── config
│ │ └── JwtFilter.java # JWT 身份认证拦截器
│ ├── controller
│ │ ├── WeixinServerController.java # 微信服务端调用接口
│ │ └── WeixinUserController.java # 浏览器调用接口
│ ├── model
│ │ ├── ApiResult.java
│ │ ├── ReceiveMessage.java # 微信消息封装类
│ │ └── WeixinQrCode.java # 微信二维码 Ticket 封装类
│ ├── service
│ │ ├── WeixinUserService.java # 微信调用处理类
│ │ └── impl
│ │ └── WeixinUserServiceImpl.java
│ └── util
│ ├── AesUtils.java # AES 加密工具类
│ ├── ApiResultUtil.java
│ ├── HttpUtil.java # HTTP 工具类
│ ├── JwtUtil.java # JWT 工具类
│ ├── KeyUtils.java
│ ├── WeixinApiUtil.java # 微信 API 工具类,如获取 AccessToken
│ ├── WeixinMsgUtil.java # 微信消息工具类
│ ├── WeixinQrCodeCacheUtil.java # 微信二维码Ticket缓存
│ └── XmlUtil.java
WeixinServerController
和 WeixinUserController
暴漏了三个 API。
/weixin/check
:用于对接微信服务端,接收微信服务端的调用。
/user/qrcode
: 用于获取二维码图片信息
/user/login/qrcode
: 用于校验是否扫描成功,成功则返回身份认证后的 JWT 字符串。
验证签名实现
typescript
// com.wdbyte.weixin.service.impl.WeixinUserServiceImpl.java
@Value("${weixin.token}")
private String token;
@Override
public void checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[] {token, timestamp, nonce};
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (String str : arr) {
content.append(str);
}
String tmpStr = DigestUtils.sha1Hex(content.toString());
if (tmpStr.equals(signature)) {
log.info("check success");
return;
}
log.error("check fail");
throw new RuntimeException("check fail");
}
获取 Access Token
获取带有 Ticket 的公众号二维码之前,需要先获取公众号的 Access Token,这是调用微信公众号所有接口的前提。 Access Token 每日调用次数有限,应该进行缓存。
typescript
// com.wdbyte.weixin.util.WeixinApiUtil.java
@Value("${weixin.appid}")
public String appId;
@Value("${weixin.appsecret}")
public String appSecret;
private static String ACCESS_TOKEN = null;
private static LocalDateTime ACCESS_TOKEN_EXPIRE_TIME = null;
/**
* 获取 access token
*
* @return
*/
public synchronized String getAccessToken() {
if (ACCESS_TOKEN != null && ACCESS_TOKEN_EXPIRE_TIME.isAfter(LocalDateTime.now())) {
return ACCESS_TOKEN;
}
String api = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret="
+ appSecret;
String result = HttpUtil.get(api);
JSONObject jsonObject = JSON.parseObject(result);
ACCESS_TOKEN = jsonObject.getString("access_token");
ACCESS_TOKEN_EXPIRE_TIME = LocalDateTime.now().plusSeconds(jsonObject.getLong("expires_in") - 10);
return ACCESS_TOKEN;
}
生成登录二维码
使用 Access Token 获取二维码 Ticket 用来换取二维码图片。
arduino
// com.wdbyte.weixin.util.WeixinApiUtil.java
private static String QR_CODE_URL_PREFIX = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
/**
* 二维码 Ticket 过期时间
*/
private static int QR_CODE_TICKET_TIMEOUT = 10 * 60;
/**
* 获取二维码 Ticket
*
* https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
*
* @return
*/
public WeixinQrCode getQrCode() {
String api = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + getAccessToken();
String jsonBody = String.format("{\n"
+ " "expire_seconds": %d,\n"
+ " "action_name": "QR_STR_SCENE",\n"
+ " "action_info": {\n"
+ " "scene": {\n"
+ " "scene_str": "%s"\n"
+ " }\n"
+ " }\n"
+ "}", QR_CODE_TICKET_TIMEOUT, KeyUtils.uuid32());
String result = HttpUtil.post(api, jsonBody);
log.info("get qr code params:{}", jsonBody);
log.info("get qr code result:{}", result);
WeixinQrCode weixinQrCode = JSON.parseObject(result, WeixinQrCode.class);
weixinQrCode.setQrCodeUrl(QR_CODE_URL_PREFIX + URI.create(weixinQrCode.getTicket()).toASCIIString());
return weixinQrCode;
}
class WeixinQrCode {
private String ticket;
private Long expireSeconds;
private String url;
private String qrCodeUrl;
}
其中 Ticket 就是二维码凭证,用户扫码后微信会把此 Ticket 回调给网站服务端。可以在下面的链接后面拼上 Ticket 换取二维码图片。
完整的源码在xiaou61/xiaou-easyproject-backend: 后端通用接口代码片段汇总 (github.com)

二维码活码实现
二维码还有一个功能,比如说,你想要二维码里面的东西是可以后台控制的。
二维码"活码"的核心思路是:二维码的内容不直接指向最终内容,而是指向我们自建服务器的一个中间跳转地址(一般是带唯一ID的短链接),然后通过后台动态控制跳转到什么内容。
我们可以用 Spring Boot 来构建这个"活码"系统比如文字、图片、网页链接等。
这个过于简单了所以我只给一个数据库。
sql
CREATE TABLE dynamic_qr (
id VARCHAR(50) PRIMARY KEY, -- 唯一短码(二维码内容中包含的部分)
type VARCHAR(20), -- 类型:text、url、html、image
content TEXT, -- 存储实际内容或地址
create_time DATETIME,
update_time DATETIME
);
就是一个简单的crud。
或者说通过一些别的东西来实现。
例如草料二维码的活码功能等。