SpringSecurity你想知道的都在这里了

写在前面:

  SpringSecurity顾名思义,就是Spring的安全模块,主要做用就是对资源的安全访问控制,安全控制分为两个方面:认证和受权,git

认证:就是对资源调用者的身份进行核实,好比你想访问某个接口或者某个方法,可能须要你登录,输入你的帐号密码,而后后台验证这个帐号是否具备访问这个资源的权力。github

受权:就是你的身份拥有访问的权限就给你权力去访问资源,web

宏观流程:

  1. 用户登陆时输入的帐号密码等信息会被封装成Authentication,通常实现类是xxxxxxtoken例如UsernamePasswordAuthenticationToken
  2. 而后将封装有用户登录信息的Token提交到认证中心AuthenticationManager,而AuthenticationManager接口的经常使用实现类是ProviderManager,ProviderManager又将认证的详细任务交给一系列的AuthenticationProvider,不一样的AuthenticationProvider可能会有不一样的认证方式,可是通常只要有一个经过就ok,AuthenticationProvider的经常使用实现类好比DAOAuthenticationProvider,
  3. 咱们确定要拿用户登录的信息和数据库中的信息进行比较,而去数据库加载数据的事情,AuthenticationProvider不作,他们会把这个工做托付给一个service,好比DAOAuthenticationProvider会经过UserDetailService加载数据库里面的用户的数据,从数据库读出来的用户的信息封装成一个UserDetails,其实若是UserDetails能够缓存起来的话,咱们就不用每次去查库了,因此能够有一些CachingUserDetailService,若是缓存没有的时候咱们再去查库也ok的。
  4. 这个时候就能够在认证中心AuthenticationManager中进行认证了,这个工做是调用AuthenticationManager的authenticate()方法完成的,即比较封装了用户输入的登录信息的Token和数据库中对应用户的信息,若是帐号密码没问题,就会返回一个Authentication对象,这里面封装了用户在数据库中的主要信息,好比什么角色啦拥有什么权力啦之类的。固然由于咱们在数据库中存储的用户密码通常是加过密的,可是用户登陆输入的通常是明文密码,因此对比时咱们可能要给AuthenticationProvider指定一个PasswordEncoder对象用来将解码密码为明文密码进行比较,比对后返回Authentication对象时通常不会保存密码,为了安全考虑。
  5. 返回的Authentication会被存入到一个上下文holder中,即SecurityContextHolder,SecurityContextHolder的存储策略多种多样,能够本身指定。

到此为止的流程基本就是大概的认证部分

AccessDecisionManager是受权中心,AccessDecisionManager是由AbstractSecurityInterceptor调用的,主要用来决定是否有资格进行对要访问资源的权限,它的void decide(Authenticationauthentication, Object object,Collection<ConfigAttribute> configAttributes)throwsAccessDeniedException,InsufficientAuthenticationException;三个参数分别以下:spring

  1. Authentication:用户登陆后鉴权经过存储的用户的信息,
  2. object:要访问的资源,
  3. configAttribute:与受保护资源相关的属性,

好比咱们调用一个方法,须要拥有ROLE_ADMIN属性,这个ROLE_ADMIN就是对应的ConfigAttribute, 若是decide()方法不throw错误则说明受权ok。数据库

这里面用谁和谁对比呢?就是用authentication中的List<GrantedAuthority>与List<configAttribute>进行对比,用上面的例子来讲,GrantedAuthority就是用户在数据库中存在的具有的角色,好比这个用户具有ROLE_USER,ROLE_ADMIN等角色,而后访问这个资源须要具有的角色是ROLE_ADMIN,这样一对比这个用户具有这个角色,后端

其实在SpringSecurity默认的受权实现中是经过投票机制的,在SpringSecurity的中几个默认的AccessDecisionManager的实现类的 decide()方法受权的时候,是经过AccessDecisionVoter投票机制进行的,固然不一样的AccessDecisionVoter的实现投票机制也不同。springSecurity在web方式时,用Voter来处理是否拥有什么角色 好比WebExpressionVoter和AuthenticatedVoter,而后咱们就能够顺利的访问资源了api

