SpringBoot | 大新闻项目后端(redis优化登录)

该项目的前篇内容的使用jwt令牌实现登录认证,使用Md5加密实现注册,在上一篇:http://t.csdnimg.cn/vn3rB

该篇主要内容:redis优化登录和ThreadLocal提供线程局部变量,以及该大新闻项目的主要代码。

redis优化登录

其实在前面项目中的登录,有一个令牌机制的bug,就是在你修改密码后,原来密码的登录进去的token,还是可以使用的,旧令牌并没有失效,这会造成用户在修改密码后,但是原来密码登录进去的页面仍然可以正常访问,有很大的安全隐患。

所以使用redis来解决这个问题

令牌主动失效机制

  • 登录成功后,给浏览器响应令牌的同时,把该令牌存储到 redis 中
  • LoginInterceptor 拦截器中,需要验证浏览器携带的令牌,并同时需要获取到 redis 中存储的与之相同的令牌
  • 当用户修改密码成功后,删除 redis 中存储的旧令牌

redis的测试代码:

java 复制代码
package com.xu;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.concurrent.TimeUnit;

@SpringBootTest //如果在测试类上添加了这个注释,那么将来单元测试方法执行之前,会先初始化Spring容器
public class RedisTest {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void testSet(){
        //让redis中存储一个键值对 StringRedisTemplate
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();

        operations.set("username","zhangsan");
        operations.set("id","1",15, TimeUnit.SECONDS);

    }
    @Test
    public void testGet(){
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();

        System.out.println(operations.get("username"));
    }
}

运行效果:

其实里面的id是设置了失效的时间,所以在超出时间的范围外,则get不到id的值。

在整个项目的代码中,redis的使用也是类似:

UserController部分代码:

java 复制代码
@Autowired
private UserService userService;
@Autowired
private StringRedisTemplate stringRedisTemplate;

@PostMapping("login")
    public Result<String> login(@Pattern(regexp = "^\\S{5,16}$")String username, @Pattern(regexp = "^\\S{5,16}$")String password){
        //根据用户名查询用户
        User loginUser=userService.findByUserName(username);

        //判断用户是否存在
        if(loginUser==null){
            return Result.error("用户名错误");
        }

        //判断密码是否正确
        if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
            //登录成功
            Map<String,Object> claims=new HashMap<>();
            claims.put("id",loginUser.getId());
            claims.put("username",loginUser.getUsername());
            String token= JwtUtil.genToken(claims);

            //把token存储到redis里面
            ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
            operations.set(token,token,1, TimeUnit.HOURS);

            return Result.success(token);
        }
        return Result.error("密码错误");

    }



    @PatchMapping("updatePwd")
    public Result updatePwd(@RequestBody Map<String,String> params,@RequestHeader("Authorization") String token){
        //校验参数
        String oldPwd = params.get("old_pwd");
        String newPwd = params.get("new_pwd");
        String rePwd = params.get("re_pwd");

        if(!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)){
            return Result.error("缺失必要的参数");
        }
        //原密码是否正确
        //调用userService根据用户名拿到原密码,再和old_pwd比对
        Map<String,Object> map=ThreadLocalUtil.get();
        String username=(String) map.get("username");
        User loginUser=userService.findByUserName(username);
        if(!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){
            return Result.error("原密码填写不正确");
        }
        //newPwd和rePwd是否一样
        if(!rePwd.equals(newPwd)){
            return Result.error("两次填写的新密码不一样");
        }
        //调用service完成密码更新
        userService.updatePwd(newPwd);
        //删除redis中对应的token
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        operations.getOperations().delete(token);
        return Result.success();

    }

LoginInterceptor代码:

java 复制代码
package com.xu.interceptors;

import com.xu.pojo.Result;
import com.xu.utils.JwtUtil;
import com.xu.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Map;

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //令牌验证
        String token=request.getHeader("Authorization");
        //验证token
        try {
            //从redis中获取相同的token
            ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
            String redisToken = operations.get(token);
            if(redisToken==null){
                //token已经失效
                throw new RuntimeException();
            }

            Map<String,Object> claims= JwtUtil.parseToken(token);

            //把业务数据存储到ThreadLocal中
            ThreadLocalUtil.set(claims);
            //放行
            return true;
        }catch (Exception e){
            response.setStatus(401);
            //不放行
            return false;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清空ThreadLocal中的数据
        ThreadLocalUtil.remove();
    }
}

