文章目录
- 1. 为什么使用sa-token
- 2. 依赖导入jichu
- 2.1 基础依赖引入
- 2.2 redis整合
- 2.3 redis 配置, 使redis能支持中文存储
- 3. 配置
- 4. 配置使用
- 4.1 权限加载接口实现, 登录实现
- 4.2 配置全局过滤器
- 4.3 登录异常处理
- 5. 登录测试
- 6. 用户session的获取
1. 为什么使用sa-token
使用简单, 自由度相对较高, 不想费工夫配shior, 不想费脑子学SpringSecurity
官网: https://sa-token.cc/doc.html#/up/integ-redis
2. 依赖导入jichu
2.1 基础依赖引入
父模块引入
support模块引入依赖:
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
若不需要整合redis的话引入上述依赖即可, 用户信息会存到框架内置CurrentHashMap中
2.2 redis整合
windows redis 安装参考: https://blog.csdn.net/qq_51355375/article/details/140726275
- 为什么使用redis , 官网解释很好,抄过来:
Sa-Token 默认将数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,
但是此模式也有一些缺点,比如:
1、重启后数据会丢失。
2、无法在分布式环境中共享数据。
为此,Sa-Token 提供了扩展接口,你可以轻松将会话数据存储在一些专业的缓存中间件上(比如 Redis), 做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。
sa提供两种方式整合,官网也说的很清楚, 这里使用第二种
注: 集成 Redis 后,框架自动保存数据。集成 Redis 只需要引入对应的 pom依赖 即可,框架所有上层 API 保持不变, 所以集成redis 影响的只是框架内部数据存储,对于使用框架来说用不用redis完全相同。
在父工程引入依赖版本控制:
support模块引入依赖:
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
记得引入连接池:
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2.3 redis 配置, 使redis能支持中文存储
package com.ylp.support.config.reidis;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类,更换默认序列化器
*
*/
@Configuration
public class RedisConfig {
/**
* 创建 RedisTemplate Bean,用于操作 Redis 数据库。
*
* @param connectionFactory Redis 连接工厂
* @return RedisTemplate 实例
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 创建 StringRedisSerializer 实例,用于序列化和反序列化 Redis 的键和哈希键
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 创建 GenericJackson2JsonRedisSerializer 实例,用于序列化和反序列化 Redis 的值和哈希值
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置默认序列化器为 StringRedisSerializer
redisTemplate.setDefaultSerializer(stringRedisSerializer);
// 设置 RedisTemplate 的键序列化器为 StringRedisSerializer
redisTemplate.setKeySerializer(stringRedisSerializer);
// 设置 RedisTemplate 的哈希键序列化器为 StringRedisSerializer
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// 设置 RedisTemplate 的值序列化器为 GenericJackson2JsonRedisSerializer
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
// 设置 RedisTemplate 的哈希值序列化器为 GenericJackson2JsonRedisSerializer
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
return redisTemplate;
}
}
3. 配置
spring:
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 使用druid连接池
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/admin_system?serverTimezone=UTC
username: root
password: root
# redis配置
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
is-read-cookie: false # 是否从Cookie中读取Token,设置为false
is-read-header: true # 是否从Header中读取Token,设置为true
4. 配置使用
关于sa配置和使用相关的类文件按项目结构存放, 这里放在sys模块auth包下,包含sa配置使用,登录鉴权信息等
4.1 权限加载接口实现, 登录实现
获取当前账号权限码集合, 需要实现StpInterface接口, 缓存当前用户权限角色信息
package com.ylp.sys.auth.config;
import cn.dev33.satoken.stp.StpInterface;
import com.ylp.sys.auth.service.impl.AuthServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 自定义权限加载接口实现类
*/
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
private AuthServiceImpl authService;
@Autowired
public void setAuthService(AuthServiceImpl authService) {
this.authService = authService;
}
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = authService.getPermissionListByUserId((Long) loginId);
return list;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = authService.getRoleLabelListByUserId((Long) loginId);
return list;
}
}
在AuthServiceImpl 中编写根据用户id 加载 权限和角色列表的函数, 账号密码检验成功调用 StpUtil.login(user.getId()) 登录, 并获取生成的token信息返回给前端。
package com.ylp.sys.auth.service.impl;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ylp.common.constants.HttpStatus;
import com.ylp.common.exception.ServiceException;
import com.ylp.common.response.Result;
import com.ylp.common.utils.StringUtils;
import com.ylp.sys.auth.config.cache.CurrentHashMapManager;
import com.ylp.sys.auth.entity.LoginDto;
import com.ylp.sys.auth.entity.LoginVo;
import com.ylp.sys.auth.entity.UserInfo;
import com.ylp.sys.auth.service.AuthService;
import com.ylp.sys.auth.utils.BCryptUtils;
import com.ylp.sys.auth.utils.ValidataCodeUtil;
import com.ylp.sys.domain.entity.*;
import com.ylp.sys.mapper.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class AuthServiceImpl implements AuthService {
private static final Logger log = LoggerFactory.getLogger(AuthServiceImpl.class);
private final SysUserMapper userMapper;
private final CurrentHashMapManager currentHashMapManager;
public AuthServiceImpl(SysUserMapper sysUserMapper, CurrentHashMapManager currentHashMapManager) {
this.userMapper = sysUserMapper;
this.currentHashMapManager = currentHashMapManager;
}
@Override
public LoginVo login(LoginDto loginDto) {
SysUser user = getUserByName(loginDto.getUsername());
if (ObjectUtil.isNull(user)) {
throw new ServiceException(HttpStatus.UNAUTHORIZED,"认证失败!");
}
if (!BCryptUtils.checkpw(loginDto.getPassword(), user.getPassword())) {
throw new ServiceException(HttpStatus.UNAUTHORIZED,"密码错误!");
}
LoginVo loginVo = null;
try {
loginVo= getLoginVo(user);
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException(HttpStatus.UNAUTHORIZED, "用户相关信息错误");
}
// 登录,获取tokenInfo , 存储用户session
StpUtil.login(user.getId()); // 登录
// 设置用户信息
StpUtil.getSession().set("userInfo", new UserInfo(user.getId(), user.getUsername()));
// 获取token
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
loginVo.setSaTokenInfo(tokenInfo);
return loginVo;
}
/**
* 根据账号查用户
* @param userName
* @return
*/
public SysUser getUserByName(String userName) {
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.isNotEmpty(userName), SysUser::getUsername, userName);
wrapper.eq(SysUser::getStatus, 0);
return userMapper.selectOne(wrapper);
}
private LoginVo getLoginVo(SysUser user) {
LoginVo loginVo = new LoginVo();
loginVo.setUserInfo(user);
// 查询角色标识
List<String> roleLabelList = getRoleLabelListByUserId(user.getId());
loginVo.setRoleLabelList(roleLabelList);
// 权限列表
List<String> permissionList = getPermissionListByUserId(user.getId());
loginVo.setPermissionList(permissionList);
return loginVo;
}
/**
* 根据用户id查询权限列表
* @param userId
* @return
*/
public List<String> getPermissionListByUserId(Long userId) {
List<String> perms = new ArrayList<>();
try {
perms = userMapper.selectPermsByUserId(userId);
} catch (Exception e) {
e.printStackTrace();
}
return perms;
}
/**
* 根据用户id查角色标识列表
* @param userId
* @return
*/
public List<String> getRoleLabelListByUserId(Long userId) {
List<String> roleLabelList = new ArrayList<>();
try {
roleLabelList = userMapper.selectRoleLabelByUserId(userId);
} catch (Exception e) {
e.printStackTrace();
}
return roleLabelList;
}
}
usermapper接口:
public interface SysUserMapper extends BaseMapper<SysUser> {
/**
* 根据用户id获取权限咧列表
* @param id
* @return
*/
@Select("SELECT m.perms FROM sys_user u JOIN sys_user_role ur ON u.id = ur.user_id " +
"JOIN sys_role_menu rm ON ur.role_id = rm.role_id " +
"JOIN sys_menu m ON rm.menu_id = m.id where u.id = #{id};")
List<String> selectPermsByUserId(@Param("id")Long id);
@Select("SELECT r.role_label from sys_user_role ur JOIN sys_role r ON ur.role_id = r.id WHERE ur.user_id = #{id}")
List<String> selectRoleLabelByUserId(@Param("id") Long id);
}
AuthController 调用接口:
package com.ylp.sys.auth.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.ylp.common.response.Result;
import com.ylp.sys.auth.entity.LoginDto;
import com.ylp.sys.auth.entity.LoginVo;
import com.ylp.sys.auth.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.*;
@Tag(name = "登录认证接口")
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
@Operation(summary = "用户登录")
public Result<LoginVo> login(@RequestBody LoginDto loginDto) {
LoginVo loginVo = authService.login(loginDto);
return Result.success("登录成功", loginVo);
}
@Operation(summary = "退出登录")
@GetMapping("loginOut")
public Result<String> loginOut() {
StpUtil.logout();
return Result.success("退出登录", null);
}
}
4.2 配置全局过滤器
package com.ylp.sys.auth.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置放行资源
// 无需拦截的接口集合
List<String> ignorePath = new ArrayList<>();
// knife4j(swagger)
ignorePath.add("/swagger-resources/**");
ignorePath.add("/swagger-ui.html");
ignorePath.add("/doc.html");
ignorePath.add("/v3/**");
ignorePath.add("/webjars/**");
ignorePath.add("/static/**");
ignorePath.add("/templates/**");
ignorePath.add("/error");
// 登录页面
ignorePath.add("/auth/login");
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns(ignorePath);
}
}
4.3 登录异常处理
在全局异常中拦截sa登录异常,返回给前端信息
package com.ylp.support.config.execption;
import cn.dev33.satoken.exception.NotLoginException;
import com.ylp.common.exception.ServiceException;
import com.ylp.common.response.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 业务异常
*/
@ExceptionHandler(ServiceException.class)
public Result handleServiceException(ServiceException e)
{
log.error(e.getMessage(), e);
return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败");
}
/**
* 服务异常
*/
@ExceptionHandler(Exception.class)
public Result handlerException(Exception e) {
log.warn(e.getMessage());
return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "服务异常");
}
/**
* 登录异常
* @param notLoginException
* @return
*/
@ExceptionHandler(NotLoginException.class)
public ResponseEntity<Result> handleNotLoginException(NotLoginException notLoginException) {
Result result = Result.error("请登录");
// 构建响应体
return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED);
}
}
5. 登录测试
使用knife4j 测试
登录成功,查看redis内容已缓存用户登录信息和session,token信息:
6. 用户session的获取
@Operation(summary = "查询当前用户信息")
@GetMapping("/currentUser")
public Result<SysUser> currenUser() {
UserInfo userLoginInfo = (UserInfo) StpUtil.getSession().get("userInfo");
Long id = userLoginInfo.getId();
return userService.getUserById(id);
}