该项目的前篇内容的使用jwt令牌实现登录认证,使用Md5加密实现注册,在上一篇:http://t.csdnimg.cn/vn3rB
该篇主要内容:redis优化登录和ThreadLocal提供线程局部变量,以及该大新闻项目的主要代码。
redis优化登录
其实在前面项目中的登录,有一个令牌机制的bug,就是在你修改密码后,原来密码的登录进去的token,还是可以使用的,旧令牌并没有失效,这会造成用户在修改密码后,但是原来密码登录进去的页面仍然可以正常访问,有很大的安全隐患。
所以使用redis来解决这个问题
令牌主动失效机制
- 登录成功后,给浏览器响应令牌的同时,把该令牌存储到 redis 中
- LoginInterceptor 拦截器中,需要验证浏览器携带的令牌,并同时需要获取到 redis 中存储的与之相同的令牌
- 当用户修改密码成功后,删除 redis 中存储的旧令牌
redis的测试代码:
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部分代码:
@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代码:
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 存储数据。
测试代码:
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代码:
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的代码:
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. 定义分组
- 2. 定义校验项时指定归属的分组
- 3. 校验时指定要校验的分组
1. 如何定义分组?
在实体类内部定义接口
2. 如何对校验项分组?通过 groups 属性指定
3. 校验时如何指定分组?给 @Validated 注解的 value 属性赋值
4. 校验项默认属于什么组 ?Default
在本项目中,category里面的新增和更新方法,需要携带的校验参数是不一样,比如:新增的id是自增的,更新的id是要修改category对应的id(那么更新就必须携带id参数),所以在实体类category里面可以使用groups进行分组
category代码:
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部分方法代码:
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里面添加:
<!-- 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部分代码:
@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的代码:
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代码:
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 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>