Sa-Token介绍与SpringBoot环境下使用

个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview


官网:Sa-Token

一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!

介绍

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

官方文档写的已经非常好了。引用官方文档开头的一段话:

本文档将会尽力讲解每个功能的设计原因、应用场景,用心阅读文档,你学习到的将不止是 Sa-Token 框架本身,更是绝大多数场景下权限设计的最佳实践。

确实,通过阅读官方文档有学到很多东西,收获更大的是结合我的使用体验,下载并阅读源码后有学到了一些东西想和大家分享!这篇文章只是开头。

使用

1、准备工作

环境:

Spring Boot2.7.3

Sa-Token1.37.0

2、配置文件

关于sa-token的配置文件如下

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: Authorization
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 3600
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: 1800
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: false
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: true
  # 是否尝试从header里读取token
  is-read-header: true
  # 是否尝试从cookie里读取token
  is-read-cookie: false
  # token前缀
  token-prefix: "Bearer"
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: tik
  # 是否输出操作日志
  is-log: true

3、自定义权限认证

官方文档:因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以 Sa-Token 将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。

所以根据自己的不同业务设计需要的权限认证方法就好。下面是我项目中的写法,仅供参考。

@Setter
public class StpInterfaceImpl implements StpInterface {

    /**
     * 登录服务
     */
    private LoginService loginService;

    /**
     * 返回指定账号id所拥有的权限码集合
     *
     * @param loginId   账号id
     * @param loginType 账号类型
     * @return 该账号id具有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        Login loginUser = loginService.getLoginUser();
        UserTypeEnum userType = UserTypeEnum.valueOf(loginUser.getType());
        if (userType == UserTypeEnum.PC) {
            return new ArrayList<>(loginUser.getPermissions());
        }

        return new ArrayList<>();
    }

    /**
     * 返回指定账号id所拥有的角色标识集合
     *
     * @param loginId   账号id
     * @param loginType 账号类型
     * @return 该账号id具有的角色标识集合
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        Login loginUser = loginService.getLoginUser();
        UserTypeEnum userType = UserTypeEnum.valueOf(loginUser.getType());
        if (userType == UserTypeEnum.PC) {
            return new ArrayList<>(loginUser.getRoleValues());
        }

        return new ArrayList<>();
    }
}

接着注册StpInterfaceImpl

@Configuration
public class SaTokenConfiguration {

    @Bean
    public LoginService loginService() {
        return new LoginServiceImpl();
    }

    @Bean
    public StpInterface stpInterface(LoginService loginService) {
        StpInterfaceImpl stpInterface = new StpInterfaceImpl();
        stpInterface.setLoginService(loginService);
        return stpInterface;
    }
}

4、Controller

4.1、工具类鉴权

satoken提供了StpUtill鉴权工具类,其中包含非常多的静态方法可以使用,详情请参考:Sa-Token

// 会话登录,参数填登录人的账号id 
StpUtil.login(10001);
// 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常
StpUtil.checkLogin();
// 将账号id为 10077 的会话踢下线 
StpUtil.kickout(10077);

4.2、注解鉴权

尽管StpUtill鉴权工具类已经非常方便但还是有同学钟爱注解鉴权,satoken也是提供了这种方式的。

// 注解鉴权:只有具备 `user:add` 权限的会话才可以进入方法
@SaCheckPermission("user:add")
public String insert(SysUser user) {
    // ... 
    return "用户增加";
}

注解鉴权默认是关闭的,要使用的话需要将satoken全局拦截器注入到项目中!

@Configuration
public class SecurityConfiguration implements WebMvcConfigurer {

    /**
     * 注册sa-token的拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册路由拦截器,自定义验证规则
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }
}

如上就注册了satoken的全局拦截器,就可以愉快的使用注解权限了。官方文档:Sa-Token

注意:这里配置的是“/**”,也就是全路径,所有后面要在不需要鉴权的接口上加上@SaIgnore用于忽略鉴权。

如下配置了/auth开头的接口不用鉴权

@RestController
@RequiredArgsConstructor
@Validated
@RequestMapping("/auth")
@SaIgnore
public class AuthController {

    private final AuthService authService;

    /**
     * 登录
     *
     * @param reqVO 登录请求
     * @return token
     */
    @PostMapping("/login")
    public CommonResult<LoginRespVO> login(@RequestBody @Valid LoginReqVO reqVO) {
        return success(authService.login(reqVO));
    }

