三、Spring Security之权限校验及源码解析

一、源码解析

1、权限校验涉及的相关类图
在这里插入图片描述
2、权限校验时序图:
在这里插入图片描述3.在权限校验过程中,几个比较关键的类:

  • UsernamePasswordAuthenticationFilter
  • AnonymousAuthenticationFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
    调用流程如下:
  • AbstractAuthenticationProcessingFilter.doFilter()
    • UsernamePasswordAuthenticationFilter.attemptAuthentication()
      • AnonymousAuthenticationFilter.doFilter()AnonymousAuthenticationFilter过滤器是在UsernamePasswordAuthenticationFilter等过滤器之后,如果它前面的过滤器都没有认证成功,Spring Security则为当前的SecurityContextHolder中添加一个Authenticaiton 的匿名实现类AnonymousAuthenticationToken
      • FilterSecurityInterceptor.doFilter():此过滤器为认证授权过滤器链中最后一个过滤器,该过滤器之后就是请求真正的请求服务
      • ExceptionTranslationFilter.doFilter()ExceptionTranslationFilter 异常处理过滤器,该过滤器用来处理在系统认证授权过程中抛出的异常(也就是下一个过滤器FilterSecurityInterceptor),主要是 处理 AuthenticationExceptionAccessDeniedException

其中,重点过滤器就是 FilterSecurityInterceptor ,也是接下来我们在权限校验过程中接触最多的类。

二、FilterSecurityInterceptor 核心类

FilterSecurityInterceptor.class

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        this.invoke(fi);
    }
    
    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            if (fi.getRequest() != null && this.observeOncePerRequest) {
                fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }
			#1. before invocation重要
            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
            	#2. 可以理解开始请求真正的 /persons 服务
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.finallyInvocation(token);
            }
			#3. after Invocation
            super.afterInvocation(token, (Object)null);
        }

    }

1.before invocation重要
2.请求真正的 /persons 服务
3.after Invocation

三个部分中,最重要的是 #1,该过程中会调用 AccessDecisionManager 来验证当前已认证成功的用户是否有权限访问该资源;

AbstractSecurityInterceptor#beforeInvocation()
protected InterceptorStatusToken beforeInvocation(Object object) {
        .....
        	#1.重点(获取当前请求路径应具备的权限)
            Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
            .....
                }

                Authentication authenticated = this.authenticateIfRequired();

                try {
                	#2.重点(将当前认证对象进行权限验证)
                    this.accessDecisionManager.decide(authenticated, object, attributes);
                } catch (AccessDeniedException var7) {
                    .....
                }

                ......

                if (this.publishAuthorizationSuccess) {
                    this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));
                }

                ......
    }

1.重点(获取当前请求路径应具备的权限)
2.重点(将当前认证对象进行权限验证)
这两部分都需要自定义实现,参见下方:

1.获取当前请求路径应具备的权限
  要想获取当前请求路径应具备的权限,那么就涉及到一个权限资源类 SecurityMetadataSource
  要实现动态的权限验证,当然要先有对应的访问权限资源了。Spring Security是通过SecurityMetadataSource来加载访问时所需要的具体权限,所以第一步需要实现SecurityMetadataSource。
  SecurityMetadataSource是一个接口,同时还有一个接口FilterInvocationSecurityMetadataSource继承于它,但FilterInvocationSecurityMetadataSource只是一个标识接口,对应于FilterInvocation,本身并无任何内容:

public interface FilterInvocationSecurityMetadataSource extends SecurityMetadataSource {
}

  因为我们做的一般都是web项目,所以实际需要实现的接口是FilterInvocationSecurityMetadataSource,这是因为Spring Security中很多web才使用的类参数类型都是FilterInvocationSecurityMetadataSource。案例,返回当前请求在数据库中对应的权限资源集合

/**
 * Created by sang on 2017/12/28.
 * 返回权限资源
 */
