SAS-QrCode-Login
实现原理
- 打开网页,发起授权申请/未登录被重定向到登录页面
- 选择二维码登录,页面从后端请求二维码
- 页面渲染二维码图片,并轮询请求,获取二维码的状态
- 事先登录过APP的手机扫描二维码,然后APP请求服务器端的API接口,把用户认证信息传递到服务器中
- 后端收到APP的请求后更改二维码状态,并把用户认证信息写入session
- 页面得到扫码确认的响应,并跳转回之前未登录的地址
在这个流程中 用户认证信息写入session 后前端在重定向时能获取到认证信息是因为现在的认证服务引入了spring session data redis依赖,并且在application.yml中配置了server.servlet.session.cookie.domain: cdhttp.cn属性(演示环境域名),这里是指定spring session的顶级域名,在该域名下的子域名服务共享session,如果你是在开发阶段可以配置为server.servlet.session.cookie.domain: 127.0.0.1,这时候前端访问使用127.0.0.1:5173,认证服务使用127.0.0.1:8080,端口可以不一样,但是认证服务和前端必须使用同一域名。
二、代码实现
1. 后端实现
1. 引入二维码依赖
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
hutool-captcha改为hutool-all
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
2. 添加二维码登录接口
提供四个接口,分别处理生成二维码、轮询、app扫码和app确认登录的逻辑。
package com.example.controller;
import com.example.model.Result;
import com.example.model.request.qrcode.QrCodeLoginConsentRequest;
import com.example.model.request.qrcode.QrCodeLoginScanRequest;
import com.example.model.response.qrcode.QrCodeGenerateResponse;
import com.example.model.response.qrcode.QrCodeLoginFetchResponse;
import com.example.model.response.qrcode.QrCodeLoginScanResponse;
import com.example.service.IQrCodeLoginService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 二维码登录接口
*
* @author vains
*/
@RestController
@AllArgsConstructor
@RequestMapping("/qrCode")
public class QrCodeLoginController {
private final IQrCodeLoginService iQrCodeLoginService;
@GetMapping("/login/generateQrCode")
public Result<QrCodeGenerateResponse> generateQrCode() {
// 生成二维码
return Result.success(iQrCodeLoginService.generateQrCode());
}
@GetMapping("/login/fetch/{qrCodeId}")
public Result<QrCodeLoginFetchResponse> fetch(@PathVariable String qrCodeId) {
// 轮询二维码状态
return Result.success(iQrCodeLoginService.fetch(qrCodeId));
}
@PostMapping("/scan")
public Result<QrCodeLoginScanResponse> scan(@RequestBody QrCodeLoginScanRequest loginScan) {
// app 扫码二维码
return Result.success(iQrCodeLoginService.scan(loginScan));
}
@PostMapping("/consent")
public Result<String> consent(@RequestBody QrCodeLoginConsentRequest loginConsent) {
// app 确认登录
iQrCodeLoginService.consent(loginConsent);
return Result.success();
}
}
3. yml中放行前端访问的接口
添加匹配规则/qrCode/login/**
custom:
# 自定义认证配置
security:
# 登录页面路径
login-url: http://k7fsqkhtbx.cdhttp.cn/login
# 授权确认页面路径
consent-page-uri: http://k7fsqkhtbx.cdhttp.cn/consent
# 设备码验证页面
device-activate-uri: http://k7fsqkhtbx.cdhttp.cn/activate
# 设备码验证成功页面
device-activated-uri: http://k7fsqkhtbx.cdhttp.cn/activated
# 不需要认证的地址
ignore-uri-list: assets/**, /webjars/**, /login, /getCaptcha, /getSmsCaptcha, /error, /oauth2/consent/parameters, /test03, /favicon.ico, /qrCode/login/**
4. 编写二维码登录服务接口
编写登录service接口
package com.example.service;
import com.example.model.request.qrcode.QrCodeLoginConsentRequest;
import com.example.model.request.qrcode.QrCodeLoginScanRequest;
import com.example.model.response.qrcode.QrCodeGenerateResponse;
import com.example.model.response.qrcode.QrCodeLoginFetchResponse;
import com.example.model.response.qrcode.QrCodeLoginScanResponse;
/**
* 二维码登录服务接口
*
* @author vains
*/
public interface IQrCodeLoginService {
/**
* 生成二维码
*
* @return 二维码
*/
QrCodeGenerateResponse generateQrCode();
/**
* 扫描二维码响应
*
* @param loginScan 二维码id
* @return 二维码信息
*/
QrCodeLoginScanResponse scan(QrCodeLoginScanRequest loginScan);
/**
* 二维码登录确认入参
*
* @param loginConsent 二维码id
*/
void consent(QrCodeLoginConsentRequest loginConsent);
/**
* web端轮询二维码状态处理
*
* @param qrCodeId 二维码id
* @return 二维码信息
*/
QrCodeLoginFetchResponse fetch(String qrCodeId);
}
5. 编写生成二维码响应类
生成二维码图片时返回二维码id和图片
package com.example.model.response.qrcode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 生成二维码响应
*
* @author vains
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QrCodeGenerateResponse {
/**
* 二维码id
*/
private String qrCodeId;
/**
* 二维码base64值(这里响应一个链接好一些)
*/
private String imageData;
}
6. 编写web前端轮询二维码状态出参
前端根据二维码id轮询二维码状态时返回二维码状态,如果已扫 描也会返回扫描者的头像、昵称。
package com.example.model.response.qrcode;
import lombok.Data;
import java.util.Set;
/**
* web前端轮询二维码状态出参
*
* @author vains
*/
@Data
public class QrCodeLoginFetchResponse {
/**
* 二维码状态
* 0:待扫描,1:已扫描,2:已确认
*/
private Integer qrCodeStatus;
/**
* 是否已过期
*/
private Boolean expired;
/**
* 扫描人头像
*/
private String avatarUrl;
/**
* 扫描人昵称
*/
private String name;
/**
* 待确认scope
*/
private Set<String> scopes;
/**
* 跳转登录之前请求的接口
*/
private String beforeLoginRequestUri;
/**
* 跳转登录之前请求参数
*/
private String beforeLoginQueryString;
}
7. 编写扫描二维码入参
app扫描二维码时传入二维码id
package com.example.model.request.qrcode;
import lombok.Data;
/**
* 扫描二维码入参
*
* @author vains
*/
@Data
public class QrCodeLoginScanRequest {
/**
* 二维码id
*/
private String qrCodeId;
}