谷粒随享
学习目标:
当用户在查询专辑列表的时候,要求用户必须是登录状态。就应该让用户登录,所以在此我们自定义一个注解来表示访问此功能时必须要登录。
注解作用:哪些需要登录才能访问必须要添加,那些需要获取到用户Id控制层方法也必须加这个注解.
在service-util
模块中添加登录注解
package com.atguigu.tingshu.common.login;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
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.model.user.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import lombok.SneakyThrows;
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.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @author atguigu
* @ClassName GuiGuLoginAspect
* @description: TODO
* @date 2023年05月19日
* @version: 1.0
*/
@Aspect
@Component
public class GuiGuLoginAspect {
@Autowired
private RedisTemplate redisTemplate;
@SneakyThrows
@Around("execution(* com.atguigu.tingshu.*.api.*.*(..)) && @annotation(guiGuLogin)")
public Object loginAspect(ProceedingJoinPoint point,GuiGuLogin guiGuLogin){
// 获取请求对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 转化为ServletRequestAttributes
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
// 获取到HttpServletRequest
HttpServletRequest request = sra.getRequest();
String token = request.getHeader("token");
// 判断是否需要登录
if (guiGuLogin.required()){
// 必须要登录,token 为空是抛出异常
if (StringUtils.isEmpty(token)){
// 没有token 要抛出异常
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}
// 如果token 不为空,从缓存中获取信息.
UserInfo userInfo = (UserInfo) this.redisTemplate.opsForValue().get(RedisConstant.USER_LOGIN_KEY_PREFIX + token);
// 判断对象是否为空
if (null == userInfo){
// 抛出异常信息
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}
}
// 不需要强制登录,但是,有可能需要用信息.
if (!StringUtils.isEmpty(token)){
// 如果token 不为空,从缓存中获取信息.
UserInfo userInfo = (UserInfo) this.redisTemplate.opsForValue().get(RedisConstant.USER_LOGIN_KEY_PREFIX + token);
if (null != userInfo){
// 将用户信息存储到请求头中
AuthContextHolder.setUserId(userInfo.getId());
AuthContextHolder.setUsername(userInfo.getNickname());
}
}
// 执行业务逻辑
return point.proceed();
}
}
在查看专辑列表的时候,添加注解。在点击查看专辑列表的时候,就会提示我们需要进行登录!
说明
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意事项
session_key
是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/41
service-user
模块完成微信登录
创建WxMaService配置类
package com.atguigu.tingshu.user.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "wechat.login")
public class WechatAccountConfig {
// 公众平台的appdId
private String appId;
// 小程序微信公众平台秘钥
private String appSecret;
}
package com.atguigu.tingshu.user.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaCloudServiceImpl;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.config.WxMaConfig;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* @author atguigu
* @ClassName WeChatMpConfig
* @description: TODO
* @date 2023年05月20日
* @version: 1.0
*/
@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
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 com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.service.KafkaService;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.service.UserInfoService;
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.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
@Autowired
private WxMaService wxMaService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private KafkaService kafkaService;
/**
* 微信小程序登录
*
* @param code 临时票据
* @return
*/
@Override
public Map<String, String> wxLogin(String code) {
try {
//1.根据临时票据获取微信用户唯一标识openId
WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code);
if (sessionInfo != null) {
String openid = sessionInfo.getOpenid();
//2.根据openId查询数据库中用户账号
LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserInfo::getWxOpenId, openid);
UserInfo userInfo = this.getOne(queryWrapper);
//2.1 如果账号不存在说明用户第一次进行微信登录
if (userInfo == null) {
//2.1.1 创建用户对象给用户基本信息赋值保存用户
userInfo = new UserInfo();
userInfo.setNickname("听友" + System.currentTimeMillis());
userInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
userInfo.setWxOpenId(openid);
this.save(userInfo);
//2.1.2 发送Kafka消息通知账户系统初始化账号信息
kafkaService.sendMessage(KafkaConstant.QUEUE_USER_REGISTER, userInfo.getId().toString());
}
//2.2 如果账号存在为登录账号生成Token令牌
String token = UUID.randomUUID().toString().replaceAll("-", "");
String key = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
redisTemplate.opsForValue().set(key, userInfo, RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
Map<String, String> mapResult = new HashMap<>();
mapResult.put("token", token);
return mapResult;
}
return null;
} catch (Exception e) {
throw new RuntimeException(ResultCodeEnum.FAIL.getMessage());
}
}
}
KafkaService 工具类
package com.atguigu.tingshu.common.service;
import com.alibaba.fastjson.JSON;
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 value
* @return
*/
public boolean sendMessage(String topic, Object value) {
// 调用发送消息方法
return this.sendMessage(topic, null, value);
}
/**
* 封装发送消息方法
*
* @param topic
* @param key
* @param value
* @return
*/
private boolean sendMessage(String topic, String key, Object value) {
// 执行发送消息
CompletableFuture completableFuture = kafkaTemplate.send(topic, key, value);
// 执行成功回调方法
completableFuture.thenAccept(result -> {
logger.debug("发送消息成功: topic={},key={},value={}", topic, key, JSON.toJSONString(value));
}).exceptionally(e -> {
logger.error("发送消息失败: topic={},key={},value={}", topic, key, JSON.toJSONString(value));
return null;
});
return true;
}
}
在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);
}
}
获取用户信息 ,将查询到的信息放入这个实体类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);
}
实现类:
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 = this.getById(userId);
// 创建UserInfoVo 对象
UserInfoVo userInfoVo = new UserInfoVo();
// 属性拷贝
BeanUtils.copyProperties(userInfo,userInfoVo);
return userInfoVo;
}
}
需求:登录成功之后,可以修改用户基本信息。
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);
}