使用AccessDecisionManager实现HttpSecurity的自定义动态路由鉴权
项目中有一批提供给另外的应用调用的开放接口,起初只是简单的对接口放行,任意的应用和接口调用工具都能进行调用。由于考虑安全的原因,需要优化成,动态的自定义的对接口进行IP白名单和黑名单限制,或者接口需要指定的请求头才能访问。
一、AccessDecisionManager是什么?
顾名思义,我们可以把它理解为访问决策管理器,这个接口非常简单,里面只定义了三个方法,我们只需要关注decide方法,如果decide抛出了AccessDeniedException异常则拒绝接口访问
public interface AccessDecisionManager { void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); }
而AccessDecisionManager接口由AbstractAccessDecisionManager抽象类来实现,但是decide、supports方法并不在该抽象类里实现,而是由继承了AbstractAccessDecisionManager抽象类的AffirmativeBased、ConsensusBased、UnanimousBased来实现。这三个具体的实现类的区别就是具体拒绝策略不同,直接看源码就很直观,或者参考这篇文章三个授权决策的区别。其中AbstractAccessDecisionManager抽象类里维护着一个AccessDecisionVoter的实现类数组。部分代码如下
public abstract class AbstractAccessDecisionManager implements AccessDecisionManager, InitializingBean, MessageSourceAware { private List> decisionVoters = Arrays.asList( //自定义动态路由控制, new AnonymousAccessVoter(routingSecurityHandler), new WebExpressionVoter(),//这个是spring security自带的投票器 new AuthenticatedVoter()//这个是spring security自带的投票器 ); return new AffirmativeBased(decisionVoters); } }
- 重中之重的实现就是这里的实现。实现AnonymousAccessVoter
public class AnonymousAccessVoter implements AccessDecisionVoter { private final RoutingSecurityHandler routingSecurityHandler; public AnonymousAccessVoter(RoutingSecurityHandler routingSecurityHandler) { this.routingSecurityHandler = routingSecurityHandler; } @Override public boolean supports(ConfigAttribute attribute) { return true; } /** * 1.获取需要校验的路径(开放接口、自定义配置接口) * 2.判断请求路径是否在需要自定义校验的路径中 * 3.根据情况投票 * 3.1需要自定义校验但不符合条件 则抛异常拒绝访问 * 3.2需要自定义校验且符合条件,则投通过票 * 3.3不需要自定义校验,则投弃权票,则走正常的Authentication认证和WebExpressionVoter认证 */ @Override public int vote(Authentication authentication, Object object, Collection attributes) { if (authentication == null) { return ACCESS_DENIED; } //自定义鉴权主要的实现 int vote = routingSecurityHandler.verifyRouting(); if (vote == 1) { return ACCESS_GRANTED; } else if (vote == -1) { throw new AccessDeniedException("该请求无权限,拒绝访问"); } return ACCESS_ABSTAIN; } @Override public boolean supports(Class clazz) { return true; } }
然而具体的实现在 int vote = routingSecurityHandler.verifyRouting();这一行代码
@Component public class RoutingSecurityHandler { private final SysSecurityConfigService sysSecurityConfigService; private final RedisCache redisCache; private final ThreadPoolTaskExecutor threadPoolTaskExecutor; private final AntPathMatcher antPathMatcher; public RoutingSecurityHandler(SysSecurityConfigService sysSecurityConfigService, RedisCache redisCache, ThreadPoolTaskExecutor threadPoolTaskExecutor) { this.sysSecurityConfigService = sysSecurityConfigService; this.redisCache = redisCache; this.threadPoolTaskExecutor = threadPoolTaskExecutor; antPathMatcher = new AntPathMatcher(); } /** * 开放接口动态自定义鉴权主要实现 * 1. 从缓存获取自定义路由列表 * 2. 判断请求路径是否需要自定义鉴权 * 3. 判断需要自定义鉴权的路由是否满足权限要求 * 4. 根据权限返回投票 * @return 1、赞成票 -1、反对票 0、弃权票 */ public int verifyRouting() { ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (sra == null) { return 0; } HttpServletRequest request = sra.getRequest(); String ip = IpUtils.getIpAddr(request); String method = request.getMethod(); String uri = request.getRequestURI(); // 1. 从缓存获取自定义路由列表 SysSecurityConfig securityConfig = null; List routingList = getRoutingList(); // 2. 判断请求路径是否需要自定义鉴权 for (SysSecurityConfig sysSecurityConfig : routingList) { if (antPathMatcher.match(sysSecurityConfig.getPath(), uri)) { securityConfig = sysSecurityConfig; break; } } //需要自定义鉴权则判断是否满足权限 if (securityConfig != null) { // 如果自定义路径 开启初始化白名单 则默认所有请求都返回赞成票并且把IP加入到白名单 if (securityConfig.getIncludeWhitelist() == 1) { Integer id = securityConfig.getId(); threadPoolTaskExecutor.execute(() -> sysSecurityConfigService.updateSecurityConfig(id, ip, method)); return 1; } // 请求IP若在黑名单中,则返回反对票 List list; if (!CollectionUtils.isEmpty((list = securityConfig.getIpBlacklist()))) { for (String blacklistIp : list) { if (ip.matches(blacklistIp)) { return -1; } } } // 若请求方式不满足权限,则返回反对票 if (StringUtils.isNotEmpty(securityConfig.getRequestMethod()) && !method.equalsIgnoreCase(securityConfig.getRequestMethod())) { return -1; } // 判断请求头是否满足权限,返回相应投票 if (StringUtils.isNotEmpty(securityConfig.getHeaderKey())) { String headerValue = request.getHeader(securityConfig.getHeaderKey()); if (StringUtils.isEmpty(headerValue) || !headerValue.equals(securityConfig.getHeaderValue())) { return -1; } else { return 1; } } // 若请求IP在白名单中,则返回赞成票 if (!CollectionUtils.isEmpty((list = securityConfig.getIpWhitelist()))) { for (String whitelistIp : list) { if (ip.matches(whitelistIp)) { return 1; } } } } // 不需要鉴权,则投弃权票 return 0; } /** * 从redis获取路由列表,3分钟刷新一次缓存 * 从数据库加载路由表同时将路径、白名单、黑名单根据通配符替换成正则的pattern */ private List getRoutingList() { List sysSecurityConfigList = redisCache.getCacheList(Constants.SYS_SECURITY_CONFIG); if (CollectionUtils.isEmpty(sysSecurityConfigList)) { // 从数据库加载自定义鉴权的路由表 LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(SysSecurityConfig::getStatus, 1); lqw.isNotNull(SysSecurityConfig::getPath); sysSecurityConfigList = sysSecurityConfigService.list(lqw); List list; for (SysSecurityConfig securityConfig : sysSecurityConfigList) { // 白名单根据通配符替换成正则的pattern if (!CollectionUtils.isEmpty((list = securityConfig.getIpWhitelist()))) { for (int i = 0; i总结
- 使用AccessDecisionManager实现HttpSecurity的自定义动态路由鉴的主要思路是利用投票机制进行鉴权。即通过自定义实现的AnonymousAccessVoter来鉴权投票,从Redis中读取到的路由权限表配置,进行逻辑判断请求是否符合配置权限要求,从而投出相对应的投票,如果请求是开放的接口,且不符合权限要求直接抛出AccessDeniedException异常即可,若符合权限要求则投已认证票,如果请求不是开放的接口,则投出弃权票,由其他AccessDecisionVoter来投票。
- 如果AnonymousAccessVoter投出了弃权票,相当于该请求不在管辖范围不参与投票,则由其他投票器投票。
- 所有的AccessDecisionVoter投票器投出的票由AccessDecisionManager来管理,其中本章示例所使用的访问决策管理器为AffirmativeBased。即只要有一个投票器投了已认证票则本次请求被视为认证通过,正常访问接口。
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。