@Component
public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     *     getAttributes方法返回本次访问需要的权限,可以有多个权限。在上面的实现中如果没有匹配的url直接返回
     * ROLE_LOGIN(登录),也就是没有配置权限的url默认都需要登录。
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) {
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        List<Menu> allMenu = menuService.getAllMenu();
        for (Menu menu : allMenu) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)
                    &&menu.getRoles().size()>0) {
                List<Role> roles = menu.getRoles();
                int size = roles.size();
                String[] values = new String[size];
                for (int i = 0; i < size; i++) {
                    values[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(values);
            }
        }
        //没有匹配上的资源,都是登录访问
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    /**
     *     getAllConfigAttributes方法如果返回了所有定义的权限资源,Spring Security会在启动时校验每个ConfigAttribute
     * 是否配置正确,不需要校验直接返回null。
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

2.将当前认证对象进行权限验证
  有了权限资源,知道了当前访问的url需要的具体权限,接下来就是决策当前的访问是否能通过权限验证了。
  需要通过实现自定义的AccessDecisionManager来实现。Spring Security内置的几个AccessDecisionManager就不讲了,在web项目中基本用不到,在项目中一般是自定义实现AccessDecisionManager,案例如下:

/**
 * 权限校验
 */
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
    /**
     *
     * @param auth authentication包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中
     *             设置的authorities。
     * @param o    object就是FilterInvocation对象,可以得到request等web资源。
     * @param cas  是本次访问需要的权限(也就是我们自定义 CustomMetadataSource#getAttributes() 中返回的权限集合)
     */
    @Override
    public void decide(Authentication auth, Object o, Collection<ConfigAttribute> cas){
        Iterator<ConfigAttribute> iterator = cas.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute ca = iterator.next();
            //当前请求需要的权限
            String needRole = ca.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (auth instanceof AnonymousAuthenticationToken) {
                    throw new BadCredentialsException("未登录");
                } else {
                    return;
                }
            }
            //当前用户所具有的权限
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足!");
    }
    /**
     * supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
     */
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }
    /**
     * supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

上面的实现中,当需要多个权限时只要有一个符合则校验通过,即的关系,想要并的关系只需要修改这里的逻辑即可。

3.配置使用上方自定义实现类
  上面权限的资源和验证我们已经都实现了,接下来就是指定让Spring Security使用我们自定义的实现类了。
  在Spring Boot中提供了ObjectPostProcessor以让用户实现更多想要的高级配置。具体看下面代码,注意withObjectPostProcessor部分:

	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(metadataSource);
                        o.setAccessDecisionManager(urlAccessDecisionManager);
                        return o;
                    }
                })
                .and()
                .formLogin().loginPage("/login_p").loginProcessingUrl("/login")
                .usernameParameter("username").passwordParameter("password")
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req,
                                                        HttpServletResponse resp,
                                                        AuthenticationException e) throws IOException {
                        resp.setContentType("application/json;charset=utf-8");
                        RespBean respBean = null;
                        if (e instanceof BadCredentialsException ||
                                e instanceof UsernameNotFoundException) {
                            respBean = RespBean.error("账户名或者密码输入错误!");
                        } else if (e instanceof LockedException) {
                            respBean = RespBean.error("账户被锁定,请联系管理员!");
                        } else if (e instanceof CredentialsExpiredException) {
                            respBean = RespBean.error("密码过期,请联系管理员!");
                        } else if (e instanceof AccountExpiredException) {
                            respBean = RespBean.error("账户过期,请联系管理员!");
                        } else if (e instanceof DisabledException) {
                            respBean = RespBean.error("账户被禁用,请联系管理员!");
                        } else {
                            respBean = RespBean.error("登录失败!");
                        }
                        resp.setStatus(401);
                        ObjectMapper om = new ObjectMapper();
                        PrintWriter out = resp.getWriter();
                        out.write(om.writeValueAsString(respBean));
                        out.flush();
                        out.close();
                    }
                })
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req,
                                                        HttpServletResponse resp,
                                                        Authentication auth) throws IOException {
                        resp.setContentType("application/json;charset=utf-8");
                        RespBean respBean = RespBean.ok("登录成功!", HrUtils.getCurrentHr());
                        ObjectMapper om = new ObjectMapper();
                        PrintWriter out = resp.getWriter();
                        out.write(om.writeValueAsString(respBean));
                        out.flush();
                        out.close();
                    }
                })
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        RespBean respBean = RespBean.ok("注销成功!");
                        ObjectMapper om = new ObjectMapper();
                        PrintWriter out = resp.getWriter();
                        out.write(om.writeValueAsString(respBean));
                        out.flush();
                        out.close();
                    }
                })
                .permitAll()
                .and().csrf().disable()
                .exceptionHandling().accessDeniedHandler(deniedHandler);
    }

主要是在创建默认的FilterSecurityInterceptor的时候把我们的accessDecisionManager和securityMetadataSource设置进去。

上面3步骤即是我们在权限过程中,编程人员需要关注的部分!

参见Spring Security源码分析二:Spring Security授权过程

关于动态权限认证案例,参见 Spring Security系列四 自定义决策管理器(动态权限码)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值