ThreadLocal

适用场景ThreadLocal 适用于每个线程需要独立的实例或数据的场景,不适用于需要线程间共享数据的场景。

  • 用来存取数据 : set()/get()
  • 使用 ThreadLocal 存储的数据 , 线程安全
  • 用完记得调用 remove 方法释放

而在本项目中文章分类和文章管理都是通过用户去操作的,所以适合用ThreadLocal 存储数据。

测试代码:

java 复制代码
package com.xu;

import org.junit.jupiter.api.Test;

public class ThreadLocalSetAndGet {

    @Test
    public void testThreadLocalSetAndGet(){
        //提供一个ThreadLocal对象
        ThreadLocal tl=new ThreadLocal();

        //开启两个线程
        new Thread(()->{
            tl.set("cookie");
            System.out.println(Thread.currentThread().getName()+":"+tl.get());
            System.out.println(Thread.currentThread().getName()+":"+tl.get());
            System.out.println(Thread.currentThread().getName()+":"+tl.get());

        },"pig").start();

        new Thread(()->{
            tl.set("offer");
            System.out.println(Thread.currentThread().getName()+":"+tl.get());
            System.out.println(Thread.currentThread().getName()+":"+tl.get());
            System.out.println(Thread.currentThread().getName()+":"+tl.get());

        },"lucky").start();
    }
}

运行结果:

ThreadLocalUtil代码:

java 复制代码
package com.xu.utils;

import java.util.HashMap;
import java.util.Map;

/**
 * ThreadLocal 工具类
 */
@SuppressWarnings("all")
public class ThreadLocalUtil {
    //提供ThreadLocal对象,
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    //根据键获取值
    public static <T> T get(){
        return (T) THREAD_LOCAL.get();
    }
	
    //存储键值对
    public static void set(Object value){
        THREAD_LOCAL.set(value);
    }


    //清除ThreadLocal 防止内存泄漏
    public static void remove(){
        THREAD_LOCAL.remove();
    }
}

ArticleServiceImpl部分使用到了ThreadLocal的代码:

java 复制代码
package com.xu.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.xu.mapper.ArticleMapper;
import com.xu.pojo.Article;
import com.xu.pojo.PageBean;
import com.xu.service.ArticleService;
import com.xu.utils.ThreadLocalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@Service
public class ArticleServiceImpl implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper;

    @Override
    public void add(Article article) {
        //补充属性值
        article.setCreateTime(LocalDateTime.now());
        article.setUpdateTime(LocalDateTime.now());

        Map<String,Object> map= ThreadLocalUtil.get();
        Integer userId=(Integer) map.get("id");
        article.setCreateUser(userId);

        articleMapper.add(article);
    }

    @Override
    public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
        //创建PageBean对象
        PageBean<Article> pb=new PageBean<>();

        //开启分页查询 PageHelper
        PageHelper.startPage(pageNum,pageSize);

        //调用mapper
        Map<String,Object> map=ThreadLocalUtil.get();
        Integer userId=(Integer)map.get("id");
        List<Article> as= articleMapper.list(userId,categoryId,state);
        Page<Article> p=(Page<Article>) as;

        //把数据填充到PageBean对象
        pb.setTotal(p.getTotal());
        pb.setItems(p.getResult());
        return pb;
    }
}

分组校验

把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项

    1. 定义分组
    1. 定义校验项时指定归属的分组
    1. 校验时指定要校验的分组
  1. 如何定义分组?

在实体类内部定义接口

  1. 如何对校验项分组?

通过 groups 属性指定

  1. 校验时如何指定分组?

给 @Validated 注解的 value 属性赋值

  1. 校验项默认属于什么组 ?

Default

在本项目中,category里面的新增和更新方法,需要携带的校验参数是不一样,比如:新增的id是自增的,更新的id是要修改category对应的id(那么更新就必须携带id参数),所以在实体类category里面可以使用groups进行分组

category代码:

java 复制代码
package com.xu.pojo;

import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.groups.Default;
import lombok.Data;