上面这一部分就是受权模块了

固然这是咱们简单的对比,是为了让你了解到在SpringSecurity中的各类概念对应的是实际的什么东西。脑子里有这些概念以后再去研究springsecurity框架或者读别人的博客解析就不会一点也对应不上了。缓存

资源访问后

在访问完资源以后Spring Security还为咱们提供了一个AfterInvocationManager接口,它容许咱们在受保护对象访问完成后对返回值进行修改或者进行权限鉴定,看是否须要抛出AccessDeniedException,其将由AbstractSecurityInterceptor的子类进行调用。安全

 相似于AuthenticationManager,AfterInvocationManager拥有一个默认的实现类AfterInvocationProviderManager,其中拥有一个由AfterInvocationProvider组成的集合,AfterInvocationProvider与AfterInvocationManager具备相同的方法定义,在调用AfterInvocationProviderManager中的方法时实际上就是依次调用一系列AfterInvocationProvider对象的相应方法。须要注意的是AfterInvocationManager须要在受保护对象成功被访问后才能执行。cookie

角色继承

  1. 对于角色继承这种需求也是常常有的,好比要求ROLE_ADMIN将拥有全部的ROLE_USER所具备的权限。固然咱们能够给拥有ROLE_ADMIN角色的用户同时授予ROLE_USER角色来达到这一效果或者修改须要ROLE_USER进行访问的资源使用ROLE_ADMIN也能够访问。Spring Security为咱们提供了一种更为简便的办法,那就是角色的继承,它容许咱们的ROLE_ADMIN直接继承ROLE_USER,这样全部ROLE_USER能够访问的资源ROLE_ADMIN也能够访问。
  2. 定义角色的继承咱们须要在ApplicationContext中定义一个RoleHierarchy,
  3. 而后再把它赋予给一个RoleHierarchyVoter,
  4. 以后再把该RoleHierarchyVoter加入到咱们基于Voter的AccessDecisionManager中,并指定当前使用的AccessDecisionManager为咱们本身定义的那个就ok了。

从上面咱们基本了解了SpringSecurity中的主要概念和大体流程,下面咱们就一些细节展开:

首先ProviderManager:

ProviderManager 能够有一个父类认证器,若是全部的提供者返回null,则将再交给父类去认证。 若是父类不可用,则会致使 AuthenticationException。
有时应用程序具备受保护资源的逻辑组(例如全部与路径模式/ api / **相匹配的Web资源),而且每一个组能够具备其本身的专用 AuthenticationManager。 一般,每一个人都是一个 ProviderManager,他们共享一个父类。 父母是一种“全局”资源,充当全部提供者的失败回调。
 

 接下来就是SecurityContext和SecurityContextHolder:

 可能你早就有这么一个疑问了,既然SecurityContext是存放在ThreadLocal中的,并且在每次权限鉴定的时候都是从ThreadLocal中获取SecurityContext中对应的Authentication所拥有的权限,而且不一样的request是不一样的线程,为何每次均可以从ThreadLocal中获取到当前用户对应的SecurityContext呢?在Web应用中这是经过SecurityContextPersistentFilter实现的,默认状况下其会在每次请求开始的时候从session中获取SecurityContext,而后把它设置给SecurityContextHolder,在请求结束后又会将SecurityContextHolder所持有的SecurityContext保存在session中,而且清除SecurityContextHolder所持有的SecurityContext。这样当咱们第一次访问系统的时候,SecurityContextHolder所持有的SecurityContext确定是空的,待咱们登陆成功后,SecurityContextHolder所持有的SecurityContext就不是空的了,且包含有认证成功的Authentication对象,待请求结束后咱们就会将SecurityContext存在session中,等到下次请求的时候就能够从session中获取到该SecurityContext并把它赋予给SecurityContextHolder了,因为SecurityContextHolder已经持有认证过的Authentication对象了,因此下次访问的时候也就再也不须要进行登陆认证了。

若是你须要访问Web端点中当前已经过身份验证的用户,则能够在 @RequestMapping 中使用方法参数。 例如。

@RequestMapping("/foo")

