使用AccessDecisionManager实现HttpSecurity的自定义动态路由鉴权

06-27 1392阅读

项目中有一批提供给另外的应用调用的开放接口,起初只是简单的对接口放行,任意的应用和接口调用工具都能进行调用。由于考虑安全的原因,需要优化成,动态的自定义的对接口进行IP白名单和黑名单限制,或者接口需要指定的请求头才能访问。

一、AccessDecisionManager是什么?

顾名思义,我们可以把它理解为访问决策管理器,这个接口非常简单,里面只定义了三个方法,我们只需要关注decide方法,如果decide抛出了AccessDeniedException异常则拒绝接口访问

使用AccessDecisionManager实现HttpSecurity的自定义动态路由鉴权

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);
    }
}
  1. 重中之重的实现就是这里的实现。实现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  

总结

  1. 使用AccessDecisionManager实现HttpSecurity的自定义动态路由鉴的主要思路是利用投票机制进行鉴权。即通过自定义实现的AnonymousAccessVoter来鉴权投票,从Redis中读取到的路由权限表配置,进行逻辑判断请求是否符合配置权限要求,从而投出相对应的投票,如果请求是开放的接口,且不符合权限要求直接抛出AccessDeniedException异常即可,若符合权限要求则投已认证票,如果请求不是开放的接口,则投出弃权票,由其他AccessDecisionVoter来投票。
  2. 如果AnonymousAccessVoter投出了弃权票,相当于该请求不在管辖范围不参与投票,则由其他投票器投票。
  3. 所有的AccessDecisionVoter投票器投出的票由AccessDecisionManager来管理,其中本章示例所使用的访问决策管理器为AffirmativeBased。即只要有一个投票器投了已认证票则本次请求被视为认证通过,正常访问接口。

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]