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

本文详细介绍了如何在SpringSecurity中使用AccessDecisionManager和自定义AccessDecisionVoter实现动态路由鉴权,涉及IP白名单/黑名单、请求头验证以及Redis作为配置存储和缓存机制。
摘要由CSDN通过智能技术生成

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

一、AccessDecisionManager是什么?

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

public interface AccessDecisionManager {

	void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
			throws AccessDeniedException, InsufficientAuthenticationException;

	boolean supports(ConfigAttribute attribute);

	boolean supports(Class<?> clazz);
}

AccessDecisionManager接口由AbstractAccessDecisionManager抽象类来实现,但是decide、supports方法并不在该抽象类里实现,而是由继承了AbstractAccessDecisionManager抽象类的AffirmativeBasedConsensusBasedUnanimousBased来实现。这三个具体的实现类的区别就是具体拒绝策略不同,直接看源码就很直观,或者参考这篇文章三个授权决策的区别。其中AbstractAccessDecisionManager抽象类里维护着一个AccessDecisionVoter的实现类数组。部分代码如下

public abstract class AbstractAccessDecisionManager
		implements AccessDecisionManager, InitializingBean, MessageSourceAware {

	private List<AccessDecisionVoter<?>> decisionVoters;

	protected AbstractAccessDecisionManager(List<AccessDecisionVoter<?>> decisionVoters) {
		Assert.notEmpty(decisionVoters, "A list of AccessDecisionVoters is required");
		this.decisionVoters = decisionVoters;
	}
}

而我们需要真正去实现自定义路由鉴权的就是实现AccessDecisionVoter接口。然而AccessDecisionVoter接口也非常简单

public interface AccessDecisionVoter<S> {

	int ACCESS_GRANTED = 1;

	int ACCESS_ABSTAIN = 0;

	int ACCESS_DENIED = -1;

	boolean supports(ConfigAttribute attribute);

	int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);

}

可见,接口中的三个静态变量分别表示已授权弃权拒绝。然后在自定义的AccessDecisionVoter实现类中,重写vote方法,根据自定义的权限返回对应的静态常量即可。

二、自定义动态路由鉴权代码实现

  1. 首先为了能在不重启项目就能做到动态自定义配置路由权限,需要将配置写在数据库,然后利用Redis缓存路由配置。这样每次请求就在Redis读取配置信息,然后根据配置信息校验请求的权限。
    数据库表如下图所示
    开放接口权限配置表设计
    建表语句如下:
CREATE TABLE `your_table_name` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `path` varchar(255) DEFAULT NULL COMMENT '访问路径',
  `ip_whitelist` varchar(1024) DEFAULT NULL COMMENT 'ip白名单 多个ip则使用逗号分隔',
  `ip_blacklist` varchar(1024) DEFAULT NULL COMMENT 'ip黑名单 多个ip则使用逗号分隔',
  `request_method` varchar(255) DEFAULT NULL COMMENT '请求方式',
  `header_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '指定请求头的key',
  `header_value` varchar(255) DEFAULT NULL COMMENT '指定请求头的value',
  `include_whitelist` tinyint(1) DEFAULT '0' COMMENT '收录白名单ip开关 0-关,1-开 用作收录初始白名单,不建议长时间开启 ',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态 0-关 1-开 关闭时该配置不生效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  1. 在项目中找到spring security配置或者新建配置,如下
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
    private RoutingSecurityHandler routingSecurityHandler;
	
	@Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
    	// 自定义动态配置开放接口权限
    	httpSecurity.accessDecisionManager(accessDecisionManager())
    	// 这里必定还有其他配置 只是不在本章讨论中,不展示无关本章配置
    	// ...
    }

	    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> 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<Object> {

    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<ConfigAttribute> 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<SysSecurityConfig> 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<String> 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<SysSecurityConfig> getRoutingList() {
        List<SysSecurityConfig> sysSecurityConfigList = redisCache.getCacheList(Constants.SYS_SECURITY_CONFIG);
        if (CollectionUtils.isEmpty(sysSecurityConfigList)) {
            // 从数据库加载自定义鉴权的路由表
            LambdaQueryWrapper<SysSecurityConfig> lqw = Wrappers.lambdaQuery();
            lqw.eq(SysSecurityConfig::getStatus, 1);
            lqw.isNotNull(SysSecurityConfig::getPath);
            sysSecurityConfigList = sysSecurityConfigService.list(lqw);
            List<String> list;
            for (SysSecurityConfig securityConfig : sysSecurityConfigList) {
                // 白名单根据通配符替换成正则的pattern
                if (!CollectionUtils.isEmpty((list = securityConfig.getIpWhitelist()))) {
                    for (int i = 0; i < list.size(); i++) {
                        list.set(i, list.get(i).replaceAll("\\*", "[0-9]+").replaceAll("\\.", "\\\\."));
                    }
                }
                // 将黑名单根据通配符替换成正则的pattern
                if (!CollectionUtils.isEmpty((list = securityConfig.getIpBlacklist()))) {
                    for (int i = 0; i < list.size(); i++) {
                        list.set(i, list.get(i).replaceAll("\\*", "[0-9]+").replaceAll("\\.", "\\\\."));
                    }
                }
            }
            redisCache.setCacheList(Constants.SYS_SECURITY_CONFIG, sysSecurityConfigList);
            redisCache.expire(Constants.SYS_SECURITY_CONFIG, 180, TimeUnit.SECONDS);
        }
        return sysSecurityConfigList;
    }
}

总结

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值