官方文档:http://sa-token.dev33.cn/
目前公司基本都会使用分布式来整活,虽然我对分布式了解甚少,但是有任务也得硬着头皮上。
公司接到一个需求,就是将按钮来进行精确控制,从而达到项目收费的功能。
先说一下gateway在整合过程的作用,我感觉就是将token来进行传递,其他的就暂时没发现什么,是我感觉,不代表其他人哈。
引用gateway的一张图,我也说不明白,目前会使用就行
直接开始上代码,satoekn的鉴权还是交到了每个子服务,gateway只传递,不鉴权
pom文件
<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.28.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.28.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
satoken全局拦截器
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**")
// 开放地址
.addExclude("/favicon.ico")
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/login", r -> StpUtil.checkLogin());
// // 权限认证 -- 不同模块, 校验不同权限
// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// ...
})
// 异常处理方法:每次setAuth函数出现异常时进入
.setError(e -> {
return SaResult.error("gateway的全局过滤器异常:"+e.getMessage());
})
;
}
}
由于swagger不直接使用gateway进行访问,所以还是在每一个子服务里面对swagger进行放行
gateway拦截器
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 全局过滤器,为请求添加 Id-Token
*/
@Component
public class ForwardAuthFilter implements GlobalFilter {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest newRequest = exchange
.getRequest()
.mutate()
.header(SaIdUtil.ID_TOKEN, SaIdUtil.getToken())
//.header(StpUtil.getTokenName(), StpUtil.getTokenValue())
.build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
}
// @Override
// public void addInterceptors(InterceptorRegistry registry) {
// // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
// // log.info("sa-token拦截器");
// registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
// }
}
因为涉及到feign调用,所以此处使用A服务和B服务进行说明
pom文件
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.28.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.28.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
A服务本身也有登陆功能,可以看上篇springboot整合satoken
Sa-Token 权限认证 配置类
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.id.SaIdUtil;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import cn.dev33.satoken.util.SaResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Sa-Token 权限认证 配置类
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
private final Logger log = LoggerFactory.getLogger(this.getClass());
// 注册 Sa-Token 全局过滤器
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" ,"/doc.html/**","/error")
.setAuth(obj -> {
// 校验 Id-Token 身份凭证 —— 以下两句代码可简化为:SaIdUtil.checkCurrentRequestToken();
String token = SaHolder.getRequest().getHeader(SaIdUtil.ID_TOKEN);
log.info("manage子服务当前的ID_TOKEN:{}",token);
SaIdUtil.checkToken(token);
//SaIdUtil.checkToken(StpUtil.getTokenValue());
})
.setError(e -> {
return SaResult.error("manage子服务当前的异常"+e.getMessage());
})
;
}
// 注册Sa-Token的注解拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info("sa-token拦截器");
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**")
// 排除外部调用接口
.excludePathPatterns("/**/outer/**")
// 排除指定url 排除企业logo 获取的方法
.excludePathPatterns("/**/login/**", "/**/logout/**", "/**/error/**",
"/**/register/**", "/**/verify/**", "/**/monitorLogin/**", "/**/enterprise/get", "/**/enterprise/getLogo","/**/getSAStoken/**")
.excludePathPatterns("/doc.html")
// 排除swagger相关
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");;
}
}
自定义权限验证接口扩展
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 自定义权限验证接口扩展
*/
@Component // 保证此类被SpringBoot扫描,完成sa-token的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final String KEY_PERMS = "key_perms";
private final String KEY_ROLENAMES = "key_rolenames";
@Autowired
private SysRoleDao sysRoleDao;
@Autowired
private SysUrlDao sysUrlDao;
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginKey) {
//以下代码为演示代码
SaSession session = StpUtil.getSession();
if (session.get(KEY_PERMS) != null) {
List<String> perms = (List<String>) session.get(KEY_PERMS);
log.info("session信息:{}", session.get(KEY_PERMS));
return perms;
}
// 通过用户id来获取权限
List<SysRole> roles = sysRoleDao.getRoleListByUser(loginId.toString());
List<String> roleIds = roles.stream().map(SysRole::getId).collect(Collectors.toList());
List<SysPermission> permissions = sysUrlDao.getUnionPermission(roleIds);
List<String> perms = permissions.stream().map(SysPermission::getPerms).collect(Collectors.toList());
session.set(KEY_PERMS, perms);
log.debug("loginId=" + loginId + ",permissons=" + perms.size());
log.info("权限码集合:{}", perms);
return perms;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginKey) {
return null;
}
}
异常类
import cn.com.yeexun.yeexunbase.model.Response;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestControllerAdvice
public class ExternalException {
/**
* SpringBoot获取当前环境代码,Spring获取当前环境代码
*/
@Value("${spring.profiles.active}")
private String profiles;
/**
* 判断是否是Ajax请求
*
* @param request
* @return
*/
public boolean isAjax(HttpServletRequest request) {
return (request.getHeader("X-Requested-With") != null
&& "XMLHttpRequest".equals(request.getHeader("X-Requested-With").toString()));
}
// 在当前类每个方法进入之前触发的操作
@ModelAttribute
public void get(HttpServletRequest request) throws IOException {
}
// 全局异常拦截(拦截项目中的所有异常)
@ExceptionHandler
public Response handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if ("dev".equals(profiles)) {
// 打印堆栈,以供调试
e.printStackTrace();
}
// 不同异常返回不同状态码
Response aj = null;
// if (e instanceof NotLoginException) { // 如果是未登录异常
// NotLoginException ee = (NotLoginException) e;
// aj = Response.error(ee.getMessage());
// if (ee.getLoginType().equals("member") && !isAjax(request)) {
// response.sendRedirect("/member/login");
// } else if (!isAjax(request)) {
// response.sendRedirect("/system/adminlogin");
// } else {
// aj = Response.error("请登录");
// }
// } else if (e instanceof NotRoleException) { // 如果是角色异常
// NotRoleException ee = (NotRoleException) e;
// aj = Response.error("无此角色:" + ee.getRole());
// } else
if(e instanceof NotLoginException){
NotLoginException notLoginException= (NotLoginException) e;
aj= Response.error("认证异常:"+notLoginException.getMessage());
} else if (e instanceof NotPermissionException) { // 如果是权限异常
NotPermissionException notPermissionException = (NotPermissionException) e;
aj = Response.error("无此权限:" + notPermissionException.getCode()+",请联系管理员");
} else { // 普通异常, 输出:5000 + 异常信息
aj = Response.error(e.getMessage());
}
// 返回给前端
return aj;
}
}
feign拦截器
import cn.dev33.satoken.id.SaIdUtil;
import cn.dev33.satoken.stp.StpUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* feign拦截器, 在feign请求发出之前,加入一些操作
*/
@Component
public class FeignInterceptor implements RequestInterceptor {
private final Logger log = LoggerFactory.getLogger(this.getClass());
// 为 Feign 的 RCP调用 添加请求头Id-Token
@Override
public void apply(RequestTemplate requestTemplate) {
String url = requestTemplate.request().url();
//无法从别处获取tokenValue,只能由feign处添加参数
String tokenValue = url.substring(url.indexOf("token=")+6, url.length());
requestTemplate.header(StpUtil.getTokenName(),tokenValue).header(SaIdUtil.ID_TOKEN, SaIdUtil.getToken());
}
}
工具类
import cn.dev33.satoken.stp.StpUtil;
public class GetTokenUtil {
public static String getSaStoekn(){
return StpUtil.getTokenInfo().getTokenValue();
}
}
feign调用说明
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(List<String> ids) {
//此处添加token
String token= GetTokenUtil.getSaStoekn();
for (String id : ids) {
Response<Boolean> hasServiceApproval = feignApiService.extInnerApiHasServiceApproval(id,token);
if(hasServiceApproval==null){
throw new BizException("远程调用没有权限");
}
logger.info("远程后:{}",hasServiceApproval.getData());
if (hasServiceApproval.getData() != null && hasServiceApproval.getData()) {
throw new BizException(BizExceptionEnum.EXTERNAL_API_HAS_APPROVAL_NO_DEL);
}
this.extExternalApiDao.deleteById(id, new Date());
}
this.externalApiAsyncService.handlerCache(CacheEnum.externalApi.toString());
}
@FeignClient(name = "${yx.api}", fallback = ApiServiceHystrix.class,configuration = FeignInterceptor.class//这个是关键)
public interface FeignApiService {
@PostMapping(value = "/extInnerApi/hasServiceApproval")
Response<Boolean> extInnerApiHasServiceApproval(@RequestParam("externalApiId") String externalApiId,@RequestParam("token")String token);
}
A服务暂时说明完毕,其实目前的A服务调用B服务,不需要修改什么,我们是A调用B,B还调用A,所以B的代码基本和A一致。
gateway目前整合没发现什么大问题,yml文件那个,子服务和springboot的一致,gateway就需要配置redis信息即可,底层使用的redis,所以每一个服务的database必须为同一个,不然找不到satoken。