public String foo(@AuthenticationPrincipal User user) {

  ... // do stuff with user

}

这个注解将当前Authentication从SecurityContext中抽出,并调用其上的 getPrincipal() 方法来产生方法参数。 认证中的委托人类型取决于用于验证认证的认证管理器,因此这对于得到对用户数据的类型安全引用是一个有用的小技巧。

若是使用Spring Security,则HttpServletRequest中的Principal将是Authentication类型,所以也能够直接使用它:

@RequestMapping("/foo")

public String foo(Principal principal) {

  Authentication authentication = (Authentication) principal;

  User = (User) authentication.getPrincipal();

  ... // do stuff with user

}

若是你须要编写在没有使用Spring Security的状况下工做的代码,那么这有时候会颇有用(你须要在加载Authentication类时更加谨慎)。

异步安全设置

若是咱们后续须要异步操做的时候,为了能继续复用用户登陆信息,springsecurity也有对应的支持,因为 SecurityContext 是线程绑定的,所以若是要执行任何调用安全方法的后台处理,例如与@Async,你须要确保上下文传播。 这归结为将 SecurityContext 包装在后台执行的任务(Runnable,Callable,etc)中。 Spring Security 提供了一些帮助器,使之变得简单,好比Runnable和Callable的包装器。 要将 SecurityContext 传播到@Async方法,你须要提供一个 AsyncConfigurer 并确保 Executor 的类型正确:

@Configuration

public class ApplicationConfiguration extends AsyncConfigurerSupport {

 

  @Override

  public Executor getAsyncExecutor() {

    return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));

  }

}

方法安全:

除了对其余的资源的鉴权,SpringSecurity还支持方法级别的资源的控制访问,Spring Security还支持将访问规则应用于Java方法。 对于Spring Security来讲,这只是一种不一样类型的“受保护的资源”。 对于用户来讲,这意味着使用相同格式的ConfigAttribute字符串(例如角色或表达式)来声明访问规则,可是在代码中具备不一样的配置。 第一步是启用方法安全配置,例如在咱们的应用程序的顶级配置中

@SpringBootApplication

@EnableGlobalMethodSecurity(securedEnabled = true)

public class SampleSecureApplication {

}

@Service

public class MyService {

 

  @Secured("ROLE_USER")

  public String secure() {

    return "Hello Security";

  }

 

}

上面的两个注解@ EnableGlobalMethodSecurity分别是打开方法级别的校验,@Secured("ROLE_USER")是只有User级别的用户能够调用方法secure(),还有其余的注解能够用于强制执行安全约束的方法,特别是@PreAuthorize@PostAuthorize,它们容许你编写包含对方法参数和返回值分别引用的表达式.

Web中的SpringSecurity的实现方式

Web层中的Spring Security(用于UI和HTTP后端)基于Servlet过滤器,因此首先查看过滤器的做用是颇有帮助的。 下图显示了单个HTTP请求的处理程序的典型分层结构

其实Filter的主干流成如上图所示,SpringBoot管理Filter顺序的机制有两种

  1. Filter类型的bean添加了@Beans和@Oreder注解或者实现了Ordered接口
  2. 它们能够是 FilterRegistrationBean 自己的Order属性做为其API的一部分

Spring Security 做为一个单独的过滤器安装在链中,其配置类型为 FilterChainProxy,缘由很快很快就会被揭示。在Spring Boot应用程序中,安全过滤器是ApplicationContext中的@Bean,并具备默认配置,以便将其应用于每一个请求。它被安装在由 SecurityProperties.DEFAULT_FILTER_ORDER 定义的位置,而该位置又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER(Spring Boot应用程序在包装请求时修改其行为的指望过滤器的最大顺序)决定。除此以外还有更多的内容:从容器的角度来看,Spring Security是一个单一的过滤器,但里面还有额外的过滤器,每一个过滤器都扮演着特殊的角色。这是一张图片:

