前言

在橙单中,目前支持以下两种类型的白名单接口。

  • 免登录接口,所有游客均可访问。
  • 所有已登录用户均可访问的后台接口。

免登录接口

顾名思义,这里是指无需登录即可访问的后台接口方法,比如 APP 首页的数据加载接口。对于此类接口有以下几点注意事项。

  • 如果用户没有登录直接访问,将不会产生 TokenData 数据。
  • 而当已登录用户访问此类接口时,会与其他接口一样产生 TokenData 对象。
  • 以上两点可以理解为,游客访问头条首页时,看到的是游客文章,而已登录用户访问同一接口时,获取的是个性化推荐文件。

单体配置

通过 Sa-Token 权限框架提供的 @SaIgnore 注解,对免登录的白名单接口进行标记,该注解可应用于 Controller 接口对象,以及 Controller 类内的接口方法。

单体代码解析

这里仅给出单体工程中,与免登录白名单接口相关的处理逻辑。代码位于鉴权拦截器类 AuthenticationInterceptor,具体实现则由 SaTokenUtil 工具类的 handleAuthIntercept 方法提供,以下代码片段将同时给出他们的具体实现。

@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        // ... ... 忽略部分不相干的代码逻辑
           
        ResponseResult<Void> result = saTokenUtil.handleAuthIntercept(request, handler);
        if (!result.isSuccess()) {
            ResponseResult.output(result.getHttpStatus(), result);
            return false;
        }
        return true;
    }
 
    // ... ... 忽略该类的其余方法实现。
}

// 该方法位于SaTokenUtil中,由拦截器拦截方法调用。
public ResponseResult<Void> handleAuthIntercept(HttpServletRequest request, Object handler) {
    Method method = ((HandlerMethod) handler).getMethod();
    String errorMessage;
    //如果没有登录则直接交给satoken注解去验证。
    if (!StpUtil.isLogin()) {
        // 如果此 Method 或其所属 Class 标注了 @SaIgnore,则忽略掉鉴权
        if (BooleanUtil.isTrue(SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class))) {
            return ResponseResult.success();
        }
        errorMessage = "非免登录接口必须包含Token信息!";
        return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage);
    }

    // ... ... 忽略部分不相干的代码逻辑
}

微服务配置

微服务工程支持两种类型的免登录白名单接口,第一种是和前面单体工程中基于 @SaIgnore 注解的方式完全一致,因此这里不做过多赘述。另一种则是本小节介绍的重点。下图所示的配置仅属于「微服务网关」,目前可支持两种配置模式。下图为网关服务在 Nacos 中的相应配置。

  • 完全匹配模式。该模式性能更好,如果 URL 是确定的,应优先使用该配置。
  • Ant Path 匹配模式。支持通配符,因此更加灵活,但是效率则低于「完全匹配模式」。

微服务代码解析

这里仅给出微服务网关服务中,与免登录白名单接口相关的处理逻辑。代码位于网关服务的鉴权前置过滤器对象 AuthenticationPreFilter。

@Slf4j
public class AuthenticationPreFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // ... ... 忽略部分不相干的代码逻辑

        // 调用私有方法判断是否为在nacos中配置的免登陆白名单接口,如果是就会在请求头中添加
        // ApplicationConstant.HTTP_HEADER_DONT_AUTH值后转发给业务微服务,业务服务的
        // 鉴权拦截器 AuthenticationInterceptor,在发现存在该请求头后,将不做任何权限验证。
        if (this.dontAuth(url)) {
            mutableReq = exchange.getRequest().mutate()
                    .header(ApplicationConstant.HTTP_HEADER_DONT_AUTH, "true").build();
        }
        return chain.filter(exchange.mutate().request(mutableReq).build());
    }
    
    private boolean dontAuth(String url) {
        // 这里的配置值均来自上一小节中截图的nacos配置。
        // 先过滤直接匹配的白名单url。
        if (CollUtil.isNotEmpty(appConfig.getWhitelistUrl()) && appConfig.getWhitelistUrl().contains(url)) {
            return true;
        }
        // 过滤ant pattern模式的白名单url。
        if (CollUtil.isNotEmpty(appConfig.getWhitelistUrlPattern())) {
            for (String urlPattern : appConfig.getWhitelistUrlPattern()) {
                if (antMatcher.match(urlPattern, url)) {
                    return true;
                }
            }
        }
        return false;
    } 
    
    // ... ... 忽略该类的其余方法实现。
}

登录后白名单接口

本节讨论的白名单后台接口,是所有「已登录用户」均可访问的后台接口,无需进行任何权限配置。

适用场景

  • 在业务系统中,前端页面的下拉列表组件,经常会以字典的方式查询后台业务数据,由于此类字典接口可能会被很多业务页面访问,如果逐个赋权就会非常麻烦,且不易于维护,因此在橙单生成器中,我们会为配置了字典的 Controller 对象生成 /admin/xxx/listDict 接口,该接口会返回统一的字典格式数据。
  • 在橙单中,还存在一些功能性的白名单接口,这里以工作流的待办任务提交审批接口为例,尽管所有已登录用户均可调用该接口,然而只有审批任务的候选人才能完成正常的审批,因此这类接口的用户鉴权逻辑是在接口内部实现的。

