5、springboot3 vue3开发平台-后端- satoken 整合

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);
    }
  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不知所云,

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值