前言
在橙单中,目前支持以下两种类型的白名单接口。
- 免登录接口,所有游客均可访问。
- 所有已登录用户均可访问的后台接口。
免登录接口
顾名思义,这里是指无需登录即可访问的后台接口方法,比如 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 的相关技术可参考官方文档。下面是橙单默认生成的字典接口代码。
结语
赠人玫瑰,手有余香,感谢您的支持和关注,选择橙单,效率乘三,收入翻番。