个人博客项目笔记_04

1. 注册

1.1 接口说明

接口url:/register

请求方式:POST

请求参数:

参数名称 参数类型 说明
account string 账号
password string 密码
nickname string 昵称

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

1.2 Controller

package com.cherriesovo.blog.controller;

import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.vo.Result;
import com.cherriesovo.blog.vo.params.LoginParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("register")
public class RegisterController {

    @Autowired
    private LoginService loginService;

    @PostMapping
    public Result register(@RequestBody LoginParam loginParam){
        //sso单点登录,后期如果把登陆注册功能提出去(单独的服务 可以独立提供接口服务)
        return loginService.register(loginParam);
    }
}

参数LoginParam类中 添加新的参数nickname。

package com.cherriesovo.blog.vo.params;

import lombok.Data;
import org.apache.commons.lang3.StringUtils;

@Data
public class LoginParam {
    private String account;

    private String password;

    private String nickname;
}

1.3 Service

package com.cherriesovo.blog.service;

import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.vo.Result;

public interface SysUserService {
    //根据账户查询用户
    SysUser findUserByAccount(String account);

    //保存用户
    void save(SysUser sysUser);
}

package com.cherriesovo.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.cherriesovo.blog.dao.mapper.SysUserMapper;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.service.SysUserService;
import com.cherriesovo.blog.utils.JWTUtils;
import com.cherriesovo.blog.vo.ErrorCode;
import com.cherriesovo.blog.vo.LoginUserVo;
import com.cherriesovo.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    private SysUserMapper sysUserMapper;
    @Override
    public SysUser findUserByAccount(String account) {
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        //SELECT * FROM sys_user WHERE account = '指定的account' LIMIT 1
        queryWrapper.eq(SysUser::getAccount,account);
        queryWrapper.last("limit 1");
        return sysUserMapper.selectOne(queryWrapper);
    }

    @Override
    public void save(SysUser sysUser) {
        //注意 保存用户的id会自动生成 默认生成的id 是分布式id 采用了雪花算法
        //采用框架提供的insert()插入数据
        this.sysUserMapper.insert(sysUser);
    }
}

  1. redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser), 1, TimeUnit.DAYS);
    • redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
      • key: Redis 中的键值,这里是以 "TOKEN_" 开头加上用户的 token。
      • value: 要存储的值,这里是用户信息对象 sysUser 转换成的 JSON 字符串,使用了 JSON.toJSONString() 方法。
      • timeout: 过期时间,这里设置为 1。
      • timeUnit: 过期时间单位,这里设置为 TimeUnit.DAYS,表示一天。

这段代码的作用是将用户信息存入 Redis 中,并设置了一天的过期时间。通常这样做是为了实现用户登录状态的持久化,以及实现一定的缓存机制。

package com.cherriesovo.blog.service.impl;

import com.alibaba.fastjson.JSON;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.service.SysUserService;
import com.cherriesovo.blog.utils.JWTUtils;
import com.cherriesovo.blog.vo.ErrorCode;
import com.cherriesovo.blog.vo.Result;
import com.cherriesovo.blog.vo.params.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
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 org.springframework.transaction.annotation.Transactional;

import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
@Transactional
public class LoginServiceImpl implements LoginService {

    @Override
    public Result register(LoginParam loginParam) {
        /*
        * 1、判断参数是否合法
        * 2、判断账户是否存在,存在则返回账户已被注册
        * 3、如果账户不存在,注册用户
        * 4、生成token
        * 5、存入redis并返回
        * 6、注意 加上事务,一旦中间出现任何问题,需要回滚
        * */

        //判断参数
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        String nickname = loginParam.getNickname();
        if (StringUtils.isBlank(account)
                || StringUtils.isBlank(password)
                || StringUtils.isBlank(nickname)
        ){
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }
        //判断账户是否存在,存在则返回账户已被注册
        SysUser sysUser = sysUserService.findUserByAccount(account);
        if (sysUser != null){
            return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),ErrorCode.ACCOUNT_EXIST.getMsg());
        }
        //如果账户不存在,注册用户
        sysUser = new SysUser();
        sysUser.setNickname(nickname);
        sysUser.setAccount(account);
        sysUser.setPassword(DigestUtils.md5Hex(password+slat));	//将密码与盐值拼接后进行 MD5 加密,并设置为用户的密码。
        sysUser.setCreateDate(System.currentTimeMillis());
        sysUser.setLastLogin(System.currentTimeMillis());
        sysUser.setAvatar("/static/img/logo.b3a48c0.png");
        sysUser.setAdmin(1); //1 为true
        sysUser.setDeleted(0); // 0 为false,设置用户是否被删除,这里将其设置为 0,代表未删除。
        sysUser.setSalt("");//设置用户的盐值为空字符串,这里是一个占位符,实际应用中根据需要生成盐值
        sysUser.setStatus("");
        sysUser.setEmail("");
        this.sysUserService.save(sysUser);

        //通过用户id生成token
        String token = JWTUtils.createToken(sysUser.getId());
		//使用 RedisTemplate 将用户信息以 JSON 格式存入 Redis 中,并设置了过期时间为一天
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
        return Result.success(token);

    }
}

