Sa-Token介绍:Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题
本文章框架使用:
SpringCloudAlibaba、SpringBoot2.1.13、sa-token1.30.0、redis
服务架构
开始
一、首先配置网关服务
1、pom.xml
<!-- Sa-Token 权限认证(Reactor响应式集成) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.30.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.30.0</version>
</dependency>
<!--GateWay 网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2、bootstrap.yml引入sa-token配置
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken
# token有效期,单位秒,-1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期),单位秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false
# 是否从cookie中读取token
is-read-cookie: false
# 是否从head中读取token
is-read-head: true
3、新建类SaTokenConfigure,实现网关拦截
package com.frontop.meta.config;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.frontop.meta.util.ResultJsonUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.server.ServerWebExchange;
/**
* @author YangBoss
* @title: SaTokenConfigure
* @projectName meta
* @description: TODO
* @date 2022/8/18 10:12
*/
@Configuration
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**")
// 开放地址
.addExclude("/favicon.ico")
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/meta-auth/phoneLogin", r -> StpUtil.checkLogin());
// 角色认证 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证
SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));
// 权限认证 -- 不同模块, 校验不同权限
SaRouter.match("/meta-system/**", r -> StpUtil.checkPermission("system-no"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
})
// 异常处理方法:每次setAuth函数出现异常时进入
.setError(e -> {
// 设置错误返回格式为JSON
ServerWebExchange exchange = SaReactorSyncHolder.getContext();
exchange.getResponse().getHeaders().set("Content-Type", "application/json; charset=utf-8");
// return new ResultJsonUtil().fail(e.getMessage());
return SaResult.error(e.getMessage());
})
.setBeforeAuth(obj -> {
// ---------- 设置跨域响应头 ----------
SaHolder.getResponse()
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
.back();
});
}
}
4、新建全局异常处理类GlobalException
package com.frontop.meta.config;
import cn.dev33.satoken.exception.DisableLoginException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import com.frontop.meta.constant.ResponseCodeConstant;
import com.frontop.meta.constant.ResponseMessageConstant;
import com.frontop.meta.util.ResultJsonUtil;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author YangBoss
* @title: GlobalException
* @projectName meta
* @description: 拦截全局异常类
* @date 2022/8/19 15:39
*/
public class GlobalException {
// 全局异常拦截(拦截项目中的所有异常)
@ResponseBody
@ExceptionHandler
public ResultJsonUtil<Object> handlerException(Exception e) {
// 打印堆栈,以供调试
// System.out.println("全局异常---------------");
e.printStackTrace();
// 不同异常返回不同状态码
ResultJsonUtil<Object> re = null;
if (e instanceof NotLoginException) { // 如果是未登录异常
NotLoginException ee = (NotLoginException) e;
re = new ResultJsonUtil().customized(
ResponseCodeConstant.OAUTH_TOKEN_FAILURE,
ResponseMessageConstant.OAUTH_TOKEN_MISSING,
null
);
}
else if(e instanceof NotRoleException) { // 如果是角色异常
NotRoleException ee = (NotRoleException) e;
re = new ResultJsonUtil().customized(
ResponseCodeConstant.OAUTH_TOKEN_DENIED,
"无此角色:" + ee.getRole(),
null
);
}
else if(e instanceof NotPermissionException) { // 如果是权限异常
NotPermissionException ee = (NotPermissionException) e;
re = new ResultJsonUtil().customized(
ResponseCodeConstant.OAUTH_TOKEN_DENIED,
"无此角色:" + ee.getCode(),
null
);
}
else if(e instanceof DisableLoginException) { // 如果是被封禁异常
DisableLoginException ee = (DisableLoginException) e;
re = new ResultJsonUtil().customized(
ResponseCodeConstant.USER_LOCK,
"账号被封禁:" + ee.getDisableTime() + "秒后解封",
null
);
}
else { // 普通异常, 输出:500 + 异常信息
re = new ResultJsonUtil().fail(e.getMessage());
}
// 返回给前端
return re;
}
}
5、新建类StpInterfaceImpl,实现获取当前账号权限角色集合
package com.frontop.meta.config;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author YangBoss
* @title: StpInterfaceImpl
* @projectName meta
* @description: TODO
* @date 2022/8/18 10:26
*/
@Component
public class StpInterfaceImpl implements StpInterface {
/**
*当前全部是模拟数据,真实情况使用根据loginId动态查询对应角色和权限
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 返回此 loginId 拥有的权限列表
List<String> strs = new ArrayList<>();
strs.add("system");
return strs;
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 返回此 loginId 拥有的角色列表
List<String> strs = new ArrayList<>();
strs.add("admin");
return strs;
}
}
二、配置授权服务
1、pom.xml
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.30.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.30.0</version>
</dependency>
2、bootstarp.yml引入sa-token配置
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken
# token有效期,单位秒,-1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期),单位秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false
# 是否从cookie中读取token
is-read-cookie: false
# 是否从head中读取token
is-read-head: true
3、编写一个简单的登录
package com.frontop.meta.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.frontop.meta.util.ResultJsonUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author YangBoss
* @title: UserLoginController
* @projectName meta
* @description: TODO
* @date 2022/8/19 14:44
*/
@RestController
@Api(tags = "用户授权登录")
public class UserLoginController {
@ApiOperation(value = "手机+密码登录")
@PostMapping("/phoneLogin")
public ResultJsonUtil<Object> getAwardCount(String phone,String password) {
if(phone.equals("18874288923") && password.equals("123")){
StpUtil.login(1001,"PC");
return new ResultJsonUtil().success(StpUtil.getTokenInfo());
}
return new ResultJsonUtil().fail("手机号或密码错误");
}
}
三、测试效果
1、启动网关服务和授权服务后调用登录接口
redis中
到这里简单的登录就完成了
2、在system业务服务中简单配置一个测试接口
system业务服务中也需要引入sa-token,bootsrap.yml配置都是一样的
<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.30.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.30.0</version>
</dependency>
3、不携带token值访问接口
4、携带token访问
这里报无权限的原因就是网关实现了拦截,在上面配置中网关配置了meta-system路由的权限必须使用system-no
而我们在添加权限集合时候没有该权限所以被拦截
角色拦截配置也是类似
四、使用注解拦截
1、如果想使用注解拦截,只能写在业务服务的接口层
2、首先要在业务服务中开启注解拦截配置
package com.frontop.meta;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author YangBoss
* @title: SaTokenConfigure
* @projectName meta
* @description: TODO
* @date 2022/9/7 10:53
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册Sa-Token的注解拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
}
}
3、也需要配置获取角色和权限的集合类
package com.frontop.meta;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author YangBoss
* @title: StpInterfaceImpl
* @projectName meta
* @description: TODO
* @date 2022/8/18 10:26
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 返回此 loginId 拥有的权限列表
List<String> strs = new ArrayList<>();
strs.add("system-no");
return strs;
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 返回此 loginId 拥有的角色列表
List<String> strs = new ArrayList<>();
strs.add("admin");
return strs;
}
}
4、接口配置注解拦截
@RestController
@RequestMapping("/test")
@Api(tags = "测试")
public class TestContorller {
@ApiOperation(value = "请求汇总",consumes = "application/json;charset=UTF-8")
@RequestMapping(value = "/apiGather", method = RequestMethod.POST)
@SaCheckRole("super-admin2")//必须拥有该角色可访问
@SaCheckPermission("system-no")//必须拥有该权限可访问
public ResultJsonUtil<Object> apiGather(){
return new ResultJsonUtil().success("111");
}
}
5、测试效果
网关跨域的问题
解决:
在网关服务中创建类Cors
package com.geo.gateway.config;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* @author YangBoss
* @title: CorsConfig
* @projectName frontop-geo-cloud
* @description: TODO
* @date 2023/1/30 17:57
*/
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (new PathPatternParser());
CorsConfiguration corsConfig = new CorsConfiguration();
// 允许所有请求方法
corsConfig.addAllowedMethod ("*");
// 允许所有域,当请求头
corsConfig.addAllowedOrigin ("*");
// 允许全部请求头
corsConfig.addAllowedHeader ("*");
// 允许携带 Cookie 等用户凭证
corsConfig.setAllowCredentials (true);
// 允许全部请求路径
source.registerCorsConfiguration ("/**", corsConfig);
return new CorsWebFilter (source);
}
}
git地址:https://gitee.com/yangjial/meta.git
代码在frontop的分支,记得切换