Spring Security 实战干货:内置 Filter 全解析 中提到的第 32 个 Filter
不知道你是否有印象。它决定了访问特定路径应该具有的权限,访问的用户的角色,权限是什么?访问的路径须要什么样的角色和权限? 它就是 FilterSecurityInterceptor
,正是咱们须要的那个轮子。html
过滤器排行榜第 32 位!肩负对 http 接口权限认证的重要职责。咱们来看它的过滤逻辑:java
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
复制代码
初始化了一个 FilterInvocation
而后被 invoke
方法处理:web
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
复制代码
每一次请求被 Filter
过滤都会被打上标记 FILTER_APPLIED
,没有被打上标记的 走了父类的 beforeInvocation
方法而后再进入过滤器链,看上去是走了一个前置的处理。那么前置处理了什么呢? 首先会经过 this.obtainSecurityMetadataSource().getAttributes(Object object)
拿受保护对象(就是当前请求的 URI)全部的映射角色(ConfigAttribute
直接理解为角色的进一步抽象) 。而后使用访问决策管理器 AccessDecisionManager
进行投票决策来肯定是否放行。 咱们来看一下这两个接口。spring
安全拦截器和“安全对象”模型参考:数据库
元数据加载器 FilterInvocationSecurityMetadataSource
是 FilterSecurityInterceptor
的属性,UML 图以下:json
FilterInvocationSecurityMetadataSource
是一个标记接口,其抽象方法继承自 SecurityMetadataSource``AopInfrastructureBean
。它的做用是来获取咱们上一篇文章所描述的资源角色元数据。缓存
ConfigAttribute
支持全部的思路仅供参考,实际以你的业务为准!安全
Collection<ConfigAttribute> getAttributes(Object object)
方法的实现:确定是获取请求中的 URI
来和 全部的 资源配置中的 Ant Pattern
进行匹配以获取对应的资源配置, 这里须要将资源查询接口查询的资源配置封装为 AntPathRequestMatcher
以方便进行 Ant Match
。 这里须要特别提一下若是你使用 Restful 风格,这里 增删改查 将很是方便你来对资源的管控。参考的实现:bash
@Bean
public RequestMatcherCreator requestMatcherCreator() {
return metaResources -> metaResources.stream()
.map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
.collect(Collectors.toSet());
}
复制代码
HttpRequest
匹配到对应的资源配置后就能根据资源配置去取对应的角色集合。这些角色将交给访问决策管理器 AccessDecisionManager
进行投票表决以决定是否放行。session
决策管理器 AccessDecisionManager
用来投票决定是否放行请求。
public interface AccessDecisionManager {
// 决策 主要经过其持有的 AccessDecisionVoter 来进行投票决策
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
// 以肯定AccessDecisionManager是否能够处理传递的ConfigAttribute
boolean supports(ConfigAttribute attribute);
//以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全 object 类型。
boolean supports(Class<?> clazz);
}
复制代码
AccessDecisionManager
有三个默认实现:
投票决策模型参考:
动态控制权限就须要咱们实现本身的访问决策器。咱们上面说了默认有三个实现,这里我选择基于确定的决策器 AffirmativeBased
,只要用户持有一个持有一个角色包含想要访问的资源就能访问该资源。接下来就是投票器 AccessDecisionVoter
的定义了,其实咱们能够选择内置的
决策投票器 AccessDecisionVoter
将安全配置属性 ConfigAttribute
以特定的逻辑进行解析并基于特定的策略来进行投票,投同意票时总票数 +1
,反对票总票数 -1
,弃权时总票数 +0
, 而后由 AccessDecisionManager
根据具体的计票策略来决定是否放行。
Spring Security 提供的最经常使用的投票器是角色投票器 RoleVoter
,它将安全配置属性 ConfigAttribute
视为简单的角色名称,并在用户被分配了该角色时授予访问权限。 若是任何 ConfigAttribute
之前缀 ROLE_
开头,它将投票。若是有一个 GrantedAuthority
返回一个字符串(经过 getAuthority()
方法)正好等于一个或多个从前缀 ROLE_
开始的 ConfigAttributes
,它将投票授予访问权限。若是没有任何以 ROLE_
开头的 ConfigAttributes
匹配,则 RoleVoter
将投票拒绝访问。若是没有 ConfigAttribute
以 ROLE_为前缀,将弃权。 这正是咱们想要的投票器。
一般要求应用程序中的特定角色应自动“包含”其余角色。例如,在具备 ROLE_ADMIN
和 ROLE_USER
角色概念的应用中,您可能但愿管理员可以执行普通用户能够执行的全部操做。你不得不进行各类复杂的逻辑嵌套来知足这一需求。如今幸亏有了 RoleHierarchyVoter
能够帮你减小这种负担。 它由上面的 RoleVoter
派生,经过配置了一个 RoleHierarchy
就能够实现 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST
这种层次包含结构,左边的必定能访问右边能够访问的资源。具体的配置规则为:角色从左到右、从高到低以 >
相连(注意两个空格),以换行符 \n
为分割线。举个例子
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
复制代码
请注意动态配置中你须要自行实现角色分层的逻辑。DEMO 中并未对该风格进行实现。
配置须要两个方面。
咱们须要将元数据加载器 和 访问决策器注入 Spring IoC :
/** * 动态权限组件配置 * * @author Felordcn */
@Configuration
public class DynamicAccessControlConfiguration {
/** * RequestMatcher 生成器 * @return RequestMatcher */
@Bean
public RequestMatcherCreator requestMatcherCreator() {
return metaResources -> metaResources.stream()
.map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
.collect(Collectors.toSet());
}
/** * 元数据加载器 * * @return dynamicFilterInvocationSecurityMetadataSource */
@Bean
public FilterInvocationSecurityMetadataSource dynamicFilterInvocationSecurityMetadataSource() {
return new DynamicFilterInvocationSecurityMetadataSource();
}
/** * 角色投票器 * @return roleVoter */
@Bean
public RoleVoter roleVoter() {
return new RoleVoter();
}
/** * 基于确定的访问决策器 * * @param decisionVoters AccessDecisionVoter类型的 Bean 会自动注入到 decisionVoters * @return affirmativeBased */
@Bean
public AccessDecisionManager affirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
return new AffirmativeBased(decisionVoters);
}
}
复制代码
Spring Security 的 Java Configuration 不会公开它配置的每一个 object 的每一个 property。这简化了大多数用户的配置。 虽然有充分的理由不直接公开每一个 property,但用户可能仍须要像本文同样的取实现个性化需求。为了解决这个问题,Spring Security 引入了 ObjectPostProcessor
的概念,它可用于修改或替换 Java Configuration 建立的许多 Object
实例。 FilterSecurityInterceptor
的替换配置正是经过这种方式来进行:
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
private static final String LOGIN_PROCESSING_URL = "/process";
/** * Json login post processor json login post processor. * * @return the json login post processor */
@Bean
public JsonLoginPostProcessor jsonLoginPostProcessor() {
return new JsonLoginPostProcessor();
}
/** * Pre login filter pre login filter. * * @param loginPostProcessors the login post processors * @return the pre login filter */
@Bean
public PreLoginFilter preLoginFilter(Collection<LoginPostProcessor> loginPostProcessors) {
return new PreLoginFilter(LOGIN_PROCESSING_URL, loginPostProcessors);
}
/** * Jwt 认证过滤器. * * @param jwtTokenGenerator jwt 工具类 负责 生成 验证 解析 * @param jwtTokenStorage jwt 缓存存储接口 * @return the jwt authentication filter */
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {
return new JwtAuthenticationFilter(jwtTokenGenerator, jwtTokenStorage);
}
/** * The type Default configurer adapter. */
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private PreLoginFilter preLoginFilter;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
@Autowired
private AccessDecisionManager accessDecisionManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
// session 生成策略用无状态策略
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
.and()
// 动态权限配置
.authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(filterSecurityInterceptorObjectPostProcessor())
.and()
.addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
// jwt 必须配置于 UsernamePasswordAuthenticationFilter 以前
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 登陆 成功后返回jwt token 失败后返回 错误信息
.formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
.and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());
}
/** * 自定义 FilterSecurityInterceptor ObjectPostProcessor 以替换默认配置达到动态权限的目的 * * @return ObjectPostProcessor */
private ObjectPostProcessor<FilterSecurityInterceptor> filterSecurityInterceptorObjectPostProcessor() {
return new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
return object;
}
};
}
}
}
复制代码
而后你编写一个 Controller
方法就将其在数据库注册为一个资源进行动态的访问控制了。无须注解或者更详细的 Java Config 配置。
从最开始到如今一共 10 个 DEMO 。咱们按部就班地从如何学习 Spring Security 到目前实现了基于 RBAC、动态的权限资源访问控制。若是你能坚持到如今那么已经能知足了一些基本开发定制的须要。固然 Spring Security 还有不少局部的一些概念,我也会在之后抽时间进行讲解。
我先喘口气休几天。后续的一些 Spring Security 教程将围绕目前更加流行的 OAuth2.0、 SSO 、OpenID 展开。敬请关注 felord.cn
老规矩, 关注 Felordcn 回复 day10 获取 DEMO 。
关注公众号:Felordcn获取更多资讯