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