spring boot security自定义权限检查

前言

鉴权主要分为身份认证和权限控制两部分:
身份认证:检查当前用户是否合法(比如已登录)
权限控制:检查当前用户是否有访问该资源的权限

本文主要给出一个示例,说明如何自定义权限控制。
因为一个完整的示例代码,比较多,我在前面的几篇文章都有相关示例:
spring boot security快速使用示例
spring boot security之前后端分离配置
spring boot security自定义认证
spring boot security验证码登录示例
spring boot security使用jwt认证
所以本文主要给出权限检查相关的示例代码。

源码分析

在前面几篇文章里,都给了完整示例,照搬就能用,这里只粘贴出部分代码,可能让人迷糊,所以给出一些源码说明。
在启用spring security后,我们启动应用应该能看到下面这行日志:

2023-07-14 13:36:41.306 INFO 9584 — [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@6af5bbd0, org.springframework.security.web.context.SecurityContextPersistenceFilter@3204e238, org.springframework.security.web.header.HeaderWriterFilter@20b9d5d5, org.springframework.web.filter.CorsFilter@76464795, org.springframework.security.web.authentication.logout.LogoutFilter@4821aa9f, com.xxd.security.demo.security.JwtRequestFilter@129bd55d, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@35a0e495, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@a5272be, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@53cf9c99, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@b34832b, org.springframework.security.web.session.SessionManagementFilter@291373d3, org.springframework.security.web.access.ExceptionTranslationFilter@4228bf58, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@10f7918f]

注意最后一个过滤器:org.springframework.security.web.access.intercept.FilterSecurityInterceptor

	public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
		...
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
		...
	}

如上:在doFilter方法调用的invoke方法内调用了super.beforeInvocation()方法。
super.beforeInvocation()方法内调用认证检查和权限检查的方法,可以自己找到这个方法里去看,我们重点看权限检查:

	private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
			Authentication authenticated) {
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException ex) {
			...
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
			throw ex;
		}
	}

主要就是:this.accessDecisionManager.decide(authenticated, object, attributes);这个方法。
accessDecisionManager,顾名思义是访问决策管理,构造方法需要传递一些投票器,在决策的时候根据投票器的投票结果来返回最终结果,默认主要有3个实现:
在这里插入图片描述
投票器的结果有3种:同意、拒绝、弃权。

  • AffirmativeBased 有一个投票器投同意票,最终结果就是同意
  • ConsensusBased 投票结果,如果同意票数大于拒绝的,就是同意,小于是拒绝,如果票数相等且不为0,有一个属性来决定:allowIfEqualGrantedDeniedDecisions, 默认是true,同意。
  • Unanimous 有一票拒绝,最终就拒绝

不是说授权么,怎么突然引入投票的概念,是不是很懵???这里的实现就是这样。
所以我们自定义授权,就是来定义投票器的实现。

示例

定义投票器

@Slf4j
@Component
public class AuthorityDecisionVoter implements AccessDecisionVoter<FilterInvocation> {

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    private final Set<String> ANON_URL_SET = new HashSet<>();

    public AuthorityDecisionVoter() {
        ANON_URL_SET.add("/login/**");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {
        // 拿到当前请求uri
        String requestUrl = object.getRequestUrl();
        String method = object.getRequest().getMethod();


        for (String anonUrl : ANON_URL_SET) {
            if (pathMatcher.match(anonUrl, requestUrl)) {
                return ACCESS_ABSTAIN;
            }
        }

        List<String> permList = Arrays.asList(requestUrl, method + ":" + requestUrl);

        // 拿到当前用户所具有的权限.不处理了,这里是做个示例,因为每个人的场景或处理不一样,有的可能这里返回的是角色信息
        Collection<? extends GrantedAuthority> permissions = ((UserDetails) authentication.getPrincipal()).getAuthorities();

        // 示例代码,写死了,/hello/world 请求通过,其它都拒绝
        for (String perm : permList) {
            if (perm.equalsIgnoreCase("get:/hello/world")) {
                return ACCESS_GRANTED;
            }
        }

        return ACCESS_DENIED;
    }
}

定义访问决策管理

前面说过了,accessDecisionManager是决策投票器的投票结果的。

@Configuration
public class WebConfiguration {
    @Bean
    public AccessDecisionManager accessDecisionManager(ApplicationContext applicationContext,
                                                       AccessDecisionVoter authorityDecisionVoter) {
        DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
        // spel 表达式中解析bean使用的,如果用到了必须设置这个
        expressionHandler.setApplicationContext(applicationContext);
        WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
        webExpressionVoter.setExpressionHandler(expressionHandler);
        // 默认是只有webExpressionVoter这个投票器的,解析spel表达式,authorityDecisionVoter是我们自定义的,注入进来,解析的时候,两个投票器都会投票的
        List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(webExpressionVoter, authorityDecisionVoter);
        // 这个决策器表示,上面的这两个投票器如果有一个反对票,就抛出访问拒绝的异常
        UnanimousBased unanimousBased = new UnanimousBased(decisionVoters);
        return unanimousBased;
    }
}

注意下,我上面针对spel注释的说明。如果没有用到spel表达式,WebExpressionVoter (这个是默认的投票器)可以直接new一个出来。

spel的使用

演示一个spel的使用,这个不是必须的啊,这里演示下,用户认证的时候,想要打印一条日志:

@Slf4j
@Component
public class AuthenticationHandler {

    public boolean hasAuthenticated(HttpServletRequest request, Authentication authentication) {
        // 获取主体
        Object principal = authentication.getPrincipal();
        if (principal instanceof String) {
            log.error("匿名用户{}访问", principal);
            return false;
        }

        // 判断主体是否属于 UserDetails
        if (principal instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) principal;
            return userDetails.isEnabled() && userDetails.isCredentialsNonExpired();
        }
        return false;
    }
}

配置权限检查

@Component
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    private final AccessDecisionManager accessDecisionManager;

    public WebSecurityConfigurer(AccessDecisionManager accessDecisionManager) {
        this.accessDecisionManager = accessDecisionManager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 在这里自定义配置
        http.authorizeRequests()
                // 登录相关接口都允许访问
                .antMatchers("/login/**").permitAll()
                .anyRequest()
                // spel的使用
                .access("@authenticationHandler.hasAuthenticated(request, authentication)")
//                .authenticated()
                // 自定义权限检查主要是一行代码
                .accessDecisionManager(accessDecisionManager)
                .and()
				...
    }
}

基本使用示例就是这样,可以根据需要调整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不识君的荒漠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值