//ErrorCode类中添加
ACCOUNT_EXIST(10004,"账号已存在"),

1.4 加事务

@Service
@Transactional
public class LoginServiceImpl implements LoginService {}

当然 一般建议加在 接口上,通用一些。

测试的时候 可以将redis 停掉,那么redis连接异常后,新添加的用户 应该执行回滚操作。

1.5 测试

2. 登录拦截器

每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。

那么可不可以统一进行登录判断呢?

可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。

2.1 拦截器实现

  1. public class LoginInterceptor implements HandlerInterceptor:

    LoginInterceptor 类实现了 HandlerInterceptor 接口,拦截请求并在进入 Controller 方法之前进行处理。

  2. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception:

    这是拦截器中的 preHandle 方法,用于在请求到达 Controller 方法之前进行处理。让我解释这个方法的参数和作用:

    • Object handler: 表示被拦截的处理器对象,通常是一个 Controller 方法。在这个方法中,我们可以对请求进行拦截、处理和转发。
    • boolean: 方法的返回类型为布尔值,用于指示是否允许请求继续执行。如果返回 true,则请求将继续执行后续的拦截器或进入 Controller 方法;如果返回 false,则请求将被拦截,不会继续执行后续的拦截器或进入 Controller 方法。
  3. if (!(handler instanceof HandlerMethod)){
                //handler可能是RequestResourceHandler springboot程序访问静态资源默认去classpath下的static目录去查询
                return true;
            }
    

    这段代码用于检查 handler 是否为 HandlerMethod 的实例,如果不是,则认为是请求静态资源,直接放行。

    • HandlerMethod 是 Spring MVC 中用于处理请求的处理器方法的封装类。通常情况下,Controller 中的方法会被包装成 HandlerMethod 实例。
    • RequestResourceHandler 是 Spring Boot 默认用于处理静态资源的处理器。当请求的路径匹配到静态资源时,会由 RequestResourceHandler 处理。

    因此,这段代码的逻辑是:如果 handler 不是 HandlerMethod 的实例,即不是 Controller 中的处理器方法,则认为是请求静态资源,直接放行,不进行拦截处理。

package com.cherriesovo.blog.handler;

import com.alibaba.fastjson.JSON;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.vo.ErrorCode;
import com.cherriesovo.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j  //日志
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private LoginService loginService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //该方法在执行controller方法之前执行
        /*
        * 1、需要判断请求的接口路径是否为HandlerMethod(controller方法)
        * 2、判断token是否为空,如果为空 未登录
        * 3、如果token不为空,登录验证loginService checkToken
        * 4、如果认证成功,放行即可
        * */
        if (!(handler instanceof HandlerMethod)){
            //handler可能是RequestResourceHandler springboot程序访问静态资源默认去classpath下的static目录去查询
            return true;
        }
        String token = request.getHeader("Authorization");
        //打印日志
        log.info("=================request start===========================");
        String requestURI = request.getRequestURI();	//获取uri,即客户端请求的资源路径
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("token:{}", token);
        log.info("=================request end===========================");

        if (token == null){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
            response.setContentType("application/json;charset=utf-8");	//设置 HTTP 响应的内容类型为 JSON 格式
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //至此是登录状态,放行

        return true;
    }
}

2.2 使拦截器生效

package com.cherriesovo.blog.config;

import com.cherriesovo.blog.handler.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMVCConfig  implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
        //本地测试 端口不一致 也算跨域
        //允许http://localhost:8080访问所有端口
        registry.addMapping("/**").allowedOrigins("http://localhost:8080");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截test接口。后续遇到需要拦截的接口时再进行配置
        registry.addInterceptor(loginInterceptor).addPathPatterns("/test");
    }
}


2.3 测试

package com.mszlu.blog.controller;