import java.time.LocalDateTime;
@Data
public class Category {
    @NotNull(groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty
    private String categoryName;//分类名称
    @NotEmpty
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;//创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;//更新时间

    //如果说某个校验项没有指定分组,默认属于Default分组
    //分组之间可以继承, A extends B  那么A中拥有B中所有的校验项


    public interface Add extends Default {

    }

    public interface Update extends Default {

    }
}

CategoryController部分方法代码:

java 复制代码
package com.xu.controller;

import com.xu.pojo.Category;
import com.xu.pojo.Result;
import com.xu.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;
    @PostMapping
    public Result add(@RequestBody @Validated(Category.Add.class) Category category){
        categoryService.add(category);
        return Result.success();
    }

   
    @PutMapping
    public Result update(@RequestBody @Validated(Category.Update.class) Category category) {
        categoryService.update(category);
        return Result.success();
    }
}

使用上面这些,需要在pom.xml里面添加:

XML 复制代码
<!--      validation依赖-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-validation</artifactId>
      </dependency>

<!--      redis坐标-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>

大新闻项目的重要业务有文件上传,分页查询以及文章管理(增删改查)等,

以下是一些难点的业务:

文件上传:

文件上传里面使用了UUID是为了防止相同文件名的,被覆盖,所以就使用UUID生成随机的文件名

分页查询:

ArticleController部分代码:

java 复制代码
    @GetMapping
    public Result<PageBean<Article>> list(
            Integer pageNum,
            Integer pageSize,
            @RequestParam(required = false) Integer categoryId,
            @RequestParam(required = false) String state
    ){
        PageBean<Article> pb=articleService.list(pageNum,pageSize,categoryId,state);
        return Result.success(pb);
    }

ArticleServiceImpl的代码:

java 复制代码
package com.xu.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.xu.mapper.ArticleMapper;
import com.xu.pojo.Article;
import com.xu.pojo.PageBean;
import com.xu.service.ArticleService;
import com.xu.utils.ThreadLocalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@Service
public class ArticleServiceImpl implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper;

    @Override
    public void add(Article article) {
        //补充属性值
        article.setCreateTime(LocalDateTime.now());
        article.setUpdateTime(LocalDateTime.now());

        Map<String,Object> map= ThreadLocalUtil.get();
        Integer userId=(Integer) map.get("id");
        article.setCreateUser(userId);

        articleMapper.add(article);
    }

    @Override
    public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
        //创建PageBean对象
        PageBean<Article> pb=new PageBean<>();

        //开启分页查询 PageHelper
        PageHelper.startPage(pageNum,pageSize);

        //调用mapper
        Map<String,Object> map=ThreadLocalUtil.get();
        Integer userId=(Integer)map.get("id");
        List<Article> as= articleMapper.list(userId,categoryId,state);
        Page<Article> p=(Page<Article>) as;

        //把数据填充到PageBean对象
        pb.setTotal(p.getTotal());
        pb.setItems(p.getResult());
        return pb;
    }
}

ArticleMapper代码:

java 复制代码
package com.xu.mapper;

import com.xu.pojo.Article;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface ArticleMapper {
    //新增
    @Insert("insert into article(title,content,cover_img,state,category_id,create_user,create_time,update_time) "+
    "values(#{title},#{content},#{coverImg},#{state},#{categoryId},#{createUser},#{createTime},#{updateTime})")
    void add(Article article);


    List<Article> list(Integer userId, Integer categoryId, String state);
}

这里使用到了动态sql,要保证在resource目录下的路径映射和mapper的一样:

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xu.mapper.ArticleMapper">
<!--    动态sql-->
    <select id="list" resultType="com.xu.pojo.Article">
        select * from article
        <where>
            <if test="categoryId!=null">
                category_id=#{categoryId}
            </if>
            <if test="state!=null">
                and state=#{state}
            </if>
            and create_user=#{userId}
        </where>
    </select>
</mapper>
相关推荐
老秦包你会1 分钟前
Qt第三课 ----------容器类控件
开发语言·qt
凤枭香4 分钟前
Python OpenCV 傅里叶变换
开发语言·图像处理·python·opencv
雷神乐乐5 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
ULTRA??8 分钟前
C加加中的结构化绑定(解包,折叠展开)
开发语言·c++
码农派大星。8 分钟前
Spring Boot 配置文件
java·spring boot·后端
王佑辉12 分钟前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
顾北川_野15 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航18 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
远望清一色24 分钟前
基于MATLAB的实现垃圾分类Matlab源码
开发语言·matlab
confiself34 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言