Springboot整合Sa-Token

把帆帆喂饱
• 阅读 416

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
点赞
收藏
评论区
推荐文章
Easter79 Easter79
3年前
springboot整合websocket完整版教程(粘贴复制即用)
SpringBoot2.0整合WebSocket,实现后端数据实时推送!(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fhaha12%2Fp%2F11933310.html)
Stella981 Stella981
3年前
Spring Boot整合redis
一、添加依赖<!SpringBoot整合redis的依赖<dependency<groupIdorg.springframework.boot</groupId<artifactIdspringbootstarter
Easter79 Easter79
3年前
SpringBoot2整合activiti6环境搭建
SpringBoot2整合activiti6环境搭建依赖<dependencies<dependency<groupIdorg.springframework.boot</groupId
Stella981 Stella981
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Stella981 Stella981
3年前
SpringBoot2整合activiti6环境搭建
SpringBoot2整合activiti6环境搭建依赖<dependencies<dependency<groupIdorg.springframework.boot</groupId
Stella981 Stella981
3年前
SpringBoot学习笔记6_整合Redis
三十四 SpringBoot整合Redis(结合SpringBoot整合Mybatis(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fwuba%2Fp%2F11232002.html))
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Easter79 Easter79
3年前
SpringBoot学习笔记6_整合Redis
三十四 SpringBoot整合Redis(结合SpringBoot整合Mybatis(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fwuba%2Fp%2F11232002.html))
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
把帆帆喂饱
把帆帆喂饱
Lv1
自卑溢出来就变成了安静和温柔。
文章
7
粉丝
6
获赞
6