Spring Boot Reimplementation of WeChat‑Style QR Login: Only Four Core States

The article walks through building a secure QR‑code login flow with Spring Boot, explaining why the QR should contain only a short‑lived random identifier, how to model four login states in Redis, avoid embedding JWTs, use WebSocket for instant updates, and enforce strict one‑time exchange checks.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Spring Boot Reimplementation of WeChat‑Style QR Login: Only Four Core States

1. Do Not Put JWT Directly in QR Code

A common mistake is to embed a generated JWT in the QR URL, e.g. String token = jwtService.generateToken(userId); and then https://example.com/login?token=xxxxx. Anyone who captures the QR (screenshot, photo, etc.) can obtain the token and log in, so the QR must never contain authenticated credentials.

Instead, the QR should only carry a short‑lived, random, one‑time qrId, such as https://m.example.com/qr-login?qrId=K6Tu7Rby95z. This identifier merely represents a browser waiting for login and holds no user identity, JWT, session, or permissions.

There is a browser waiting for login; it has no user identity.

2. The Whole QR Login Has Only Four Core States

The Redis‑backed state machine is defined as:

public enum QrLoginStatus {
    WAITING,
    SCANNED,
    CONFIRMED,
    EXPIRED
}

The flow is:

Web generates QR → WAITING → (user scans) → SCANNED → (user confirms) → CONFIRMED → Session / JWT

If no action occurs within two minutes, WAITING → EXPIRED. The SCANNED state cannot directly become a successful login because scanning and user consent are distinct actions.

3. Generating the QR Code in Java

Dependencies:

Java 21
Spring Boot 4.1.0
Redis
Spring WebSocket
ZXing 3.5.4

Add ZXing:

com.google.zxing:core:3.5.4
com.google.zxing:javase:3.5.4

Generate a truly random qrId:

public String randomToken() {
    byte[] bytes = new byte[32];
    SecureRandom random = new SecureRandom();
    random.nextBytes(bytes);
    return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}

String qrId = randomToken();
String url = "https://m.example.com/qr-login?qrId=" + qrId;

Encode the URL with ZXing:

BitMatrix matrix = new QRCodeWriter().encode(url, BarcodeFormat.QR_CODE, 300, 300);
BufferedImage image = MatrixToImageWriter.toBufferedImage(matrix);
The QR image itself carries no login permission; it is only an entry point.

4. Adding a Browser Token

When the browser requests POST /api/qr-login/create, the server generates both qrId and a browserToken and returns them (along with TTL and QR image URL). The browserToken never appears in the QR image.

{
  "qrId": "QR_xxxxx",
  "browserToken": "BT_xxxxx",
  "expiresIn": 120,
  "qrCodeUrl": "/api/qr-login/QR_xxxxx/image"
}

Redis stores the session:

{
  "status": "WAITING",
  "browserTokenHash": "...",
  "userId": null,
  "createdAt": "...",
  "expiresAt": "..."
}

5. Scanning the QR Code

Assuming the user is already logged into the mobile app (e.g., userId = 10086), the scan request is: POST /api/qr-login/{qrId}/scan Controller:

@PostMapping("/{qrId}/scan")
public void scan(@PathVariable String qrId, Authentication authentication) {
    Long userId = loginUserId(authentication);
    qrLoginService.scan(qrId, userId);
}

Service logic:

if (session.status() != QrLoginStatus.WAITING) {
    throw new IllegalStateException("二维码状态错误");
}
session.setUserId(userId);
session.setStatus(QrLoginStatus.SCANNED);
save(session);

The browser UI changes from “请扫码” to “已扫码,请在手机确认”.

6. Real‑Time Notification with WebSocket

Instead of polling, a WebSocket endpoint /ws/qr-login/{qrId} is opened. When the mobile app scans, it sends {"type":"SCANNED"} through the socket, causing the browser to display the confirmation prompt instantly.

After the user clicks “确认登录”, the mobile app posts: POST /api/qr-login/{qrId}/confirm Server updates the state to CONFIRMED and pushes {"type":"CONFIRMED"} via WebSocket.

WebSocket pushes are still not JWTs; they only tell the browser that it may now exchange the login.

7. Exchanging for a Real Session

The browser, upon receiving CONFIRMED, calls: POST /api/qr-login/exchange Payload:

{
  "qrId": "QR_xxxxx",
  "browserToken": "BT_xxxxx"
}

Backend checks four conditions:

1. QR still exists
2. Status is CONFIRMED
3. browserToken matches
4. QR has never been exchanged before

If all pass, it retrieves userId, creates a real session/JWT, and calls consumeQrLoginSession(qrId) to mark the QR as used.

The QR can be exchanged only once.

Atomic consumption can be implemented with Redis Lua scripts or other atomic operations to avoid race conditions.

8. Security Hardening Before Production

QR TTL of 1–2 minutes (e.g., 120 seconds).

Require explicit mobile confirmation; scanning alone does not log in.

Display device information (browser, OS, location, timestamp) on the confirmation page.

Never place sensitive credentials (JWT, refresh token, userId, permissions) inside the QR.

Destroy the entire temporary state (qrId, browserToken, userId, status) immediately after successful exchange.

9. Conclusion

Implementing QR‑code login reveals a compact yet comprehensive workflow involving random token generation, a Redis‑backed one‑time state machine, authentication, one‑time credential exchange, WebSocket for instant feedback, expiration handling, concurrency control, and strict security boundaries. Remember: the QR code is not a login credential; it is merely a short‑lived authorization identifier.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRedisSpring BootwebsocketSecurityZXingQR Login
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.