一、概述
1、扫码登录介绍
二维码扫描登录原理
二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情:告诉系统我是谁,以及向系统证明我是谁。
比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁; 比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;
2、扫码登录原理
- PC 端发送 “扫码登录” 请求,服务端生成二维码 id,并存储二维码的过期时间、状态等信息
- PC 端获取二维码并显示
- PC 端开始轮询检查二维码的状态,二维码最初为 "待扫描"状态
- 手机端扫描二维码,获取二维码 id
- 手机端向服务端发送 “扫码” 请求,请求中携带二维码 id、手机端 token 以及设备信息
- 服务端验证手机端用户的合法性,验证通过后将二维码状态置为 “待确认”,并将用户信息与二维码关联在一起,之后为手机端生成一个一次性 token,该 token 用作确认登录的凭证
- PC 端轮询时检测到二维码状态为 “待确认”
- 手机端向服务端发送 “确认登录” 请求,请求中携带着二维码 id、一次性 token 以及设备信息
- 服务端验证一次性 token,验证通过后将二维码状态置为 “已确认”,并为 PC 端生成 PC 端 token
- PC 端轮询时检测到二维码状态为 “已确认”,并获取到了 PC 端 token,之后 PC 端不再轮询
- PC 端通过 PC 端 token 访问服务端
二、扫码登录实战(轮询版)
1、环境准备
2、RedisTemplate序列化
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
3、Token工具类
@Slf4j
public class TokenUtil {
private final static long TIME_OUT = 25 * 24 * 60 * 60 *1000L;
private final static String SECRET = "shawn222";
public static String token(String userId) {
String token = null;
try {
Date date = new Date(System.currentTimeMillis() + TIME_OUT);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Map<String, Object> headers = new HashMap<>();
headers.put("type", "jwt");
headers.put("alg", "HS256");
token = JWT.create()
.withClaim("account", userId)
.withExpiresAt(date)
.withHeader(headers)
.sign(algorithm);
} catch (IllegalArgumentException | JWTCreationException e) {
e.printStackTrace();
}
return token;
}
public static boolean verify(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
log.info("account:" + decodedJWT.getClaim("account").asString());
return true;
} catch (IllegalArgumentException | JWTVerificationException e) {
e.printStackTrace();
return false;
}
}
4、定义扫码状态
public enum CodeStatus {
EXPIRE,
UNUSED,
CONFIRMING,
CONFIRMED
}
5、定义返回类
@Data
@NoArgsConstructor
public class CodeVO<T> {
private CodeStatus codeStatus;
private String message;
private T token;
public CodeVO(CodeStatus codeStatus) {
this.codeStatus = codeStatus;
}
public CodeVO(CodeStatus codeStatus,String message) {
this.codeStatus = codeStatus;
this.message = message;
}
public CodeVO(CodeStatus codeStatus,String message,T token) {
this.codeStatus = codeStatus;
this.message = message;
this.token=token;
}
}
6、定义二维码工具类
public class CodeUtil {
public static CodeVO getExpireCodeInfo() {
return new CodeVO(CodeStatus.EXPIRE,"二维码已更新");
}
public static CodeVO getUnusedCodeInfo() {
return new CodeVO(CodeStatus.UNUSED,"二维码等待扫描");
}
public static CodeVO getConfirmingCodeInfo() {
return new CodeVO(CodeStatus.CONFIRMING,"二维码扫描成功,等待确认");
}
public static CodeVO getConfirmedCodeInfo(String token) {
return new CodeVO(CodeStatus.CONFIRMED, "二维码已确认",token);
}
}
7、编写相应方法
@Slf4j
@Service
public class LoginService {
@Resource
RedisTemplate<String, Object> redisTemplate;
public CommonResult<String> generateUUID(){
try{
String uuid = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(RedisKeyUtil.getScanUUID(uuid),
CodeUtil.getUnusedCodeInfo(),RedisKeyUtil.getTimeOut(), TimeUnit.MINUTES);
return new CommonResult<>(uuid);
}catch (Exception e){
log.warn("redis二维码生成异常{}",e.getMessage());
}
return new CommonResult("二维码异常,请重新扫描",400);
}
public CommonResult<CodeVO> getInfoUUID(String uuid) {
Object object = redisTemplate.opsForValue().get(RedisKeyUtil.getScanUUID(uuid));
if(object==null){
return new CommonResult("二维码不存在或者已过期",400);
}
return new CommonResult<>((CodeVO)object);
}
public CommonResult scanQrLogin(String uuid, String account) {
try {
Object o = redisTemplate.opsForValue().get(RedisKeyUtil.getScanUUID(uuid));
if(null==o){
return new CommonResult<>("二维码异常,请重新扫描",400);
}
CodeVO codeVO = (CodeVO) o;
CodeStatus codeStatus = codeVO.getCodeStatus();
if(codeStatus==CodeStatus.UNUSED){
redisTemplate.opsForValue().set(RedisKeyUtil.getScanUUID(uuid),
CodeUtil.getConfirmingCodeInfo(),RedisKeyUtil.getTimeOut(), TimeUnit.MINUTES);
return new CommonResult<>("请确认登录",200,null);
}
}catch (Exception e){
log.warn("二维码异常{}",e.getMessage());
return new CommonResult<>("内部错误",500);
}
return new CommonResult<>("二维码异常,请重新扫描",400);
}
public CommonResult confirmQrLogin(String uuid, String id) {
try{
CodeVO codeVO = (CodeVO) redisTemplate.opsForValue().get(RedisKeyUtil.getScanUUID(uuid));
if(null==codeVO){
return new CommonResult<>("二维码已经失效,请重新扫描",400);
}
CodeStatus codeStatus = codeVO.getCodeStatus();
if(codeStatus==CodeStatus.CONFIRMING){
String token = TokenUtil.token(studentLoginVO.getAccount());
redisTemplate.opsForValue().set(RedisKeyUtil.getScanUUID(uuid),
CodeUtil.getConfirmedCodeInfo(token),RedisKeyUtil.getTimeOut(), TimeUnit.MINUTES);
return new CommonResult<>("登陆成功",200);
}
return new CommonResult<>("二维码异常,请重新扫描",400);
}
catch (Exception e){
log.error("确认二维码异常{}",e);
return new CommonResult<>("内部错误",500);
}
}
}
三、扫码登录(长连接版)
当然不仅仅包括短轮训,还有SSE(Server-Send Events,可以用WebFlux实现)以及WebSocket长连接实现,可以参考:Spring Boot + Web Socket 实现扫码登录
参考文章:
Java 语言实现简易版扫码登录
Java实现二维码扫描登录
|