import com.mszlu.blog.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {

    @RequestMapping
    public Result test(){
        return Result.success(null);
    }
}

3. ThreadLocal保存用户信息

ThreadLocal 是 Java 提供的一个线程局部变量类,它允许我们在每个线程中存储和获取各自的值,而不会被其他线程共享。每个线程都拥有自己独立的 ThreadLocal 实例,可以在其中存储数据,这些数据对其他线程是不可见的。

ThreadLocal 的主要特点包括:

  1. 线程隔离:每个线程都拥有自己独立的 ThreadLocal 对象实例,通过该实例可以存储和获取线程私有的数据。
  2. 数据共享:在同一个线程内部,ThreadLocal 可以在多个方法之间共享数据,而不需要通过参数传递或全局变量。
  3. 线程安全:由于每个线程拥有自己的 ThreadLocal 实例,因此对 ThreadLocal 的操作是线程安全的,不会受到其他线程的干扰。
  4. 高效性:ThreadLocal 使用线程的 ThreadLocalMap 存储数据,底层是一个数组结构,查找速度快,不会引起线程间的竞争。

ThreadLocal 的常见用途包括:

  1. 保存用户上下文信息:可以在 Web 请求处理过程中将用户信息存储在 ThreadLocal 中,便于后续的业务处理方法获取用户信息,而不必每次都去查询数据库。
  2. 避免参数传递:可以在同一个线程的不同方法之间共享数据,避免参数传递的复杂性。
  3. 线程安全的日期格式化:可以使用 ThreadLocal 存储日期格式化对象,确保在多线程环境下的安全使用。

需要注意的是,由于 ThreadLocal 是与线程绑定的,因此在使用完 ThreadLocal 后需要及时清理以避免内存泄漏。通常在线程结束时,或在合适的时机调用 remove() 方法进行清理。

  1. 定义了一个 UserThreadLocal 类,用于在当前线程中存储和获取用户信息。

  2. private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();:

    声明一个 ThreadLocal 对象,用于保存用户信息。ThreadLocal 是一个线程局部变量,可以在每个线程中存储各自的值,而不会被其他线程共享。

  3. public static void put(SysUser sysUser): 将传入的 sysUser 对象存储在当前线程ThreadLocal 中。

  4. public static SysUser get(): 从当前线程ThreadLocal 中获取存储的 SysUser 对象。

  5. public static void remove(): 从当前线程ThreadLocal 中移除存储的 SysUser 对象。

package com.cherriesovo.blog.utils;

import com.cherriesovo.blog.dao.pojo.SysUser;

public class UserThreadLocal {
    private UserThreadLocal(){}

    private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();

    public static void put(SysUser sysUser){
        LOCAL.set(sysUser);
    }
    public static SysUser get(){
        return LOCAL.get();
    }
    public static void remove(){
        LOCAL.remove();
    }
}

package com.cherriesovo.blog.handler;

import com.alibaba.fastjson.JSON;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.utils.UserThreadLocal;
import com.cherriesovo.blog.vo.ErrorCode;
import com.cherriesovo.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j  //日志
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private LoginService loginService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //该方法在执行controller方法之前执行
        /*
        * 1、需要判断请求的接口路径是否为HandlerMethod(controller方法)
        * 2、判断token是否为空,如果为空 未登录
        * 3、如果token不为空,登录验证loginService checkToken
        * 4、如果认证成功,放行即可
        * */
        if (!(handler instanceof HandlerMethod)){
            //handler可能是RequestResourceHandler springboot程序访问静态资源默认去classpath下的static目录去查询
            return true;
        }
        String token = request.getHeader("Authorization");
        log.info("=================request start===========================");
        String requestURI = request.getRequestURI();
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("token:{}", token);
        log.info("=================request end===========================");

        if (token == null){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //至此是登录状态,放行
        //我希望在controller中直接获取用户信息,怎么获取
        UserThreadLocal.put(sysUser);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //如果不删除,Threadlocal中用完的信息会有内存泄漏的风险
        UserThreadLocal.remove();
    }

}

package com.cherriesovo.blog.controller;

import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.utils.UserThreadLocal;
import com.cherriesovo.blog.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {

    @RequestMapping
    public Result test(){
//        SysUser
        SysUser sysUser = UserThreadLocal.get();
        System.out.println(sysUser);
        return Result.success(null);
    }
}

posted @ 2024-04-10 19:55  CherriesOvO  阅读(8)  评论(0编辑  收藏  举报