SpringSecurity你想知道的都在这里了

写在前面:

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

认证:就是对资源调用者的身份进行核实,比如你想访问某个接口或者某个方法,可能需要你登陆,输入你的账号密码,然后后台验证这个账号是否具有访问这个资源的权力。

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

宏观流程:

  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;三个参数分别如下:

  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,然后我们就可以顺利的访问资源了

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

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

资源访问后

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

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

角色继承

  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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值