【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解鉴权
SpringBoot整合SaToken,token自动续期+token定期刷新+注解鉴权
目录
🥤 一、需求
1、token自动续期
当用户一直在操作页面请求服务器时,token应该是需要一直有效的,不能那个页面点着点着就告诉用户需要重新登录吧,除非是用户长时间没有请求服务器了,才需要重新登录。
实现超过指定时间没有请求服务器,重新登录,可以直接在配置文件中设置 activity-timeout 值就行。
但是token自动续期,用satoken自带的话,它需要调用 StpUtil 类里面的一些方法才会续期,但是我要的效果是不管调用哪个接口,token都会续期,所以这里需要自定义一个拦截器来实现。
2、token定期刷新
如果token长时间续期或者token的有效期很长,token值一直不变的话可能不太安全,所以需要加个定期刷新token的功能。同样在拦截器中实现,获取token的创建时间,当创建时间距离当前时间超过了两个小时,就生成一个新的token,并设置到响应头中。
3、注解鉴权
通过注解来实现角色权限认证或者是菜单权限认证等。比如某个接口只有管理员角色才能访问,或者必须具有指定权限才能进入该方法。
注意:要使用注解鉴权,只有注册satoken自带的拦截器才能用,只用自定义的拦截器是没有效果的。
🏺 二、项目搭建
本次使用的SaToken是基于1.31.0版本的
1、引入依赖
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.5.6</spring-boot.version>
<sa-token-version>1.31.0</sa-token-version>
</properties>
<dependencies>
<!-- ......省略其他依赖...... -->
<!-- sa-token权限认证框架 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token-version}</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>${sa-token-version}</version>
</dependency>
</dependencies>
2、配置文件
server:
port: 7070
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/base_project?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
username: root
password: root
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: "127.0.0.1"
port: 6379
timeout: 10s
password: 123456
database: 0
lettuce:
pool:
max-active: -1
max-wait: -1
max-idle: 16
min-idle: 8
main:
allow-bean-definition-overriding: true
servlet:
multipart:
max-file-size: -1
max-request-size: -1
aop:
auto: true
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: base-project
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 3600
# token风格
token-style: random-32
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: false
# 是否开启token自动续签
auto-renew: true
# 临时有效期,单位s,例如将其配置为 1800 (30分钟),代表用户如果30分钟无操作,则此Token会立即过期
activity-timeout: 1800
mybatis-plus:
mapper-locations: classpath:mapper/*/*.xml
type-aliases-package: com.entity.sys,;com.common.base,;com.entity.biz
global-config:
db-config:
id-type: auto
field-strategy: NOT_EMPTY
db-type: MYSQL
configuration:
map-underscore-to-camel-case: true
call-setters-on-nulls: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
filePath: upload/
3、全局配置
import cn.dev33.satoken.interceptor.SaInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.*;
/**
* 全局配置
*/
@Configuration
@EnableWebMvc
public class GlobalCorsConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String path = System.getProperty("user.dir") + System.getProperty("file.separator")+ "upload" + System.getProperty("file.separator");
registry.addResourceHandler("/upload/**").addResourceLocations("file:" + path);
}
/**
* 注册拦截器
* 关于 Sa-Token的拦截器 和 自定义的拦截器,其实也可以只选其中一个的。
* 我之所以两个都用了,是因为假如只用自带的拦截器的话,token续期只有调用 StpUtil 类里面的一些方法才会续期,但是我想要的是不管调用哪个接口,都自动续期。
* 假如只用自定义的拦截器的话,它不能用注解鉴权,有试过用 extends SaInterceptor 也还是不行,所以只能两个拦截器一起用。
* 可以根据自己实际情况选择其中一种或者两种都用。
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token的路由拦截器
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
// 注册自定义拦截器,这个拦截器用于手动刷新token过期时间
registry.addInterceptor(new CustomInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/sys/login","/sys/getCode","/sys/getKey","/api/favicon.ico","/upload/**");
}
/**
* 允许跨域调用的过滤器
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//允许所有域名进行跨域调用
config.addAllowedOriginPattern("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600)
.exposedHeaders();
}
}
4、全局异常处理
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import com.common.base.BaseConstant;
import com.common.util.ResultUtil;
import com.common.vo.ExceptionVo;
import com.common.vo.ResultVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
/**
* 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionConfig {
/**
* 自定义异常
*/
@ExceptionHandler(value = ExceptionVo.class)
public ResultVo processException(ExceptionVo e) {
log.error("位置:{} -> 错误信息:{}", e.getMethod() ,e.getMessage());
return ResultUtil.error(e.getCode(),e.getMessage());
}
/**
* 拦截表单参数校验
*/
@ResponseBody
@ExceptionHandler(BindException.class)
public ResultVo bindExceptionHandler(BindException ex) {
StringBuffer sb = new StringBuffer();
BindingResult bindingResult = ex.getBindingResult();
if (bindingResult.hasErrors()) {
for (int i = 0; i < bindingResult.getAllErrors().size(); i++) {
ObjectError error = bindingResult.getAllErrors().get(i);
sb.append((i == 0 ? "" : "\n") + error.getDefaultMessage());
}
}
return ResultUtil.error(sb.toString());
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResultVo handler(ConstraintViolationException ex) {
StringBuffer sb = new StringBuffer();
int i = 0;
for (ConstraintViolation violation : ex.getConstraintViolations()) {
sb.append((++i == 1 ? "" : "\n") + violation.getMessage());
}
return ResultUtil.error(sb.toString());
}
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResultVo httpReqMethodNotSupported(HttpRequestMethodNotSupportedException e) {
log.error("错误信息:{}", e.getLocalizedMessage());
return ResultUtil.error("请求方式不支持");
}
/**
* 未登录异常
*/
@ExceptionHandler(NotLoginException.class)
public ResultVo notLoginException(NotLoginException e) {
return ResultUtil.error(1003,"用户未登录");
}
/**
* 通用异常
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(Exception.class)
public ResultVo exception(Exception e) {
if (e instanceof NotPermissionException){
return ResultUtil.error("没有操作权限");
}
e.printStackTrace();
return ResultUtil.error(BaseConstant.UNKNOWN_EXCEPTION);
}
}
5、自定义拦截器(token续期 和 定期刷新)
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.common.base.BaseConstant;
import com.common.util.RedisUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义拦截器(token续期 和 token定期刷新)
*/
public class CustomInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
response.setHeader( "Set-Cookie" , "cookiename=httponlyTest;Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly");
response.setHeader( "Content-Security-Policy" , "default-src 'self'; script-src 'self'; frame-ancestors 'self'");
response.setHeader("Access-Control-Allow-Origin", (request).getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Referrer-Policy","no-referrer");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// 获取当前token(这个token获取的是请求头的token,也可以用 request 获取)
String tokenValue = StpUtil.getTokenValue();
// 根据token获取用户id(这里如果找不到id直接返回null,不会报错)
String loginId = (String) StpUtil.getLoginIdByToken(tokenValue);
//判断token的创建时间是否大于2小时,如果是的话则需要生成新的token
long time = System.currentTimeMillis() - StpUtil.getSession().getCreateTime();
long hour = time/1000/(60 * 60);
if (hour>2){
/**
* TODO: 生成新的token有两种方式:
* 方式一:先退出,然后再重新登录:退出之前得先把session中的用户信息拿出来,登录之后重新设置到session中。
* 方式二:重新登录,并且重写token生成方式:重新token后,redis中以token值为key的旧token还存在于redis中,得手动删除
*/
// TODO 方式一:获取session中存储的用户信息,重新登录后,将这个用户信息重新设置到session中。
/*SysUser user = (SysUser) StpUtil.getSession().get("user");
StpUtil.logout(loginId); // 这里要生成新的token的话,要先退出再重新登录
StpUtil.login(loginId); // 然后再重新登录,生成新的token
String newToken = StpUtil.getTokenValue();
StpUtil.getSession().set("user",user);*/
// TODO 方式二:重新登录,并且重写token生成方式,并且把redis中旧token手动删除
StpUtil.login(loginUserId);
SaStrategy.me.createToken = (loginId, loginType) -> {
return SaFoxUtil.getRandomString(32); // 生成新的token,随机32位长度字符串
};
String newToken = StpUtil.getTokenValue();
RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class);
redisUtil.del(BaseConstant.tokenCachePrefix+tokenValue);// 删除旧token
response.setHeader(BaseConstant.tokenHeader, newToken);
}
long tokenTimeout = StpUtil.getTokenTimeout();// 获取过期时间
//token没过期,过期时间不是-1的时候,每次请求都刷新过期时间
if (tokenTimeout != -1){
StpUtil.renewTimeout(3600);// 用于token续期
StpUtil.updateLastActivityToNow();
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
6、自定义权限验证接口
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import com.entity.sys.SysMenu;
import com.entity.sys.SysUser;
import com.entity.sys.query.SysQuery;
import com.service.sys.SysMenuService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 自定义权限验证接口
*/
@Component
public class PermissionInterface implements StpInterface {
@Resource
private SysMenuService sysMenuService;
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
List<String> list = new ArrayList<String>();
// 2. 遍历角色列表,查询拥有的权限码
for (String roleId : getRoleList(loginId, loginType)) {
SysQuery queryVo = new SysQuery();
queryVo.setId(roleId);
//查询角色和权限(这里根据业务自行查询)
List<SysMenu> menuList = sysMenuService.selectPermsByRoleId(queryVo);
List<String> collect = menuList.stream().map(SysMenu::getPerms).collect(Collectors.toList());
list.addAll(collect);
}
return list;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
List<String> list = new ArrayList<>();
SysUser user = (SysUser) StpUtil.getSession().get("user");
list.add(user.getRoleId());
return list;
}
}
🥃 三、注解鉴权
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.common.base.BaseConstant;
import com.common.base.BaseController;
import com.common.log.Log;
import com.common.util.*;
import com.common.vo.ResultVo;
import com.entity.sys.SysUser;
import com.entity.sys.query.SysQuery;
import com.service.sys.SysSafeService;
import com.service.sys.SysUserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.time.LocalDateTime;
import java.util.*;
/**
* 用户信息
*/
@RestController
@RequestMapping("/sys/user")
public class SysUserController extends BaseController<SysUserService,SysUser> {
@Resource
private SysSafeService sysSafeService;
@Value("${filePath}")
private String path;
/**
* 获取当前登录用户信息
*/
@GetMapping("/getLoginUser")
public ResultVo getLoginUser() {
Map<String,Object> map = new HashMap<>();
SysUser user = service.getCurrentUser();
//判断密码是否过了有效期
if (user.getUpdatePwdTime() == null){
SysUser byId = service.getById(user.getId());
user.setUpdatePwdTime(byId.getUpdatePwdTime());
}
sysSafeService.getPwdCycle(map,user.getUpdatePwdTime());
map.put("user",user);
map.put("roles",StpUtil.getRoleList()); // 当前用户的角色列表
map.put("permissions",StpUtil.getPermissionList()); // 当前用户的权限列表
return ResultUtil.success(map);
}
/**
* 用户列表
*/
@GetMapping("/list")
@SaCheckPermission("system:user:view")
public ResultVo list(SysQuery query) {
return ResultUtil.success(service.page(query));
}
/**
* 根据id获取用户
*/
@GetMapping("/getById/{id}")
public ResultVo getById(@PathVariable("id")String id) {
return ResultUtil.success(service.getUserById(id));
}
/**
* 新增
*/
@Log(title = "新增用户")
@PostMapping("/insert")
@SaCheckPermission("system:user:add")
public ResultVo insert(@RequestBody SysUser user) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (service.checkUserNameAndPhone(user.getUserName(), user.getId())) {
return ResultUtil.error("用户名已存在");
}
String password = RSAUtil.decrypt(user.getPassword());// 密码私钥解密
String salt = EncryptionUtil.generateSalt();
// 盐值加密
password = EncryptionUtil.getEncryptedPassword(password, salt);
user.setSalt(salt);
user.setPassword(password);
user.setId(IdUtil.getSnowflakeNextIdStr());
service.save(user);
return ResultUtil.success();
}
/**
* 修改用户
*/
@Log(title = "修改用户")
@PostMapping("/update")
@SaCheckPermission("system:user:update")
public ResultVo update(@RequestBody @Validated SysUser user) {
if (service.checkUserNameAndPhone(user.getUserName(), user.getId())) {
return ResultUtil.error("用户名已存在");
}
service.updateById(user);
SysUser currentUser = service.getCurrentUser();
// 如果修改的用户信息是当前登录用户的话,将最新的用户信息重新设置到session中
if (user.getId().equals(currentUser.getId())){
SysUser userById = service.getUserById(user.getId());
service.setDataScope(userById); // 设置用户的数据范围查询条件
StpUtil.getSession().set("user",userById);
}
return ResultUtil.success();
}
/**
* 删除用户
*/
@Log(title = "删除用户")
@DeleteMapping("/delete/{id}")
@SaCheckPermission("system:user:delete")
public ResultVo delete(@PathVariable("id")String id) {
String [] ids = Convert.toStrArray(id);
if (service.removeByIds(Arrays.asList(ids))){
return ResultUtil.success();
}
return ResultUtil.error("删除失败");
}
/**
* 修改密码
*/
@Log(title = "修改密码")
@PostMapping("/updatePassword")
@SaCheckPermission("system:user:updatePassword")
public ResultVo updatePassword(String oldPassword,String newPassword,String userId) throws InvalidKeySpecException, NoSuchAlgorithmException {
//私钥解密
oldPassword = RSAUtil.decrypt(oldPassword);
newPassword = RSAUtil.decrypt(newPassword);
if (StrUtil.isEmpty(oldPassword) || StrUtil.isEmpty(newPassword)){
return ResultUtil.error("数据解密失败");
}
if (BaseConstant.initPassword.equals(newPassword)){
return ResultUtil.error("新密码不可与初始密码相同");
}
SysUser user = service.getById(userId);
//对旧密码进行加密对比
boolean authenticate = EncryptionUtil.authenticate(oldPassword, user.getPassword(), user.getSalt());
if (!authenticate){
return ResultUtil.error("旧密码错误");
}
String salt = EncryptionUtil.generateSalt();
String password = EncryptionUtil.getEncryptedPassword(newPassword, salt);
UpdateWrapper<SysUser> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("password", password);
updateWrapper.set("salt", salt);
updateWrapper.set("update_pwd_time", LocalDateTime.now());
updateWrapper.eq("id",user.getId());
service.update(updateWrapper);
return ResultUtil.success();
}
}
上面使用的注解是 @SaCheckPermission 注解来校验菜单权限,当用户对应的角色没有分配这个菜单时,在全局异常里面有配置会提示 “没有操作权限”。其他注解也是类似的,具体使用可以看下官方文档,很详细。
三、源码
完整的数据库和代码都在源码里。
查看下一篇:【SaToken使用】SpringBoot整合SaToken(二)关于数据权限
希望大家多多点赞支持一下(点个赞再走吧~)
更多推荐
所有评论(0)