谷粒随享
学习目标:
当用户在查询专辑列表的时候,要求用户必须是登录状态。就应该让用户登录,所以在此我们自定义一个注解来表示访问此功能时必须要登录。
注解作用:哪些需要登录才能访问必须要添加,那些需要获取到用户Id控制层方法也必须加这个注解.
在service-util
模块中添加登录注解
package com.atguigu.tingshu.common.login;
import org.springframework.aot.hint.annotation.Reflective;
import java.lang.annotation.*;
/**
* 认证自定义注解:
* 属性:要求是否必须登录属性(true:要求必须登录)
* 元注解:
*
* @Target:注解使用位置:指定方法,类(接口),属性,构造器,构造器参数
* @Retention:注解生命周期 例如设置为SOURCE 编译后注解没了;CLASS 运行是注解没了
* @Inherited 该注解是否可以被继承
* @Documented javadoc命令是否生成文档 javadoc -encoding UTF-8 -charset UTF-8 Test.java
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface GuiGuLogin {
//是否要求用户必须登录:默认为必须登录
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 jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
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;
/**
* 认证校验切面类:对自定义注解@GuiguLogin修饰方法进行增强,获取登录用户ID,执行不同业务
* 1.声明切面类-产生对象
* 2.声明通知
* 3.声明切入点(对注解进行增强)
* 4.完善认证通知代码,尝试获取登录用户ID
*
* @author: atguigu
* @create: 2023-10-20 11:15
*/
@Slf4j
@Aspect //声明切面
@Component //将切面对象注册到IOC容器
public class GuiGuLoginAspect {
@Autowired
private RedisTemplate redisTemplate;
/**
* @param joinPoint 切入点方法对象
* @param guiGuLogin 自定义注解对象
* @return
*/
@SneakyThrows //自动对方法体try catch
@Around("execution(* com.atguigu.tingshu.*.api.*.*(..)) && @annotation(guiGuLogin)")
public Object guiguLoginAspect(ProceedingJoinPoint joinPoint, GuiGuLogin guiGuLogin) {
Object object = new Object();
log.info("前置通知逻辑...");
//1.尝试从请求对象中获取用户Token(请求头)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//RequestAttributes是接口 ServletRequestAttributes接口实现类
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = sra.getRequest();
HttpServletResponse response = sra.getResponse();
//2.根据Token获取用户信息(用户ID,用户昵称)
//2.1 获取用户token
String token = request.getHeader("token");
//2.2 拼接用户登录时候存入Redis中Key
String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
//2.3 根据Key查询用户信息
UserInfoVo userInfoVo = (UserInfoVo) redisTemplate.opsForValue().get(loginKey);
if (guiGuLogin.required()) {
//要求用户必须登录才可以,如果此时用户信息为空抛出异常,小程序员引导用户进行登录
if (userInfoVo == null) {
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}
}
//3.将用户信息隐式传入,在当前线程生命周期内获取到用户信息
if (userInfoVo != null) {
//说明用户登录-将用户ID跟用户名称存入ThreadLocal
AuthContextHolder.setUserId(userInfoVo.getId());
}
//执行目标方法->切入点方法(被增强方法)
object = joinPoint.proceed();
log.info("后置通知逻辑...");
//4.避免ThreadLocal导致内存泄漏,产生OOM问题,立即将ThreadLocal中数据清理
AuthContextHolder.removeUserId();
return object;
}
}
在查看专辑列表的时候,添加注解。在点击查看专辑列表的时候,就会提示我们需要进行登录!
在所有业务微服务中,com.atguigu.tingshu.*.api.XXXXController类中下所有需要登录才可以访问方法上加注解@GuiGuLogin 以下接口测试需要完成登录代码后才进行测试
/**
*
* 创作者新增专辑
*
* @return
*/
@GuiGuLogin
@Operation(summary = "新增专辑")
@PostMapping("/albumInfo/saveAlbumInfo")
public Result saveAlbumInfo(@RequestBody @Validated AlbumInfoVo albumInfoVo){}
/**
*
* 根据专辑ID删除专辑
*
* @param id
* @return
*/
@GuiGuLogin
@Operation(summary = "根据专辑ID删除专辑")
@DeleteMapping("/albumInfo/removeAlbumInfo/{id}")
public Result removeAlbumInfo(@PathVariable("id") Long id);
/**
*
* @param trackInfoVo
* @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>> getUserTrackByPage(...){}
....
说明
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意事项
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: wxcc651fcbab275e33 # 小程序微信公众平台appId
secret: 5f353399a2eae7ff6ceda383e924c5f6 # 小程序微信公众平台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
*/
@Operation(summary = "小程序授权登录")
@GetMapping("/wxLogin/{code}")
public Result wxLogin(@PathVariable String code) {
Map<String, String> mapResult = userInfoService.wxLogin(code);
return Result.ok(mapResult);
}
}
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
* @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.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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
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;
/**
* 采用微信JavaSDK进行微信登录
*
* @param code
* @return
*/
//@Override
//public Map<String, String> weiXinLogin(String code) {
// try {
// //1.根据code调用微信SDK获取用户会话信息-得到微信用户唯一标识(OpenID)(微信端用户标识不变)-大家无法调用(没有在开发者列表)
// WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code);
// if (sessionInfo != null) {
// String openid = sessionInfo.getOpenid();
// //2.根据openId查询用户记录 TODO 固定写死OpenID odo3j4q2KskkbbW-krfE-cAxUnzU
// LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
// queryWrapper.eq(UserInfo::getWxOpenId, openid);
// UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
//
// //2.1 根据OpenId没有得到用户记录 新增用户记录 且 采用MQ异步初始化账户(余额)信息
// if (userInfo == null) {
// userInfo = new UserInfo();
// userInfo.setWxOpenId(openid);
// userInfo.setNickname("听友" + IdUtil.getSnowflake());
// userInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
// userInfoMapper.insert(userInfo);
// //TODO 发送异步MQ消息,通知账户微服务初始化当前用户账户余额信息
// }
// //2.2 根据OpenID获取到用户记录,
//
// //3.为登录微信用户生成令牌,将令牌存入Redis中
// String token = IdUtil.fastSimpleUUID();
// String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
// UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class);
// redisTemplate.opsForValue().set(loginKey, userInfoVo, RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
//
//
// //4.响应令牌
// Map<String, String> mapResult = new HashMap<>();
// mapResult.put("token", token);
// return mapResult;
// }
// return null;
// } catch (WxErrorException e) {
// log.error("微信登录异常:{}", e);
// throw new RuntimeException(e);
// }
//}
/**
* 采用微信JavaSDK进行微信登录
*
* @param code
* @return
*/
@Override
public Map<String, String> weiXinLogin(String code) {
try {
//1.根据code调用微信SDK获取用户会话信息-得到微信用户唯一标识(OpenID)(微信端用户标识不变)-大家无法调用(没有在开发者列表)
//WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code);
//if (sessionInfo != null) {
// String openid = sessionInfo.getOpenid();
String openid = "odo3j4q2KskkbbW-krfE-cAxUnzU";
//2.根据openId查询用户记录 TODO 固定写死OpenID odo3j4q2KskkbbW-krfE-cAxUnzU
LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserInfo::getWxOpenId, openid);
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
//2.1 根据OpenId没有得到用户记录 新增用户记录 且 采用MQ异步初始化账户(余额)信息
if (userInfo == null) {
userInfo = new UserInfo();
userInfo.setWxOpenId(openid);
userInfo.setNickname("听友" + IdUtil.getSnowflake().nextId());
userInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
userInfoMapper.insert(userInfo);
//TODO 发送异步MQ消息,通知账户微服务初始化当前用户账户余额信息
kafkaService.sendMessage(KafkaConstant.QUEUE_USER_REGISTER, userInfo.getId().toString());
}
//2.2 根据OpenID获取到用户记录,
//3.为登录微信用户生成令牌,将令牌存入Redis中
String token = IdUtil.fastSimpleUUID();
String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class);
redisTemplate.opsForValue().set(loginKey, userInfoVo, RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
//4.响应令牌
Map<String, String> mapResult = new HashMap<>();
mapResult.put("token", token);
return mapResult;
} catch (Exception e) {
log.error("微信登录异常:{}", e);
throw new RuntimeException(e);
}
}
}
在kafka-util
模块中定义KafkaService 工具类
package com.atguigu.tingshu.common.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class KafkaService {
private static final Logger logger = LoggerFactory.getLogger(KafkaService.class);
@Autowired
private KafkaTemplate kafkaTemplate;
/**
* 发送消息工具方法
*
* @param topic 话题名称
* @param data 业务消息
*/
public void sendMessage(String topic, Object data) {
this.sendMessage(topic, null, data);
}
/**
* 发送消息工具方法
*
* @param topic 话题名称
* @param key 业务消息Key
* @param data 业务消息
*/
public void sendMessage(String topic, String key, Object data) {
//1.调用模板发送消息
CompletableFuture completableFuture = kafkaTemplate.send(topic, key, data);
//2.获取发送消息结果 3.获取发送异常后错误信息
completableFuture.thenAcceptAsync(result -> {
logger.info("发送消息成功:{}", result);
}).exceptionally(e -> {
logger.error("发送消息失败:{}", e);
return null;
});
}
}
在service-acount
微服务中监听消息:
package com.atguigu.tingshu.account.receiver;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
* @author: atguigu
* @create: 2023-09-18 22:05
*/
@Component
public class AccountReceiver {
@Autowired
private UserAccountService userAccountService;
/**
* 监听消息并添加账户信息
*
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_USER_REGISTER)
public void initUserAccount(ConsumerRecord<String, String> record) {
String userId = record.value();
if (StringUtils.isBlank(userId)) {
return;
}
userAccountService.saveUserAccount(Long.valueOf(userId));
}
}
**UserAccountService接口 **初始化账户信息:
package com.atguigu.tingshu.account.service;
import com.atguigu.tingshu.model.account.UserAccount;
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserAccountService extends IService<UserAccount> {
/**
* 新增账号信息
*
* @param userId
*/
void saveUserAccount(Long userId);
}
UserAccountServiceImpl实现类:
package com.atguigu.tingshu.account.service.impl;
import com.atguigu.tingshu.account.mapper.UserAccountMapper;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.model.account.UserAccount;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jdk.jfr.StackTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {
@Autowired
private UserAccountMapper userAccountMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveUserAccount(Long userId) {
// user_account
UserAccount userAccount = new UserAccount();
userAccount.setUserId(userId);
userAccountMapper.insert(userAccount);
}
}
TIPS:以前在AuthContextHolder硬编码的用户ID可以修改为动态获取
获取用户信息 ,将查询到的信息放入这个实体类UserInfoVo中
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/43
WxLoginApiController 控制器
/**
* 根据用户Id获取到用户数据
* @return
*/
@GuiGuLogin
@Operation(summary = "获取登录信息")
@GetMapping("/getUserInfo")
public Result getUserInfo(){
// 获取到用户Id
Long userId = AuthContextHolder.getUserId();
// 调用服务层方法
UserInfoVo userInfoVo = userInfoService.getUserInfoVoByUserId(userId);
// 返回数据
return Result.ok(userInfoVo);
}
UserInfoService接口:
package com.atguigu.tingshu.user.service;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserInfoService extends IService<UserInfo> {
/**
* 根据userId 获取用户登录信息
* @param userId
* @return
*/
UserInfoVo getUserInfoVoByUserId(Long userId);
}
UserInfoServiceImpl实现类:
package com.atguigu.tingshu.user.service.impl;
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.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public UserInfoVo getUserInfoVoByUserId(Long userId) {
// 获取到用户信息对象
UserInfo userInfo = userInfoMapper.selectById(userId);
if (userInfo != null) {
//属性拷贝,创建UserInfoVo 对象
UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class);
return userInfoVo;
}
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){
// 获取到用户Id
Long userId = AuthContextHolder.getUserId();
userInfoService.updateUser(userInfoVo, userId);
return Result.ok();
}
UserInfoService接口
/**
* 更新用户信息
* @param userInfoVo
* @param userId
*/
void updateUser(UserInfoVo userInfoVo, Long userId);
UserInfoServiceImpl实现类
/**
* 更新用户个人基本信息
*
* @param userInfoVo
* @param userId
*/
@Override
public void updateUser(UserInfoVo userInfoVo, Long userId) {
UserInfo userInfo = new UserInfo();
//只允许更新头像、昵称信息
userInfo.setId(userId);
userInfo.setNickname(userInfoVo.getNickname());
userInfo.setAvatarUrl(userInfoVo.getAvatarUrl());
// 执行更新方法
userInfoMapper.updateById(userInfo);
}