Sa-Token介绍:Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题
本文章框架使用:
SpringCloudAlibaba、SpringBoot2.1.13、sa-token1.30.0、redis
服务架构
开始
cn.dev33
sa-token-reactor-spring-boot-starter
1.30.0
cn.dev33
sa-token-dao-redis-jackson
1.30.0
org.springframework.cloud
spring-cloud-starter-gateway
# 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
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();
});
}
}
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
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 getPermissionList(Object loginId, String loginType) {
// 返回此 loginId 拥有的权限列表
List strs = new ArrayList<>();
strs.add("system");
return strs;
}
@Override
public List getRoleList(Object loginId, String loginType) {
// 返回此 loginId 拥有的角色列表
List strs = new ArrayList<>();
strs.add("admin");
return strs;
}
}
cn.dev33
sa-token-spring-boot-starter
1.30.0
cn.dev33
sa-token-dao-redis-jackson
1.30.0
# 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
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 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("手机号或密码错误");
}
}
redis中
到这里简单的登录就完成了
system业务服务中也需要引入sa-token,bootsrap.yml配置都是一样的
cn.dev33
sa-token-reactor-spring-boot-starter
1.30.0
cn.dev33
sa-token-dao-redis-jackson
1.30.0
这里报无权限的原因就是网关实现了拦截,在上面配置中网关配置了meta-system路由的权限必须使用system-no
而我们在添加权限集合时候没有该权限所以被拦截
角色拦截配置也是类似
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("/**");
}
}
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 getPermissionList(Object loginId, String loginType) {
// 返回此 loginId 拥有的权限列表
List strs = new ArrayList<>();
strs.add("system-no");
return strs;
}
@Override
public List getRoleList(Object loginId, String loginType) {
// 返回此 loginId 拥有的角色列表
List strs = new ArrayList<>();
strs.add("admin");
return strs;
}
}
@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 apiGather(){
return new ResultJsonUtil().success("111");
}
}
网关跨域的问题
解决:
在网关服务中创建类Cors
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.support.DefaultServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Configuration
public class Cors {
private static final String MAX_AGE = "18000L";
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "PUT,POST,GET,DELETE,OPTIONS");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,"Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,authorization,Authorization");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
}
git地址:https://gitee.com/yangjial/meta.git
代码在frontop的分支,记得切换