第3章 用户登录.md 22 KB

谷粒随享

第3章 用户登录

学习目标:

1、认证校验注解

1.1 自定义注解

​ 当用户在查询专辑列表的时候,要求用户必须是登录状态。就应该让用户登录,所以在此我们自定义一个注解来表示访问此功能时必须要登录。

注解作用:哪些需要登录才能访问必须要添加,那些需要获取到用户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;
}

1.2 切面类

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 继承关系

  1. HttpServletBean 进行初始化工作
  2. FrameworkServlet 初始化 WebApplicationContext,并提供service方法预处理请
  3. DispatcherServlet 具体分发处理.

那么就可以在FrameworkServlet查看到该类重写了service(),doGet(),doPost()...等方法,这些实现里面都有一个预处理方法processRequest(request, response);,所以定位到了我们要找的位置

查看processRequest(request, response);的实现,具体可以分为三步:

  1. 获取上一个请求的参数
  2. 重新建立新的参数
  3. 设置到XXContextHolder
  4. 父类的service()处理请求
  5. 恢复request
  6. 发布事

自定义切面类

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();
    }
}

在查看专辑列表的时候,添加注解。在点击查看专辑列表的时候,就会提示我们需要进行登录!

1.3 使用认证校验注解

2、微信登录

小程序登录 | 微信开放文档 (qq.com)

说明

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

注意事项

  1. 会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥
  2. 临时登录凭证 code 只能使用一次

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);
    }
}

3、获取登录用户信息

获取用户信息 ,将查询到的信息放入这个实体类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;
	}
}

4、更新用户信息方法

需求:登录成功之后,可以修改用户基本信息。

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);
}