白名单表

  • 在橙单中,白名单接口位于 zz_sys_perm_whitelist 数据表中,该表没有提供前端操作页面,因此白名单的数据维护,目前只能通过手动修改数据表数据的方式完成。
  • 白名单数据修改后,后台服务无需重启,但是用户需要重新登录后方可生效。
  • 在用户登录 doLogin/doMobileLogin 接口中,会将当前用户已被授权的动态渲染表单的业务请求接口,如在线表单、在线表单工作流和在线统计表单等,连同该表包含的全部白名单接口,一同存入与当前用户 SessionId 关联的 Redis 缓存键中,以供 AuthenticationInterceptor 拦截器进行接口鉴权时使用。
private JSONObject buildLoginDataAndLogin(SysUser user) {

    // ... ... 忽略部分不相干的代码逻辑

    // 这里定义的permSet就会包含zz_sys_perm_whitelist表中的所有请求url。
    // 通过后面的代码可以看到,还会包含动态渲染表单业务请求接口的动态url
    // 集合,如用户已授权的在线表单、在线表单工作流和在线统计表单。
    Set<String> permSet = new HashSet<>();
    if (!isAdmin) {
        // 所有登录用户都有白名单接口的访问权限。
        CollUtil.addAll(permSet, sysPermWhitelistService.getWhitelistPermList());
    }
    List<String> menuCodeList = new LinkedList<>();
    // 计算当前用于已授权菜单中,属于在线表单的菜单,并推演其会使用到的动态业务请求接口。
    OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList);
    CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet);
    // 计算当前用于已授权菜单中,属于在线表单工单的菜单,并推演其会使用到的动态业务请求接口。
    OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList);
    CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet);
    if (!isAdmin) {
        permSet.addAll(onlinePermData.permUrlSet);
        permSet.addAll(onlineFlowPermData.permUrlSet);
        // 计算当前用于已授权菜单中,属于在线统计表单的菜单,并推演其会使用到的动态业务请求接口。
        Set<String> reportPermSet = this.getReportMenuPermData(allMenuList);
        permSet.addAll(reportPermSet);
        String sessionId = tokenData.getSessionId();
        // 缓存用户的权限资源,这里缓存的是基于URL验证的权限资源,比如在线表单、工作流和统计表单中的白名单资源。
        this.putUserSysPermCache(sessionId, permSet);
        // 缓存权限字字段,StpInterfaceImpl中会从缓存中读取,并交给satoken进行接口权限的验证。
        this.putUserSysPermCodeCache(sessionId, permCodeList);
        sysDataPermService.putDataPermCache(sessionId, user.getUserId(), user.getDeptId());
    }
    this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList);
    return jsonData;
}

private void putUserSysPermCache(String sessionId, Collection<String> permUrlSet) {
    if (CollUtil.isEmpty(permUrlSet)) {
        return;
    }
    String sessionPermKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
    RSet<String> redisPermSet = redissonClient.getSet(sessionPermKey);
    redisPermSet.addAll(permUrlSet);
    redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
}
  • 此类接口的鉴权也是在 AuthenticationInterceptor 拦截器中完成的。
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        // ... ... 忽略部分不相干的代码逻辑
        //  saTokenUtil.handleAuthIntercept 方法中给出了具体实现。
        ResponseResult<Void> result = saTokenUtil.handleAuthIntercept(request, handler);
        if (!result.isSuccess()) {
            ResponseResult.output(result.getHttpStatus(), result);
            return false;
        }
        return true;
    }
}

@Component
public class SaTokenUtil {

    public ResponseResult<Void> handleAuthIntercept(HttpServletRequest request, Object handler) {
        // ... ... 忽略部分不相干的代码逻辑
        
        //如果是管理员可以直接跳过验证了。
        //基于橙单内部的权限规则优先验证,主要用于内部的白名单接口,以及在线表单和工作流那些动态接口的权限验证。
        if (Boolean.TRUE.equals(tokenData.getIsAdmin())
                || this.hasPermission(tokenData.getSessionId(), request.getRequestURI())) {
            return ResponseResult.success();
        }
        // ... ... 忽略部分不相干的代码逻辑
    }
    
    // 该方法会从缓存中读取,缓存中的数据是在登录接口调用时存入的,存在的数据中就包括zz_sys_perm_whitelist白名单表中的URL。
    private boolean hasPermission(String sessionId, String url) {
        Set<String> localPermSet;
        String permKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
        Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name());
        Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL.");
        Cache.ValueWrapper wrapper = cache.get(permKey);
        if (wrapper == null) {
            RSet<String> permSet = redissonClient.getSet(permKey);
            localPermSet = permSet.readAll();
            cache.put(permKey, localPermSet);
        } else {
            localPermSet = (Set<String>) wrapper.get();
        }
        return CollUtil.contains(localPermSet, url);
    }
}

SaCheckLogin注解

这是 Sa-Token 权限框架提供的对用户是否登录进行鉴权验证的注解,该注解可用于标记已登录用户可以访问的白名单接口。需要说明的是,如果 Controller 类中的接口方法没有提供任何注解,Sa-Token 权限框架将默认使用该注解进行鉴权,而有关 Sa-Token 的相关技术可参考官方文档。下面是橙单默认生成的字典接口代码。

结语

赠人玫瑰,手有余香,感谢您的支持和关注,选择橙单,效率乘三,收入翻番。