梳理微信小程序登录、OpenID 查询、Redis 保存登录态,以及自定义注解和 AOP 校验登录的流程。

本篇要点

  • 从 code 获取用户身份
  • 用 Redis 保存登录状态
  • 通过注解与切面统一校验登录

内容回顾

核心模块

  • 登录
  • 专辑详情
  • 订单
  • 搜索

声音管理模块

  • 保存声音
  • 声音列表
  • 删除声音
  • 修改声音

今天内容

前置知识

  • 自定义注解
  • AOP
  • Redis

1、用户登录流程(背)

image-20251024093426958

image-20251024093439803

  • **httpClient:**请求微信接口服务器使用httpClient
  • **RabbitMQ:**第一次登录,使用RabbitMQ异步初始化账户余额
  • **Redis:**通过Redis存储token数据,Redis的key是token,value是用户信息,设置过期时间
  • **Cookie(localStorage):**因为cookie默认不能跨域传递的,每次发送请求时候,把cookie值放到请求头里面进行传递
  • 自定义注解
  • AOP

2、实现校验登录

流程

  • 每次请求接口都需要首先做登录判断
  • 1、从请求头获取token(前端传递过来的)
  • 2、根据token查询redis(redis的key是token),如果可以查询到是登录,查询不到不是登录

基础代码实现

1
2
3
4
5
6
7
8
9
10
11
//* **1、从请求头获取token(前端传递过来的)**
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)) {//不是登录
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}

//* **2、根据token查询redis(redis的key是token),如果可以查询到是登录,查询不到不是登录**
UserInfo userInfo = (UserInfo)redisTemplate.opsForValue().get(token);
if(userInfo == null) {//不是登录
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}

最终实现

分析过程

  • 上面代码虽然可以实现实现登录校验,但是每个接口方法里面都需要写上面重复代码
  • 实现方式有很多种

第一种 使用Spring里面拦截器实现

第二种 使用Gateway网关里面过滤器实现

第三种 使用自定义注解 + AOP实现

实现步骤

第一步 创建注解,创建相关属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//元注解:定义注解特性的注解 @Target @Retention
@Target({ElementType.METHOD})//设置注解可以使用在什么地方,比如类上面,方法上面,属性上面
@Retention(RetentionPolicy.RUNTIME) //在什么时候生效
public @interface GuiguLogin {
//@GuiguLogin(value = "123")
//属性定义格式
//类型 属性名称() 默认值
//String value() default "";

/**
* 是否必须要登录
* @return
*/
boolean required() default true;
}
第二步 创建切面类

image-20251024143500093

  • 在切面类里面,创建方法,方法里面写登录校验逻辑

  • 在方法上面添加注解,使用通知(环绕)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Aspect   //设置当前类切面类
@Component
public class GuiGuLoginAspect {

@Autowired
private RedisTemplate redisTemplate;

//ProceedingJoinPoint:获取被增强方法信息和被增强方法执行
//使用环绕通知 @Around(切入点表达式)
@Around("execution(* com.atguigu.tingshu.*.api.*.*(..)) && @annotation(guiguLogin)")
public Object login(ProceedingJoinPoint joinPoint,
GuiguLogin guiguLogin) throws Throwable {
//RequestContextHolder 上下文对象 获取request对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();

//* **1、从请求头获取token(前端传递过来的)**
String token = request.getHeader("token");

//* **2、根据token查询redis(redis的key是token),
// 如果可以查询到是登录,查询不到不是登录**
//因为GuiguLogin注解有属性 required
//这个属性 required==true表示必须登录,required==false可以登录可以不登录(比如首页面)
boolean isRequired = guiguLogin.required();
if(isRequired) { //表示必须登录
//判断请求头token是否为空,如果为空,返回登录提示信息
if(StringUtils.isEmpty(token)) {
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}

//如果token不为空,根据token查询redis,判断查询数据是否为空,如果为空,返回登录提示信息
UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(token);
//如果为空,返回登录提示信息
if(userInfo == null) {
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}
}

if (!StringUtils.isEmpty(token)){
// 组成缓存key
String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX+token;
// 获取缓存中用户数据
UserInfo userInfo = (UserInfo) this.redisTemplate.opsForValue().get(loginKey);
if (null != userInfo){
// 存储用户Id
AuthContextHolder.setUserId(userInfo.getId());
}
}

Object obj = joinPoint.proceed();
return obj;
}
}
第三步 在具体接口上面,添加注解,实现登录校验

image-20251024105321291

切面类问题

  • 如果在一个项目中,创建多个切面类,内部切面类异常,在外部不能感知到
  • 多个切面类,内部切面类异常需要手动抛出去,否则外部无法感知到

