原创/朱季谦java
在Spring Security权限框架里,若要对后端http接口实现权限受权控制,有两种实现方式。程序员
1、一种是基于注解方法级的鉴权,其中,注解方式又有@Secured和@PreAuthorize两种。spring
@Secured如:后端
1 @PostMapping("/test") 2 @Secured({WebResRole.ROLE_PEOPLE_W}) 3 public void test(){ 4 ...... 5 return null; 6 }
@PreAuthorize如:设计模式
1 @PostMapping("save") 2 @PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')") 3 public RestResponse save(@RequestBody @Validated SysUser sysUser, BindingResult result) { 4 ValiParamUtils.ValiParamReq(result); 5 return sysUserService.save(sysUser); 6 }
2、一种基于config配置类,需在对应config类配置@EnableGlobalMethodSecurity(prePostEnabled = true)注解才能生效,其权限控制方式以下:安全
1 @Override 2 protected void configure(HttpSecurity httpSecurity) throws Exception { 3 //使用的是JWT,禁用csrf 4 httpSecurity.cors().and().csrf().disable() 5 //设置请求必须进行权限认证 6 .authorizeRequests() 7 //首页和登陆页面 8 .antMatchers("/").permitAll() 9 .antMatchers("/login").permitAll() 10 // 其余全部请求须要身份认证 11 .anyRequest().authenticated(); 12 //退出登陆处理 13 httpSecurity.logout().logoutSuccessHandler(...); 14 //token验证过滤器 15 httpSecurity.addFilterBefore(...); 16 }
这两种方式各有各的特色,在平常开发当中,普通程序员接触比较多的,则是注解方式的接口权限控制。springboot
那么问题来了,咱们配置这些注解或者类,其security框是如何帮作到能针对具体的后端API接口作权限控制的呢?app
单从一行@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")注解上看,是看不出任何头绪来的,若要回答这个问题,还需深刻到源码层面,方能对security受权机制有更好理解。cors
若要对这个过程作一个总的概述,笔者总体以本身的思考稍做了总结,能够简单几句话说明其总体实现,以该接口为例:框架
1 @PostMapping("save") 2 @PreAuthorize("hasAuthority('sys:user:add')") 3 public RestResponse save(@RequestBody @Validated SysUser sysUser, BindingResult result) { 4 ValiParamUtils.ValiParamReq(result); 5 return sysUserService.save(sysUser); 6 }
即,认证经过的用户,发起请求要访问“/save”接口,若该url请求在配置类里设置为必须进行权限认证的,就会被security框架使用filter拦截器对该请求进行拦截认证。拦截过程主要一个动做,是把该请求所拥有的权限集与@PreAuthorize设置的权限字符“sys:user:add”进行匹配,若能匹配上,说明该请求是拥有调用“/save”接口的权限,那么,就能够被容许执行该接口资源。
在springboot+security+jwt框架中,经过一系列内置或者自行定义的过滤器Filter来达到权限控制,如何设置自定义的过滤器Filter呢?例如,能够经过设置httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)来自定义一个基于JWT拦截的过滤器JwtFilter,这里的addFilterBefore方法将在下一篇文详细分析,这里暂不展开,该方法大概意思就是,将自定义过滤器JwtFilter加入到Security框架里,成为其中的一个优先安全Filter,代码层面就是将自定义过滤器添加到List<Filter> filters。
设置增长自行定义的过滤器Filter伪代码以下:
1 @Configuration 2 @EnableWebSecurity 3 @EnableGlobalMethodSecurity(prePostEnabled = true) 4 public class SecurityConfig extends WebSecurityConfigurerAdapter { 5 ...... 6 @Override 7 protected void configure(HttpSecurity httpSecurity) throws Exception { 8 //使用的是JWT,禁用csrf 9 httpSecurity.cors().and().csrf().disable() 10 //设置请求必须进行权限认证 11 .authorizeRequests() 12 ...... 13 //首页和登陆页面 14 .antMatchers("/").permitAll() 15 .antMatchers("/login").permitAll() 16 // 其余全部请求须要身份认证 17 .anyRequest().authenticated(); 18 ...... 19 //token验证过滤器 20 httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); 21 } 22 }
该过滤器类extrends继承BasicAuthenticationFilter,而BasicAuthenticationFilter是继承OncePerRequestFilter,该过滤器确保在一次请求只经过一次filter,而不须要重复执行。这样配置后,当请求过来时,会自动被JwtFilter类拦截,这时,将执行重写的doFilterInternal方法,在SecurityContextHolder.getContext().setAuthentication(authentication)认证经过后,会执行过滤器链FilterChain的方法chain.doFilter(request, response);
1 public class JwtFilter extends BasicAuthenticationFilter { 2 3 @Autowired 4 public JwtFilter(AuthenticationManager authenticationManager) { 5 super(authenticationManager); 6 } 7 8 @Override 9 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 10 // 获取token, 并检查登陆状态 11 // 获取令牌并根据令牌获取登陆认证信息 12 Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request); 13 // 设置登陆认证信息到上下文 14 SecurityContextHolder.getContext().setAuthentication(authentication); 15 16 chain.doFilter(request, response); 17 } 18 19 }
那么,问题来了,过滤器链FilterChain到底是什么?
这里,先点进去看下其类源码:
1 package javax.servlet; 2 3 import java.io.IOException; 4 5 public interface FilterChain { 6 void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException; 7 }
FilterChain只有一个 doFilter方法,这个方法的做用就是将请求request转发到下一个过滤器filter进行过滤处理操做,执行过程以下:
过滤器链像一条铁链,把相关的过滤器连接起来,请求线程如蚂蚁同样,会沿着这条链一直爬过去-----即,经过chain.doFilter(request, response)方法,一层嵌套一层地传递下去,当传递到该请求对应的最后一个过滤器,就会将处理完成的请求转发返回。所以,经过过滤器链,可实如今不一样的过滤器当中对请求request作处理,且过滤器之间彼此互不干扰。
这实际上是一种责任链的设计模式。在这种模式当中,一般每一个接受者都包含对另外一个接收者的引用。若是一个对象不能处理该请求,那么,它就会把相同的请求传给下一个接收者,以此类推。
Spring Security框架上过滤器链上都有哪些过滤器呢?
能够在DefaultSecurityFilterChain类根据输出相关log或者debug来查看Security都有哪些过滤器,如在DefaultSecurityFilterChain类中的构造器中打断点,如图所示,能够看到,自定义的JwtFilter过滤器也包含其中:
这些过滤器都在同一条过滤器链上,即经过chain.doFilter(request, response)可将请求一层接一层转发,处理请求接口是否受权的主要过滤器是FilterSecurityInterceptor,其主要做用以下:
1. 获取到需访问接口的权限信息,即@Secured({WebResRole.ROLE_PEOPLE_W}) 或@PreAuthorize定义的权限信息;
2. 根据SecurityContextHolder中存储的authentication用户信息,来判断是否包含与需访问接口的权限信息,若包含,则说明拥有该接口权限;
3. 主要受权功能在父类AbstractSecurityInterceptor中实现;
咱们将从FilterSecurityInterceptor这里开始重点分析Security受权机制原理的实现。
过滤器链将请求传递转发FilterSecurityInterceptor时,会执行FilterSecurityInterceptor的doFilter方法:
1 public void doFilter(ServletRequest request, ServletResponse response, 2 FilterChain chain) throws IOException, ServletException { 3 FilterInvocation fi = new FilterInvocation(request, response, chain); 4 invoke(fi); 5 }
在这段代码当中,FilterInvocation类是一个有意思的存在,其实它的功能很简单,就是将上一个过滤器传递过滤的request,response,chain复制保存到FilterInvocation里,专门供FilterSecurityInterceptor过滤器使用。它的有意思之处在于,是将多个参数统一概括到一个类当中,其到统一管理做用,你想,如果N多个参数,传进来都分散到类的各个地方,参数多了,代码多了,方法过于分散时,可能就很容易形成阅读过程当中,弄糊涂这些个参数都是哪里来了。但若统一概括到一个类里,就能很快定位其来源,方便代码阅读。网上有人提到该FilterInvocation类还起到解耦做用,即避免与其余过滤器使用一样的引用变量。
总而言之,这个地方的设定虽简单,但很值得咱们学习一番,将其思想运用到实际开发当中,不外乎也是一种能简化代码的方法。
FilterInvocation主要源码以下:
1 public class FilterInvocation { 2 3 private FilterChain chain; 4 private HttpServletRequest request; 5 private HttpServletResponse response; 6 7 8 public FilterInvocation(ServletRequest request, ServletResponse response, 9 FilterChain chain) { 10 if ((request == null) || (response == null) || (chain == null)) { 11 throw new IllegalArgumentException("Cannot pass null values to constructor"); 12 } 13 14 this.request = (HttpServletRequest) request; 15 this.response = (HttpServletResponse) response; 16 this.chain = chain; 17 } 18 ...... 19 }
FilterSecurityInterceptor的doFilter方法里调用invoke(fi)方法:
1 public void invoke(FilterInvocation fi) throws IOException, ServletException { 2 if ((fi.getRequest() != null) 3 && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) 4 && observeOncePerRequest) { 5 //筛选器已应用于此请求,每一个请求处理一次,因此不需从新进行安全检查 6 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 7 } 8 else { 9 // 第一次调用此请求时,需执行安全检查 10 if (fi.getRequest() != null && observeOncePerRequest) { 11 fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); 12 } 13 //1.受权具体实现入口 14 InterceptorStatusToken token = super.beforeInvocation(fi); 15 try { 16 //2.受权经过后执行的业务 17 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 18 } 19 finally { 20 super.finallyInvocation(token); 21 } 22 //3.后续处理 23 super.afterInvocation(token, null); 24 } 25 }
受权机制实现的入口是super.beforeInvocation(fi),其具体实如今父类AbstractSecurityInterceptor中实现,beforeInvocation(Object object)的实现主要包括如下步骤:
1、获取需访问的接口权限,这里debug的例子是调用了前文提到的“/save”接口,其权限设置是@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')"),根据下面截图,可知变量attributes获取了到该请求接口的权限:
2、获取认证经过以后保存在 SecurityContextHolder的用户信息,其中,authorities是一个保存用户所拥有所有权限的集合;
这里authenticateIfRequired()方法核心实现:
1 private Authentication authenticateIfRequired() { 2 Authentication authentication = SecurityContextHolder.getContext() 3 .getAuthentication(); 4 if (authentication.isAuthenticated() && !alwaysReauthenticate) { 5 ...... 6 return authentication; 7 } 8 authentication = authenticationManager.authenticate(authentication); 9 SecurityContextHolder.getContext().setAuthentication(authentication); 10 return authentication; 11 }
在认证过程经过后,执行SecurityContextHolder.getContext().setAuthentication(authentication)将用户信息保存在Security框架当中,以后可经过SecurityContextHolder.getContext().getAuthentication()获取到保存的用户信息;
3、尝试受权,用户信息authenticated、请求携带对象信息object、所访问接口的权限信息attributes,传入到decide方法;
decide()是决策管理器AccessDecisionManager定义的一个方法。
1 public interface AccessDecisionManager { 2 void decide(Authentication authentication, Object object, 3 Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, 4 InsufficientAuthenticationException; 5 boolean supports(ConfigAttribute attribute); 6 boolean supports(Class<?> clazz); 7 }
AccessDecisionManager是一个interface接口,这是受权体系的核心。FilterSecurityInterceptor 在鉴权时,就是经过调用AccessDecisionManager的decide()方法来进行受权决策,若能经过,则可访问对应的接口。
AccessDecisionManager类的方法具体实现都在子类当中,包含AffirmativeBased、ConsensusBased、UnanimousBased三个子类;
AffirmativeBased表示一票经过,这是AccessDecisionManager默认类;
ConsensusBased表示少数服从多数;
UnanimousBased表示一票反对;
如何理解这个投票机制呢?
点进去AffirmativeBased类里,能够看到里面有一行代码int result = voter.vote(authentication, object, configAttributes):
这里的AccessDecisionVoter是一个投票器,用到委托设计模式,即AffirmativeBased类会委托投票器进行选举,而后将选举结果返回赋值给result,而后判断result结果值,若为1,等于ACCESS_GRANTED值时,则表示可一票经过,也就是,容许访问该接口的权限。
这里,ACCESS_GRANTED表示赞成、ACCESS_DENIED表示拒绝、ACCESS_ABSTAIN表示弃权:
1 public interface AccessDecisionVoter<S> { 2 int ACCESS_GRANTED = 1;//表示赞成 3 int ACCESS_ABSTAIN = 0;//表示弃权 4 int ACCESS_DENIED = -1;//表示拒绝 5 ...... 6 }
那么,什么状况下,投票结果result为1呢?
这里须要研究一下投票器接口AccessDecisionVoter,该接口的实现以下图所示:
这里简单介绍两个经常使用的:
1. RoleVoter:这是用来判断url请求是否具有接口须要的角色,这种主要用于使用注解@Secured处理的权限;
2. PreInvocationAuthorizationAdviceVoter:针对相似注解@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")处理的权限;
到这一步,代码就开始难懂了,这部分封装地过于复杂,整体的逻辑,是将用户信息所具备的权限与该接口的权限表达式作匹配,若能匹配成功,返回true,在三目运算符中,
allowed ? ACCESS_GRANTED : ACCESS_DENIED,就会返回ACCESS_GRANTED ,即表示经过,这样,返回给result的值就为1了。
到此为止,本文就结束了,笔者仍存在不足之处,欢迎各位读者可以给予珍贵的反馈,也算是对笔者写做的一种鼓励。