Springboot整合Sa-Token
导入依赖
<properties>
<sa-token.version>1.33.0</sa-token.version>
</properties>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 解决LocalDateTime类型数据无法序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.14.0</version>
</dependency>
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<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>
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
yml配置
application.yml配置
sa-token:
token-name: Authorization
# token有效期,单位s 默认2小时, -1代表永不过期
timeout: 7200
# 是否允许同一账号并发登录
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token
is-share: true
# token风格
token-style: simple-uuid
# 是否输出操作日
is-log: false
# token前缀 注意必须是 Bearer {token}, Bearer后面加空格
token-prefix: Bearer
# jwt秘钥
jwt-secret-key: qwertyuiop[]\';lkjhgfdsazxcvbnm,./
spring:
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: xxx.xxx.xxx.xxx
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
代码配置
Sa-Token配置
SaTokenConfigure.java
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpLogic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaTokenConfigure {
// Sa-Token 整合 jwt (Simple 简单模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
}
MyWebMvcConfig.java
@Configuration
public class MyWebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/register", "/email", "/password/reset")
.excludePathPatterns("/swagger**/**", "/webjars/**", "/v3/**", "/doc.html", ""); // 排除 swagger拦截
}
}
全局异常处理
@ExceptionHandler(value = SaTokenException.class)
public Result notLoginException(SaTokenException e) {
log.error("权限验证错误", e);
return Result.error("401", "权限异常");
}
Redis配置
RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 key的序列化方式 防止默认的jdk序列化方式出现二进制码 看不懂
redisTemplate.setKeySerializer(new StringRedisSerializer());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
Redis中对时间的序列化处理
LDTConfig.java
public class LDTConfig {
/**
* localdatetime 序列化成 13 位时间戳
* 北京时间
*/
public static class CmzLdtSerializer extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeNumber(value.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
}
}
/**
* 将 13 位时间戳转成 localdatetime
* 北京时间
*/
public static class CmzLdtDeSerializer extends JsonDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonParser p,
DeserializationContext ctxt) throws IOException {
long timestamp = p.getLongValue();
return LocalDateTime.ofEpochSecond(timestamp / 1000, 0, ZoneOffset.ofHours(8));
}
}
}
实体类中的处理
@TableField(fill = FieldFill.INSERT)
@JsonDeserialize(using = LDTConfig.CmzLdtDeSerializer.class)
@JsonSerialize(using = LDTConfig.CmzLdtSerializer.class)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonDeserialize(using = LDTConfig.CmzLdtDeSerializer.class)
@JsonSerialize(using = LDTConfig.CmzLdtSerializer.class)
private LocalDateTime updateTime;
自动更新时间
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 配置新增策略
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
}
}
Redis工具类
@SuppressWarnings(value = {"unchecked"})
@Component
@Slf4j
public class RedisUtils {
private static RedisTemplate<String, Object> staticRedisTemplate;
private final RedisTemplate<String, Object> redisTemplate;
public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// Springboot启动成功之后会调用这个方法
@PostConstruct
public void initRedis() {
// 初始化设置 静态staticRedisTemplate对象,方便后续操作数据
staticRedisTemplate = redisTemplate;
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public static <T> void setCacheObject(final String key, final T value) {
staticRedisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public static <T> void setCacheObject(final String key, final T value, final long timeout, final TimeUnit timeUnit) {
staticRedisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public static <T> T getCacheObject(final String key) {
return (T) staticRedisTemplate.opsForValue().get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public static boolean deleteObject(final String key) {
return Boolean.TRUE.equals(staticRedisTemplate.delete(key));
}
/**
* 获取单个key的过期时间
*
* @param key
* @return
*/
public static Long getExpireTime(final String key) {
return staticRedisTemplate.getExpire(key);
}
/**
* 发送ping命令
* redis 返回pong
*/
public static void ping() {
String res = staticRedisTemplate.execute(RedisConnectionCommands::ping);
log.info("Redis ping ==== {}", res);
}
}
项目中实际使用
后端
封装登陆用户信息返回类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginDTO implements Serializable {
private static final long serialVersionUID = 1L;
private User user;
private String token;
}
登陆功能
public interface Constants {
// 用户名次前缀
String USER_NAME_PREFIX = "partner_";
// 时间的规则
String DATE_RULE_YYYYMMDD = "yyyyMMdd";
String EMAIL_CODE = "email.code.";
String LOGIN_USER_KEY = "userInfo";
String PASSWORD_KEY = "fas__ef[]d--awed[]/da(-_=wdm]";
}
@Override
public LoginDTO login(UserRequest user) {
User dbUser = null;
try {
dbUser = getOne(new UpdateWrapper<User>().eq("username", user.getUsername())
.or().eq("email", user.getUsername()));
} catch (Exception e) {
throw new RuntimeException("系统异常");
}
if (dbUser == null) {
throw new ServiceException("未找到用户");
}
String securePass = SaSecureUtil.aesEncrypt(Constants.PASSWORD_KEY, user.getPassword());
if (!securePass.equals(dbUser.getPassword())) {
throw new ServiceException("用户名或密码错误");
}
StpUtil.login(dbUser.getUid());
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, dbUser);
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
String tokenValue = StpUtil.getTokenValue();
log.info("token信息:{}", tokenInfo);
return LoginDTO.builder().user(dbUser).token(tokenValue).build();
}
前端
路由
import {createRouter, createWebHistory} from 'vue-router'
import {useUserStore} from "../stores/user";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/register',
name: 'Register',
component: () => import('../views/Register.vue')
},{
path: '/404',
name: '404',
component: () => import('../views/404.vue')
},
{
path: '/:pathMatch(.*)', // 匹配所有未知路由
redirect: '/404' // 重定向到404页面
}
]
})
// 路由守卫
router.beforeEach((to, from, next) => {
const store = useUserStore() // 拿到用户对象信息
const user = store.loginInfo.user
const hasUser = user && user.id
const noPermissionPaths = ['/login', '/register'] // 定义无需登录的路由
if (!hasUser && !noPermissionPaths.includes(to.path)) { // 用户没登录, 假如你当前跳转login页面,然后login页面没有用户信息,这个时候你再去往 login页面跳转,就会发生无限循环跳转
// 获取缓存的用户数据
// 如果to.path === '/login' 的时候 !noPermissionPaths.includes(to.path) 是返回 false的,也就不会进 next("/login")
next("/login")
} else {
next()
}
})
export default router
pinia持久化
import { defineStore } from 'pinia' // 导入 defineStore
export const useUserStore = defineStore('user', {
state: () => ({
loginInfo: {} // { user: {}, token: '' }
}),
getters: {
getUserId() {
return this.loginInfo.user ? this.loginInfo.user.id : 0
},
getUser() {
return this.loginInfo.user || {}
},
getBearerToken() {
return this.loginInfo.token ? 'Bearer ' + this.loginInfo.token : ''
},
getToken() {
return this.loginInfo.token || ""
}
},
actions: {
setLoginInfo(loginInfo) {
this.loginInfo = loginInfo
},
setUser(user) {
this.loginInfo.user = JSON.parse(JSON.stringify(user))
}
},
// 开启数据持久化
persist: true
})
封装请求工具类
import { ElMessage } from 'element-plus'
import router from '../router'
import config from "/public/config";
import axios from "axios";
import { useUserStore } from "../stores/user.js";
const request = axios.create({
baseURL: `http://${config.serverUrl}`,
// timeout: 5000 // 后台接口超时时间设置
})
// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8';
config.headers['Authorization'] = useUserStore().getBearerToken; // 设置请求头
return config
}, error => {
return Promise.reject(error)
});
// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
response => {
let res = response.data;
// 如果是返回的文件
if (response.config.responseType === 'blob') {
return res
}
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
}
// 当权限验证不通过的时候给出提示
if (res.code === '401') {
ElMessage.error(res.msg);
router.push("/login")
}
return res;
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
export default request