其实上面的Filter对应于web.xml中,

  1. <filter>  
  2.     <filter-name>springSecurityFilterChain</filter-name>  
  3.     <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filt       er-class>  
  4. </filter>  
  5. <filter-mapping>  
  6.     <filter-name>springSecurityFilterChain</filter-name>  
  7.     <url-pattern>/*</url-pattern>  
  8. </filter-mapping>  

这里ServletContext在加载Web.xml后,会经过反射调用DelegatingFilterProxy的init()方法,而后从ApplicationContext中获取注册在Spring上下文中beanName是targetBeanName,若是在web.xml中没有指定的话,默认使用<filter-name>中定义的springSecurityFilterChain,也就是从Application中获取名为springSecurityFilter的bean实例,而后赋值给DelegatingFilterProxy类中的代理变量delegate, 而后调用delegate的doFilter()方法,

那么这个名为“springSecurityFilterChain”的bean是谁呢?

咱们知道当咱们启用SpringSecurity时咱们会用@EnableWebSecurity或者继承WebSecurityConfigurerAdapter,而后咱们

代理委托给一个 FilterChainProxy,一般使用固定的名称:springSecurityFilterChain。 FilterChainProxy 包含全部安全逻辑,内部安排为过滤器的一个或多个链。全部的过滤器都有相同的API(他们都实现了Servlet规范中的Filter接口),他们都有机会否决链的其他部分。

在同一个顶级FilterChainProxy中,能够有多个由 Spring Security 管理的过滤器链,而且容器都是未知的。 Spring Security筛选器包含一个筛选器链列表,并向与之匹配的第一个链派发一个请求。下图显示了匹配请求路径(/foo/** 在 /** 以前匹配)的转发状况。这是很是广泛的,但不是匹配请求的惟一方法。这个调度过程最重要的特色是只有一个链处理请求。

SpringSecurity中默认配置的Filter

  Spring Security的底层是经过一系列的Filter来管理的,每一个Filter都有其自身的功能,并且各个Filter在功能上还有关联关系,因此它们的顺序也是很是重要的。

 Filter顺序

       Spring Security已经定义了一些Filter,无论实际应用中你用到了哪些,它们应当保持以下顺序。

       (1)ChannelProcessingFilter,若是你访问的channel错了,那首先就会在channel之间进行跳转,如http变为https。

       (2)SecurityContextPersistenceFilter,这样的话在一开始进行request的时候就能够在SecurityContextHolder中创建一个SecurityContext,而后在请求结束的时候,任何对SecurityContext的改变均可以被copy到HttpSession。

       (3)ConcurrentSessionFilter,由于它须要使用SecurityContextHolder的功能,并且更新对应session的最后更新时间,以及经过SessionRegistry获取当前的SessionInformation以检查当前的session是否已通过期,过时则会调用LogoutHandler。

       (4)认证处理机制,如UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等,以致于SecurityContextHolder能够被更新为包含一个有效的Authentication请求。

       (5)SecurityContextHolderAwareRequestFilter,它将会把HttpServletRequest封装成一个继承自HttpServletRequestWrapper的SecurityContextHolderAwareRequestWrapper,同时使用SecurityContext实现了HttpServletRequest中与安全相关的方法。

       (6)JaasApiIntegrationFilter,若是SecurityContextHolder中拥有的Authentication是一个JaasAuthenticationToken,那么该Filter将使用包含在JaasAuthenticationToken中的Subject继续执行FilterChain。

       (7)RememberMeAuthenticationFilter,若是以前的认证处理机制没有更新SecurityContextHolder,而且用户请求包含了一个Remember-Me对应的cookie,那么一个对应的Authentication将会设给SecurityContextHolder。

       (8)AnonymousAuthenticationFilter,若是以前的认证机制都没有更新SecurityContextHolder拥有的Authentication,那么一个AnonymousAuthenticationToken将会设给SecurityContextHolder。

       (9)ExceptionTransactionFilter,用于处理在FilterChain范围内抛出的AccessDeniedException和AuthenticationException,并把它们转换为对应的Http错误码返回或者对应的页面。

       (10)FilterSecurityInterceptor,保护Web URI,而且在访问被拒绝时抛出异常。

 

1.2     添加Filter到FilterChain

       当咱们在使用NameSpace时,Spring Security是会自动为咱们创建对应的FilterChain以及其中的Filter。但有时咱们可能须要添加咱们本身的Filter到FilterChain,又或者是由于某些特性须要本身显示的定义Spring Security已经为咱们提供好的Filter,而后再把它们添加到FilterChain。使用NameSpace时添加Filter到FilterChain是经过http元素下的custom-filter元素来定义的。定义custom-filter时须要咱们经过ref属性指定其对应关联的是哪一个Filter,此外还须要经过position、before或者after指定该Filter放置的位置。诚如在上一节《Filter顺序》中所提到的那样,Spring Security对FilterChain中Filter顺序是有严格的规定的。Spring Security对那些内置的Filter都指定了一个别名,同时指定了它们的位置。咱们在定义custom-filter的position、before和after时使用的值就是对应着这些别名所处的位置。如position=”CAS_FILTER”就表示将定义的Filter放在CAS_FILTER对应的那个位置,before=”CAS_FILTER”就表示将定义的Filter放在CAS_FILTER以前,after=”CAS_FILTER”就表示将定义的Filter放在CAS_FILTER以后。此外还有两个特殊的位置能够指定,FIRST和LAST,分别对应第一个和最后一个Filter,如你想把定义好的Filter放在最后,则可使用after=”LAST”。

       接下来咱们来看一下Spring Security给咱们定义好的FilterChain中Filter对应的位置顺序、它们的别名以及将触发自动添加到FilterChain的元素或属性定义。下面的定义是按顺序的。

别名

Filter

对应元素或属性

CHANNEL_FILTER

ChannelProcessingFilter

http/intercept-url@requires-channel

SECURITY_CONTEXT_FILTER

SecurityContextPersistenceFilter

http

CONCURRENT_SESSION_FILTER

ConcurrentSessionFilter

http/session-management/concurrency-control

LOGOUT_FILTER

LogoutFilter

http/logout

X509_FILTER

X509AuthenticationFilter

http/x509

PRE_AUTH_FILTER

AstractPreAuthenticatedProcessingFilter 的子类

CAS_FILTER

CasAuthenticationFilter

FORM_LOGIN_FILTER

UsernamePasswordAuthenticationFilter

http/form-login

BASIC_AUTH_FILTER

BasicAuthenticationFilter

http/http-basic

SERVLET_API_SUPPORT_FILTER

SecurityContextHolderAwareRequestFilter

http@servlet-api-provision

JAAS_API_SUPPORT_FILTER

JaasApiIntegrationFilter

http@jaas-api-provision

REMEMBER_ME_FILTER

RememberMeAuthenticationFilter

http/remember-me

ANONYMOUS_FILTER

AnonymousAuthenticationFilter

http/anonymous

SESSION_MANAGEMENT_FILTER

SessionManagementFilter

http/session-management

EXCEPTION_TRANSLATION_FILTER

ExceptionTranslationFilter

http

FILTER_SECURITY_INTERCEPTOR

FilterSecurityInterceptor

http

SWITCH_USER_FILTER

SwitchUserFilter

下面详细介绍几个 Filter

FilterSecurityInterceptor

FilterSecurityInterceptor是用于保护Http资源的,它须要一个AccessDecisionManager和一个AuthenticationManager的引用。它会从SecurityContextHolder获取Authentication,而后经过SecurityMetadataSource能够得知当前请求是否在请求受保护的资源。对于请求那些受保护的资源,若是Authentication.isAuthenticated()返回false或者FilterSecurityInterceptor的alwaysReauthenticate属性为true,那么将会使用其引用的AuthenticationManager再认证一次,认证以后再使用认证后的Authentication替换SecurityContextHolder中拥有的那个。而后就是利用AccessDecisionManager进行权限的检查。

ExceptionTranslationFilter

经过前面的介绍咱们知道在Spring Security的Filter链表中ExceptionTranslationFilter就放在FilterSecurityInterceptor的前面。而ExceptionTranslationFilter是捕获来自FilterChain的异常,并对这些异常作处理。ExceptionTranslationFilter可以捕获来自FilterChain全部的异常,可是它只会处理两类异常,AuthenticationException和AccessDeniedException,其它的异常它会继续抛出。若是捕获到的是AuthenticationException,那么将会使用其对应的AuthenticationEntryPoint的commence()处理。若是捕获的异常是一个AccessDeniedException,那么将视当前访问的用户是否已经登陆认证作不一样的处理,若是未登陆,则会使用关联的AuthenticationEntryPoint的commence()方法进行处理,不然将使用关联的AccessDeniedHandler的handle()方法进行处理。

       AuthenticationEntryPoint是在用户没有登陆时用于引导用户进行登陆认证的,在实际应用中应根据具体的认证机制选择对应的AuthenticationEntryPoint。

       AccessDeniedHandler用于在用户已经登陆了,可是访问了其自身没有权限的资源时作出对应的处理。ExceptionTranslationFilter拥有的AccessDeniedHandler默认是AccessDeniedHandlerImpl,其会返回一个403错误码到客户端。咱们能够经过显示的配置AccessDeniedHandlerImpl,同时给其指定一个errorPage使其能够返回对应的错误页面。固然咱们也能够实现本身的AccessDeniedHandler。

   在捕获到AuthenticationException以后,调用AuthenticationEntryPoint的commence()方法引导用户登陆以前,ExceptionTranslationFilter还作了一件事,那就是使用RequestCache将当前HttpServletRequest的信息保存起来,以致于用户成功登陆后须要跳转到以前的页面时能够获取到这些信息,而后继续以前的请求,好比用户可能在未登陆的状况下发表评论,待用户提交评论的时候就会将包含评论信息的当前请求保存起来,同时引导用户进行登陆认证,待用户成功登陆后再利用原来的request包含的信息继续以前的请求,即继续提交评论,因此待用户登陆成功后咱们一般看到的是用户成功提交了评论以后的页面。Spring Security默认使用的RequestCache是HttpSessionRequestCache,其会将HttpServletRequest相关信息封装为一个SavedRequest保存在HttpSession中。

SecurityContextPersistenceFilter

 SecurityContextPersistenceFilter会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,而后把它设置给SecurityContextHolder。在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository,同时清除SecurityContextHolder所持有的SecurityContext。在使用NameSpace时,Spring  Security默认会给SecurityContextPersistenceFilter的SecurityContextRepository设置一个HttpSessionSecurityContextRepository,其会将SecurityContext保存在HttpSession中。此外HttpSessionSecurityContextRepository有一个很重要的属性allowSessionCreation,默认为true。这样须要把SecurityContext保存在session中时,若是不存在session,能够自动建立一个。也能够把它设置为false,这样在请求结束后若是没有可用的session就不会保存SecurityContext到session了。SecurityContextRepository还有一个空实现,NullSecurityContextRepository,若是在请求完成后不想保存SecurityContext也可使用它。

       这里再补充说明一点为何SecurityContextPersistenceFilter在请求完成后须要清除SecurityContextHolder的SecurityContext。SecurityContextHolder在设置和保存SecurityContext都是使用的静态方法,具体操做是由其所持有的SecurityContextHolderStrategy完成的。默认使用的是基于线程变量的实现,即SecurityContext是存放在ThreadLocal里面的,这样各个独立的请求都将拥有本身的SecurityContext。在请求完成后清除SecurityContextHolder中的SucurityContext就是清除ThreadLocal,Servlet容器通常都有本身的线程池,这能够避免Servlet容器下一次分发线程时线程中还包含SecurityContext变量,从而引发没必要要的错误

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,对应的参数名默认为j_username和j_password。若是不想使用默认的参数名,能够经过UsernamePasswordAuthenticationFilter的usernameParameter和passwordParameter进行指定。表单的提交路径默认是“j_spring_security_check”,也能够经过UsernamePasswordAuthenticationFilter的filterProcessesUrl进行指定。经过属性postOnly能够指定只容许登陆表单进行post请求,默认是true。其内部还有登陆成功或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler,这些均可以根据需求作相关改变。此外,它还须要一个AuthenticationManager的引用进行认证,这个是没有默认配置的。

参考:https://www.iteye.com/blogs/subjects/spring_security

           https://www.cnkirito.moe/categories/Spring-Security/