該項目的前篇內容的使用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 {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testpublic void testSet(){//讓redis中存儲一個鍵值對 StringRedisTemplateValueOperations<String, String> operations = stringRedisTemplate.opsForValue();operations.set("username","zhangsan");operations.set("id","1",15, TimeUnit.SECONDS);}@Testpublic 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中對應的tokenValueOperations<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 {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//令牌驗證String token=request.getHeader("Authorization");//驗證tokentry {//從redis中獲取相同的tokenValueOperations<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;}}@Overridepublic 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 {@Testpublic 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 {@Autowiredprivate ArticleMapper articleMapper;@Overridepublic 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);}@Overridepublic PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {//創建PageBean對象PageBean<Article> pb=new PageBean<>();//開啟分頁查詢 PageHelperPageHelper.startPage(pageNum,pageSize);//調用mapperMap<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@NotEmptyprivate String categoryName;//分類名稱@NotEmptyprivate 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 {@Autowiredprivate CategoryService categoryService;@PostMappingpublic Result add(@RequestBody @Validated(Category.Add.class) Category category){categoryService.add(category);return Result.success();}@PutMappingpublic 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部分代碼:
@GetMappingpublic 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 {@Autowiredprivate ArticleMapper articleMapper;@Overridepublic 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);}@Overridepublic PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {//創建PageBean對象PageBean<Article> pb=new PageBean<>();//開啟分頁查詢 PageHelperPageHelper.startPage(pageNum,pageSize);//調用mapperMap<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 mapperPUBLIC "-//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>