谷粒随享
学习目标:
关键点:客户端发起请求后响应业务状态码code是208,自动到登录页面引导用户进行登录
当用户在查询专辑列表的时候,要求用户必须是登录状态。就应该让用户登录,所以在此我们自定义一个注解来表示访问此功能时必须要登录。
注解作用:哪些需要登录才能访问必须要添加,那些需要获取到用户Id控制层方法也必须加这个注解.
在service-util
模块中添加登录注解
package com.atguigu.tingshu.common.login;
import java.lang.annotation.*;
/**
* 修饰controller层方法,被注解标识方法对其所在类进行增强
* 元注解:
*
* @Target:指定使用位置,ElementType.TYPE:类上 ElementType.METHOD:方法上
* @Retention:注解保留阶段
* @Inherited:是否可以被继承
* @Documented:通过javadoc生成类文档时候是否显示类的注解信息
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface GuiGuLogin {
/**
* 该注解修饰方法是否必须登录,默认为必须登录
* @return
*/
boolean required() default true;
}
RequestContextHolder类持有上下文的Request容器。
用法:
// 获取请求对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 转化为ServletRequestAttributes
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
// 获取到HttpServletRequest 对象
HttpServletRequest request = sra.getRequest();
// 获取到HttpServletResponse 对象
HttpServletResponse response = sra.getResponse();
request 和 response 如何与 当前进行挂钩的?看底层源码
首先分析RequestContextHolder这个类,里面有两个ThreadLocal保存当前线程下的request
public abstract class RequestContextHolder {
// 得到存储进去的request
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
//可被子线程继承的reques
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
再看getRequestAttributes() 方法,相当于直接获取ThreadLocal里面的值,这样就保证了每一次获取到的Request是该请求的request.
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
if (attributes == null) {
attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
}
return attributes;
}
request和response等是什么时候设置进去的?
springMVC 核心类DispatcherServlet 继承关系
HttpServletBean
进行初始化工作FrameworkServlet
初始化 WebApplicationContext
,并提供service方法预处理请DispatcherServlet
具体分发处理.那么就可以在FrameworkServlet
查看到该类重写了service(),doGet(),doPost()...等方法,这些实现里面都有一个预处理方法processRequest(request, response);
,所以定位到了我们要找的位置
查看processRequest(request, response);
的实现,具体可以分为以下几步:
自定义切面类
package com.atguigu.tingshu.common.login;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @author: atguigu
* @create: 2025-05-30 09:51
*/
@Slf4j
@Aspect
@Component
public class GuiGuLoginAspect {
@Autowired
private RedisTemplate redisTemplate;
@Around("execution(* com.atguigu.tingshu.*.api.*.*(..)) && @annotation(guiGuLogin)")
public Object around(ProceedingJoinPoint pjp, GuiGuLogin guiGuLogin) throws Throwable {
//一、前置逻辑
log.info("前置逻辑...");
//1.获取到请求对象
//1.1 通过请求上下文(底层ThreadLocal)对象获取请求对象 RequestAttributes接口
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//1.2 将接口类型强转为ServletRequestAttributes实现类
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
//1.3 获取请求对象
HttpServletRequest request = sra.getRequest();
//2.获取请求头token中令牌
String token = request.getHeader("token");
//3.查询Redis中用户基本信息
//3.1 构建在登录成功后存入Redis用户信息Key 形式=前缀+用户令牌
String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
//3.2 查询在登录成功后存入Redis用户基本信息 UserInfoVo
UserInfoVo userInfoVo = (UserInfoVo) redisTemplate.opsForValue().get(loginKey);
//4.如果Redis中登录用户信息为空切目标方法要求必须登录 抛出异常 前端要求响应业务状态码为208,前端引导用户登录
if (userInfoVo == null && guiGuLogin.required()) {
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}
//5.如果Redis中用户信息存在,获取用户ID,将用户ID存入ThreadLocal 方便后续javaEE三层获取用户ID
if (userInfoVo != null) {
AuthContextHolder.setUserId(userInfoVo.getId());
}
//二、目标方法 controller使用使用自定义认证注解的方法
Object result = pjp.proceed();
//三、后置逻辑
log.info("后置逻辑...");
//6.避免ThreadLocal引发的内存泄漏,清理ThreadLocal
AuthContextHolder.removeUserId();
return result;
}
}
在查看专辑列表的时候,添加注解。在点击查看专辑列表的时候,就会提示我们需要进行登录!
在所有业务微服务中,com.atguigu.tingshu.*.api.XXXXController类中下所有需要登录才可以访问方法上加注解@GuiGuLogin 以下接口测试需要完成登录代码后才进行测试
AlbumInfoApiController
/**
* 该接口登录才能访问,目前并未实现登录无法获取用户ID,动态获取到用户ID
* 提供给内容创作者/运营人员保存专辑
* @param albumInfoVo
* @return
*/
@Operation(summary = "保存专辑")
@PostMapping("/albumInfo/saveAlbumInfo")
@GuiGuLogin//(required = true)
public Result saveAlbumInfo(@RequestBody @Validated AlbumInfoVo albumInfoVo){//...}
/**
* 该接口登录才能访问
* 查询当前用户专辑分页列表
* @param page 页码
* @param limit 页大小
* @param albumInfoQuery 查询条件对象
* @return
*/
@Operation(summary = "查询当前用户专辑分页列表")
@PostMapping("/albumInfo/findUserAlbumPage/{page}/{limit}")
@GuiGuLogin
public Result<Page<AlbumListVo>> getUserAlbumPage(
@PathVariable int page,
@PathVariable int limit,
@RequestBody AlbumInfoQuery albumInfoQuery){//...}
/**
* 该接口登录才能访问
* 查询当前登录用户专辑列表
* @return
*/
@GuiGuLogin
@Operation(summary = "查询当前登录用户专辑列表")
@GetMapping("/albumInfo/findUserAllAlbumList")
public Result<List<AlbumInfo>> getUserAllAlbumList(){//...}
TrackInfoApiController
/**
* 该接口需要登录才能访问
* 保存专辑下声音
*
* @param trackInfoVo 声音信息VO对象
* @return
*/
@GuiGuLogin
@Operation(summary = "保存专辑下声音")
@PostMapping("/trackInfo/saveTrackInfo")
public Result saveTrackInfo(@RequestBody @Validated TrackInfoVo trackInfoVo){//...}
/**
* 该接口登录才能访问
* 分页获取当前登录用户声音列表
*
* @param page 页码
* @param limit 页大小
* @param trackInfoQuery 查询条件
* @return 分页对象
*/
@GuiGuLogin
@Operation(summary = "分页获取当前登录用户声音列表")
@PostMapping("/trackInfo/findUserTrackPage/{page}/{limit}")
public Result<Page<TrackListVo>> getUserTrackPage(@PathVariable int page, @PathVariable int limit, @RequestBody TrackInfoQuery trackInfoQuery){//...}
说明
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意事项
session_key
是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。这里我们采用微信开发 Java SDK(非官方提供)简化开发。支持微信支付、开放平台、公众号、企业号/企业微信、小程序等的后端开发,官方地址:http://wxjava.fly2you.cn/zh-CN/
在service-user
模块中导入依赖
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
在nacos注册中心增加配置信息(已添加-注意:一定要修改为自己申请微信小程序测试号信息)
wx:
miniapp:
appid: # 小程序微信公众平台appId 改成同学申请测试号应用id
secret: # 小程序微信公众平台api秘钥 改成同学申请测试号秘钥
msgDataFormat: JSON
在IOC容器中会自动产生对象类型,可以自动注入
WxMaService
WxMaConfig
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/41
service-user
模块完成微信登录WxLoginApiController
package com.atguigu.tingshu.user.api;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.user.service.UserInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@Tag(name = "微信授权登录接口")
@RestController
@RequestMapping("/api/user/wxLogin")
@Slf4j
public class WxLoginApiController {
@Autowired
private UserInfoService userInfoService;
/**
* 微信一键登录
*
* @param code 小程序端根据当前微信,生成访问为微信服务端临时凭据
* @return {token:令牌}
*/
@Operation(summary = "微信一键登录")
@GetMapping("/wxLogin/{code}")
public Result<Map<String, String>> wxLogin(@PathVariable String code) {
Map<String, String> map = userInfoService.wxLogin(code);
return Result.ok(map);
}
}
UserInfoService
package com.atguigu.tingshu.user.service;
import com.atguigu.tingshu.model.user.UserInfo;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.Map;
public interface UserInfoService extends IService<UserInfo> {
/**
* 提供给小程序微信登录接口
*
* @param code 小程序集成SDK后获取临时票据(基于当前微信用户产生的)
* @return
*/
Map<String, String> wxLogin(String code);
}
UserInfoServiceImpl
package com.atguigu.tingshu.user.service.impl;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.atguigu.tingshu.common.rabbit.service.RabbitService;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@SuppressWarnings({"all"})
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
@Autowired
private WxMaService wxMaService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RabbitService rabbitService;
/**
* 微信一键登录
*
* @param code 小程序端根据当前微信,生成访问为微信服务端临时凭据
* @return
*/
@Override
public Map<String, String> wxLogin(String code) {
try {
//1.拿着临时凭据+应用ID+应用秘钥 调用微信接口 获取当前微信账户唯一标识:openId
//1.1 微信账户信息业务类
WxMaUserService userService = wxMaService.getUserService();
//1.2 获取会话信息
WxMaJscode2SessionResult sessionInfo = userService.getSessionInfo(code);
//1.3 获取微信账号唯一标识
String openid = sessionInfo.getOpenid();
//2.根据微信账户唯一标识,查询数据库,看当前微信是否已经注册
UserInfo userInfo = userInfoMapper.selectOne(
new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getWxOpenId, openid)
);
//3.如果微信账户首次注册,则新增用户记录,为用户初始化账户记录用于后续订单支付
if (userInfo == null) {
//3.1 新增用户记录 绑定微信账户唯一标识
userInfo = new UserInfo();
userInfo.setWxOpenId(openid);
userInfo.setNickname(IdUtil.nanoId());
userInfo.setAvatarUrl("http://192.168.200.6:9000/tingshu/2024-04-02/0b033705-4603-4fb2-bd0f-db84076aef84.jpg");
userInfoMapper.insert(userInfo);
//3.2 TODO 为当前注册用户初始化账户记录
// 方案一:Openfeign远程调用 分布式事务问题 方案二:采用MQ可靠性消息队列实现数据最终一致
//3.2.1 构建消息对象 注意:如果是VO对象一定要实现序列化接口以及生成序列化版本号
Map<String, Object> map = new HashMap<>();
map.put("userId", userInfo.getId());
map.put("amount", new BigDecimal("100"));
map.put("title", "新用户专项体验金活动");
map.put("orderNo", "ZS"+IdUtil.getSnowflakeNextId());
//3.2.2 发送消息到MQ
rabbitService.sendMessage(MqConst.EXCHANGE_USER, MqConst.ROUTING_USER_REGISTER, map);
}
//4.基于当前用户信息生成令牌
//4.1 创建令牌
String token = IdUtil.randomUUID();
//4.2 构建登录成功后Redis的Key 形式为:user:login:token
String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
//4.3 构建登录成功后Redis的Value 形式为:userInfo
UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class);
//4.4 存入Redis 设置有效期:7天
redisTemplate.opsForValue().set(loginKey, userInfoVo, RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
//5.封装令牌返回给前端
Map<String, String> map = new HashMap<>();
map.put("token", token);
return map;
} catch (WxErrorException e) {
log.error("微信登录失败");
throw new GuiguException(500, "微信登录失败");
}
}
}
在Nacos配置中心修改common.yaml
配置文件新增RabbitMQ相关信息
spring:
rabbitmq:
host: 192.168.200.6
port: 5672
username: admin
password: admin
virtual-host: /tingshu #作用隔离环境
在RabbitMQ新增虚拟主机/tingshu
在service-acount
微服务中监听消息:
package com.atguigu.tingshu.account.receiver;
import cn.hutool.core.collection.CollUtil;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.rabbitmq.client.Channel;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author: atguigu
* @create: 2025-05-30 14:11
*/
@Slf4j
@Component
public class AccountReceiver {
@Autowired
private UserAccountService userAccountService;
/**
* 用户首次注册成功后,为用户初始账户余额记录
*
* @param map 消息对象 包含:用户ID,赠送体验金额,标题,订单编号
* @param channel
* @param message
*/
@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 initUserAccount(Map<String, Object> map, Channel channel, Message message) {
log.info("[账户服务]用户首次注册成功后,为用户初始账户余额记录:{}", map);
if(CollUtil.isNotEmpty(map)){
userAccountService.initUserAccount(map);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
**UserAccountService接口 **初始化账户信息:
package com.atguigu.tingshu.account.service;
import com.atguigu.tingshu.model.account.UserAccount;
import com.baomidou.mybatisplus.extension.service.IService;
import java.math.BigDecimal;
import java.util.Map;
public interface UserAccountService extends IService<UserAccount> {
/**
* 用户首次注册成功后,为用户初始账户余额记录
*
* @param map 消息对象 包含:用户ID,赠送体验金额,标题,订单编号
*/
void initUserAccount(Map<String, Object> map);
/**
* 新增账户变动日志
* @param userId 用户ID
* @param title 内容
* @param tradeType 交易类型
* @param amount 金额
* @param orderNo 订单编号
*/
void saveUserAccountDetail(Long userId, String title, String tradeType, BigDecimal amount, String orderNo);
}
UserAccountServiceImpl实现类:
package com.atguigu.tingshu.account.service.impl;
import com.atguigu.tingshu.account.mapper.UserAccountDetailMapper;
import com.atguigu.tingshu.account.mapper.UserAccountMapper;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.account.UserAccount;
import com.atguigu.tingshu.model.account.UserAccountDetail;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Map;
@Slf4j
@Service
@SuppressWarnings({"all"})
public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {
@Autowired
private UserAccountMapper userAccountMapper;
@Autowired
private UserAccountDetailMapper userAccountDetailMapper;
/**
* 用户首次注册成功后,为用户初始账户余额记录
*
* @param map 消息对象 包含:用户ID,赠送体验金额,标题,订单编号
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void initUserAccount(Map<String, Object> map) {
//1.新增账户记录
Long userId = (Long) map.get("userId");
BigDecimal amount = (BigDecimal) map.get("amount");
String title = (String) map.get("title");
String orderNo = (String) map.get("orderNo");
UserAccount userAccount = new UserAccount();
userAccount.setUserId(userId);
userAccount.setTotalAmount(amount);
userAccount.setAvailableAmount(amount);
userAccount.setTotalIncomeAmount(amount);
userAccount.setLockAmount(new BigDecimal("0.00"));
userAccount.setTotalPayAmount(new BigDecimal("0.00"));
userAccountMapper.insert(userAccount);
//2.新增账户变动日志
this.saveUserAccountDetail( userId, title, SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT, amount, orderNo);
}
/**
* 新增账户变动日志
* @param userId 用户ID
* @param title 内容
* @param tradeType 交易类型 1201-充值 1202-锁定 1203-解锁 1204-消费',
* @param amount 金额
* @param orderNo 订单编号
*/
@Override
public void saveUserAccountDetail(Long userId, String title, String tradeType, BigDecimal amount, String orderNo) {
UserAccountDetail userAccountDetail = new UserAccountDetail();
userAccountDetail.setUserId(userId);
userAccountDetail.setTitle(title);
userAccountDetail.setTradeType(tradeType);
userAccountDetail.setAmount(amount);
userAccountDetail.setOrderNo(orderNo);
userAccountDetailMapper.insert(userAccountDetail);
}
}
TIPS:以前在AuthContextHolder硬编码的用户ID可以修改为动态获取
获取用户信息 ,将查询到的信息放入这个实体类UserInfoVo中
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/43
WxLoginApiController 控制器
/**
* 获取当前登录用户基本信息
*
* @return
*/
@GuiGuLogin
@Operation(summary = "获取当前登录用户基本信息")
@GetMapping("/getUserInfo")
public Result<UserInfoVo> getUserInfo() {
Long userId = AuthContextHolder.getUserId();
UserInfoVo userInfoVo = userInfoService.getUserInfo(userId);
return Result.ok(userInfoVo);
}
UserInfoService接口:
/**
* 根据用户ID查询用户基本信息
* @param userId
* @return
*/
UserInfoVo getUserInfo(Long userId);
UserInfoServiceImpl实现类:
/**
* 根据用户ID查询用户基本信息
*
* @param userId
* @return
*/
@Override
public UserInfoVo getUserInfo(Long userId) {
UserInfo userInfo = userInfoMapper.selectById(userId);
if (userInfo != null) {
return BeanUtil.copyProperties(userInfo, UserInfoVo.class);
}
return null;
}
需求:登录成功之后,可以修改用户基本信息。
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/45
WxLoginApiController控制器
/**
* 更新当前用户信息
* @param userInfoVo
* @return
*/
@GuiGuLogin
@Operation(summary = "更新当前用户信息")
@PostMapping("/updateUser")
public Result updateUser(@RequestBody UserInfoVo userInfoVo) {
userInfoService.updateUser(userInfoVo);
return Result.ok();
}
UserInfoService接口
/**
* 更新用户信息方法
* @param userInfoVo
*/
void updateUser(UserInfoVo userInfoVo);
UserInfoServiceImpl实现类
/**
* 更新用户信息方法
* 只允许修改昵称头像
* @param userInfoVo
*/
@Override
public void updateUser(UserInfoVo userInfoVo) {
//1.获取用户ID
Long userId = AuthContextHolder.getUserId();
//2.更新用户昵称跟头像
UserInfo userInfo = new UserInfo();
userInfo.setId(userId);
userInfo.setAvatarUrl(userInfoVo.getAvatarUrl());
userInfo.setNickname(userInfoVo.getNickname());
userInfoMapper.updateById(userInfo);
}