    ...
}

如下是一个简单的需要鉴权的示例

@RestController
@RequestMapping("/system/role")
@RequiredArgsConstructor
public class RoleController {

    private final RoleService roleService;

    /**
     * 创建角色
     *
     * @param reqVO 角色信息
     * @return id
     */
    @PostMapping("/create")
    @SaCheckPermission("system:role:create")
    public CommonResult<Long> createRole(@Valid @RequestBody RoleCreateReqVO reqVO) {
        return success(roleService.createRole(reqVO));
    }

    /**
     * 更新角色
     *
     * @param reqVO 角色信息
     * @return 结果
     */
    @PutMapping("/update")
    @SaCheckPermission("system:role:update")
    public CommonResult<Boolean> updateRole(@Valid @RequestBody RoleUpdateReqVO reqVO) {
        roleService.updateRole(reqVO);
        return success(true);
    }

    ...
}

5、全局异常

通过定义全局异常拦截器可以返回前端统一的格式,以下仅供参考。

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 权限码异常
     */
    @ExceptionHandler(NotPermissionException.class)
    public CommonResult<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
        String requestUri = request.getRequestURI();
        log.error("请求地址'{}',权限码校验失败'{}'", requestUri, e.getMessage());
        return CommonResult.error(FORBIDDEN);
    }

    /**
     * 角色权限异常
     */
    @ExceptionHandler(NotRoleException.class)
    public CommonResult<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
        String requestUri = request.getRequestURI();
        log.error("请求地址'{}',角色权限校验失败'{}'", requestUri, e.getMessage());
        return CommonResult.error(FORBIDDEN);
    }

    /**
     * 认证失败
     */
    @ExceptionHandler(NotLoginException.class)
    public CommonResult<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
        String requestUri = request.getRequestURI();
        log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestUri, e.getMessage());
        return CommonResult.error(UNAUTHORIZED);
    }

    /**
     * 无效认证
     */
    @ExceptionHandler(SameTokenInvalidException.class)
    public CommonResult<Void> handleSameTokenInvalidException(SameTokenInvalidException e, HttpServletRequest request) {
        String requestUri = request.getRequestURI();
        log.error("请求地址'{}',内网认证失败'{}',无法访问系统资源", requestUri, e.getMessage());
        return CommonResult.error(UNAUTHORIZED);
    }

    ...

}

6、跨域(可选)

官网也有提供解决方法Sa-Token,我非常建议你看完这篇文章后再决定使用什么方式Sa-Token

看个人选择吧,我这里使用的是satoken拦截器+普通corsFilter的方式。

@Configuration
public class OkayWebAutoConfiguration implements WebMvcConfigurer {

    /**
     * 跨域配置
     */
    @Bean
    public CorsFilter corsFilter()
    {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 设置访问源地址
        config.addAllowedOriginPattern("*");
        // 设置访问源请求头
        config.addAllowedHeader("*");
        // 设置访问源请求方法
        config.addAllowedMethod("*");
        // 有效期 1800秒
        config.setMaxAge(1800L);
        // 添加映射路径,拦截一切请求
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        // 返回新的CorsFilter
        return new CorsFilter(source);
    }

}

7、启动类

启动类确保以上需要的组件被扫描注册就好。

@SpringBootApplication
public class AdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(AdminApplication.class, args);
    }
}

8、启动测试

这里我使用了redis,可以先忽略。

因为我的前端是一整个项目,不好拆出来贴代码,所以直接展示登录结果了!

尝试登录

发起一个创建请求,因为前端项目已经配置了发请求的header,所以请求就会带上前面的token了。

然后satoken拦截器发挥作用进行鉴权,关于拦截器如何鉴权的,我之后还会再讲的。这次先到这吧。


个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview

posted @ 2024-01-18 08:45  wnhyang  阅读(62)  评论(0编辑  收藏  举报