image-20251024145144973

image-20251024145030244

3、实现用户登录

准备两个值

image-20251024145819911

  • 微信公众平台 id 和 秘钥
  • 正式号

image-20251024150018064

  • 测试号

image-20251024150135983

登录接口

配置文件添加值

  • 在service-user模块编写登录接口

  • 把微信公众平台 id 和 秘钥放到nacos配置中心配置文件中

image-20251024152223669

准备工具类

读取配置文件中值,对微信封装对象进行初始化

1
2
3
4
5
6
7
8
9
@Data
@Component
@ConfigurationProperties(prefix = "wechat.login")
public class WechatAccountConfig {
// 公众平台的appId
private String appId;
// 小程序微信公众平台秘钥
private String appSecret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class WeChatMpConfig {

@Autowired
private WechatAccountConfig wechatAccountConfig;

@Bean
public WxMaService wxMaService(){
// 创建对象
WxMaDefaultConfigImpl wxMaConfig = new WxMaDefaultConfigImpl();
wxMaConfig.setAppid(wechatAccountConfig.getAppId());
wxMaConfig.setSecret(wechatAccountConfig.getAppSecret());
wxMaConfig.setMsgDataFormat("JSON");

// 创建 WxMaService 对象
WxMaService service = new WxMaServiceImpl();
// 给 WxMaService 设置配置选项
service.setWxMaConfig(wxMaConfig);
return service;
}
}

WxLoginApiController

image-20251024155919901

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.atguigu.tingshu.user.api;

import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.atguigu.tingshu.common.rabbit.service.RabbitService;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Tag(name = "微信授权登录接口")
@RestController
@RequestMapping("/api/user/wxLogin")
@Slf4j
public class WxLoginApiController {

@Autowired
private UserInfoService userInfoService;

@Autowired
private WxMaService wxMaService;

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private RabbitService rabbitService;

//code前端传递过来
@GetMapping("/wxLogin/{code}")
public Result wxLogin(@PathVariable("code") String code) throws WxErrorException {
//1 拿着code + 微信公众平台id + 秘钥 请求微信服务器接口,返回openid
WxMaJscode2SessionResult sessionInfo =
wxMaService.getUserService().getSessionInfo(code);
String openid = sessionInfo.getOpenid();

//2 根据openid判断是否第一次登录
LambdaQueryWrapper<UserInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserInfo::getWxOpenId,openid);
UserInfo userInfo = userInfoService.getOne(wrapper);

//如果第一次登录,添加用户信息,发送mq消息初始化账户
if (userInfo == null) {
//添加用户信息
userInfo = new UserInfo();
// 赋值用户昵称
userInfo.setNickname("听友"+System.currentTimeMillis());
// 赋值用户头像图片
userInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
//openid
userInfo.setWxOpenId(openid);
//调用方法添加
userInfoService.save(userInfo);

//发送mq消息异步初始化账户
rabbitService.sendMessage(MqConst.EXCHANGE_USER,
MqConst.ROUTING_USER_REGISTER,
userInfo.getId());
}

//3 生成token,把数据放到redis里面,
// redis的key是token,value是用户信息,设置redis过期时间
String token = UUID.randomUUID().toString()
.replaceAll("-","");
//数据放到redis里面,
redisTemplate.opsForValue().set(token,
userInfo,
30, TimeUnit.MINUTES);

//4 返回token
HashMap<String, Object> map = new HashMap<>();
map.put("token",token);
// 返回数据
return Result.ok(map);
}
}

MQ消息接收端

  • 在service-account模块编写接收端,获取发送过来userId,初始化账户信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class AccountReceiver {

@Autowired
private UserAccountService userAccountService;

@SneakyThrows
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = MqConst.EXCHANGE_USER, durable = "true"),
value = @Queue(value = MqConst.QUEUE_USER_REGISTER, durable = "true"),
key = {MqConst.ROUTING_USER_REGISTER}
))
public void receive(Long userId, Message message, Channel channel) {
if(userId != null) {
userAccountService.initUserAccount(userId);
}
//手动确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}

总结

登录问题总结

1、登录实现流程(图里面流程)

2、登录实现过程中使用的技术

  • **RabbitMQ:**第一次登录,使用RabbitMQ异步初始化账户余额
  • **Redis:**通过Redis存储token数据,Redis的key是token,value是用户信息,设置过期时间
  • **Cookie(localStorage):**因为cookie默认不能跨域传递的,每次发送请求时候,把cookie值放到请求头里面进行传递
  • 自定义注解
  • AOP

3、有亮点地方

  • 自定义注解 + AOP实现登录校验

4、遇到问题

  • 多个切面类,内部切面类异常主动抛出去,否则外部不知道的