官方文档:http://sa-token.dev33.cn/
目前公司基本都会使用分布式来整活,虽然我对分布式了解甚少,但是有任务也得硬着头皮上。
公司接到一个需求,就是将按钮来进行精确控制,从而达到项目收费的功能。
先说一下gateway在整合过程的作用,我感觉就是将token来进行传递,其他的就暂时没发现什么,是我感觉,不代表其他人哈。
引用gateway的一张图,我也说不明白,目前会使用就行
在这里插入图片描述
直接开始上代码,satoekn的鉴权还是交到了每个子服务,gateway只传递,不鉴权

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服务进行说明

A服务

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。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