Spring Security 基本套路

原理:

在 Spring Web 项目中,基于过滤器处理认证与授权,在过滤器中调用认证管理器 AuthenticationManager 与 授权管理器 accessDecisionManager 进行认证及授权

自动配置:

Spring boot 通过 SecurityAutoConfiguration 给项目提供了默认的安全配置,一个用户名为 'user' 的用户,随机密码并再启动时打印到控制台,所有请求都需要通过认证。

自定义配置:

如果要移除默认的行为,自定义安全策略,则可以提供一个 继承 WebSecurityConfigurerAdapter 并注解 @Configuration 与 @EnableWebSecurity 

@EnableWebSecurity

作用是创建下图中的 FilterChainProxy ,并提供一些基本的安全防护,如 CSRF attack prevention 、Session Fixation protection 等,具体查看官方文档


WebSecurityConfigurerAdapter.class 

该类中提供一个默认配置方法 protected void configure(HttpSecurity http) throws Exception 自定义策略则需重写该方法。

HttpSecurity

该类是 SecurityBuilder 接口的实现类,是一个通过 http request 方式配置 Web 安全的构造器,与使用 xml <http> 配置的方式相似

调用 HttpSecurity 的方法实际上就是在进行配置,如:authorizeRequests() formLogin() 分别会创建 ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer ,它们是 SecurityConfigurer 接口的实现类,意思差不多就是不同的安全配置器。每一个配置器 (SecurityConfigurer) 又会对应创建一个或多个过滤器 (可查看 api 或 源码有说明),上面的配置器对应创建了 FilterSecurityInterceptor、UsernamePasswordAuthenticationFilter。

FilterSecurityInterceptor (在授权流程中有详细介绍)

这个过滤器比较重要,负责 Http 资源的安全性 (responsible for handling the security of HTTP resources),需要依赖 AuthenticationManager、AccessDecisionManager、FilterInvocationSecurityMetadataSource。

重点解析一下认证的基本流程:

使用 InMemoryUser 的方式

入口:UsernamePasswordAuthenticationFilter
该类继承自 AbstractAuthenticationProcessingFilter ,在父类中将注入 authenticationManager、拦截请求、处理认证成功与失败后的逻辑
UsernamePasswordAuthenticationFilter 定义了请求 Url ,并实现了父类的抽象方法 attemptAuthentication ,拦截到请求后,调用 attemptAuthentication 方法尝试认证。

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password)
通过获取的请求参数,构造一个 Authentication,且被标记为未认证 ( setAuthenticated(false) )
不同的认证方式会构造不同的 token , 这些 token 的实现类都继承自 AbstractAuthenticationToken
而 AbstractAuthenticationToken 则实现了 Authentication ,即所有的 token 也是 Authentication 的实例 。

AbstractAuthenticationToken 的其他实现:


setDetails(request, authRequest)
该方法为 UsernamePasswordAuthenticationToken 的父类 AbstractAuthenticationToken 填充了 details 属性 (主要就是 remoteAddress 与 sessionId)
通过调用 WebAuthenticationDetailsSource 的 buildDetails 方法来完成,该类是 AuthenticationDetailsSource 接口的其中一个实现类 。

this.getAuthenticationManager().authenticate(authRequest)
将认证的具体工作交给 AuthenticationManager,该接口只有一个 authenticate 方法

ProviderManager

该类是 AuthenticationManager 的实现,这里实际上是调用 ProviderManager 的 authenticate 方法
该方法循环在系统中定义或配置的 AuthenticationProvider, 通过 supports 方法判断 UsernamePasswordAuthenticationFilter 构造的 Authentication 是哪个 provider 所支持的 。

AuthenticationProvider

该接口只有两个方法, authenticate 用于认证, supports  用于判断是否支持验证该 Authentication

AuthenticationProvider 有如下实现类:


在这里最终匹配到 DaoAuthenticationProvider,该类的父类 AbstractUserDetailsAuthenticationProvider 的 supports 方法接受 UsernamePasswordAuthenticationToken.class


匹配后最终将调用 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法进行验证
该方法首先尝试从 cache 中获取用户信息 UserDetails ( UserDetails user = this.userCache.getUserFromCache(username) )
如果没有则通过 UserDetailsService 接口的 loadUserByUsername 获取 UserDetails ,UserDetails 只是一个接口,定义了一些对用户信息的操作方法,User 类实现了 UserDetails 。

DaoAuthenticationProvider

该类实现了父类 AbstractUserDetailsAuthenticationProvider 的抽象方法 retrieveUser ,该方法调用 UserDetailsService 接口的 loadUserByUsername 方法获取用户信息 ( this.getUserDetailsService().loadUserByUsername(username) )
在这里 loadUserByUsername 方法由 InMemoryUserDetailsManager 实现,该类实现了 UserDetailsManager 接口,而 UserDetailsManager 接口则继承自 UserDetailsService
AbstractUserDetailsAuthenticationProvider 还要一个抽象方法 additionalAuthenticationChecks
文档中指出:
打多数情况下,retrieveUser 方法并不履行密码的检验,而是在 additionalAuthenticationChecks 方法进行,如果需要比较 UserDetails 与 UsernamePasswordAuthenticationToken 中的其他的属性,也将在该方法中进行

上面的认证逻辑走完后:

UsernamePasswordAuthenticationFilter 返回一个标记为认证成功的 Authenticate (UsernamePasswordAuthenticationToken)

如果认证成功:

则调用 AbstractAuthenticationProcessingFilter 的 successfulAuthentication 方法,将认证后的 Authentication 存储到 SecurityContextHolder 中,处理 RememberMe 逻辑,然后再调用SimpleUrlAuthenticationSuccessHandler 的 onAuthenticationSuccess 方法跳转视图
Srping 默认使用 ThreadLocalSecurityContextHolderStrategy 策略通过 ThreadLocal 来存储 SecurityContextHolder,这意味着在该线程中(一个请求)任意地方都可以访问 SecurityContextHolder

如果认证失败:

则调用 unsuccessfulAuthentication 方法, 该方法首先清除 SecurityContextHolder ,然后调用 AuthenticationFailureHandler 接口的实现类 SimpleUrlAuthenticationFailureHandler 的 onAuthenticationFailure 方法,在该方法主要完成两个操作,保存异常信息与跳转。

保存异常信息 saveException

通过 forwardToDestination 属性判断保存方式,如果 true , 则保存在 request 中

request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);

如果 false 则保存在 session 中

request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);

AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION"

跳转同样通过 forwardToDestination 判断是用 forward 还是 sendRedirect

授权的基本流程:

入口过滤器是 FilterSecurityInterceptor,该类继承自 AbstractFilterSecurityInterceptor ,核心逻辑在 beforeInvocation 方法中,主要负责三个工作

获取访问该资源(请求url)所需的角色列表

系统将定义(configure中硬编码或xml配置)的资源与角色关系存储在一个 requestMap 中

Map<RequestMatcher, Collection<ConfigAttribute>> requestMap

该 Map 的 key 即 RequestMatcher 则是资源 url ,value 即 Collection<ConfigAttribute> 则是角色的集合 (一个资源对应多个角色)

调用 DefaultFilterInvocationSecurityMetadataSource.getAttributes() 方法用请求的 url 去匹配 Map 中的 key ,如果匹配上则返回角色列表

获取已认证用户 Authentication

从 SecurityContextHolder 中获取 Authentication 

判断用户角色与资源所需角色是否相等

调用 accessDecisionManager 实现类的 decide 方法,decide 方法内调用投票器 (Voter) 进行判断

在投票器中获取认证用户的角色 getAuthorities()  ,并与第一步中取得的资源所需的角色列表进行对比后进行授权

实现细粒度的权限控制:

思路1:

new 一个 FilterSecurityInterceptor , 并注入自定义的 FilterInvocationSecurityMetadataSource (从数据库获取权限集合) 、AccessDecisionManager (授权决策、判断),用新建的 filter 替换原 filter , http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)

思路2:

使用 ObjectPostProcessor 配置 FilterSecurityInterceptor 的相关属性,使用自定义的 FilterInvocationSecurityMetadataSource (从数据库获取权限集合) 与 AccessDecisionManager (判断是否授权)

心得:

自定义一个拦截器来处理授权感觉更方便,只不过脱离了 Security 的授权架构了。

还有原因需要考虑:

Security 通过 filter 在启动时加载权限集合,如果有变动岂不是要重启项目;

处理逻辑和目前项目有区别,Security 首先要为资源配置角色,再取出访问资源授予的全部角色与当前用户拥有的角色进行比较,而目前项目一般是对角色赋予可访问的资源,并未对每个资源提前配置角色集合;

这两点不符合目前的权限控制方式,Security 的方式不够灵活

一些额外的:

ThreadLocal 是什么:

就是 java.lang.ThreadLocal ,线程本地变量,简单的理解,对于当前线程来说,它是一个全局变量,在当前线程的任意地方都可以访问该变量中存储的值;对于多线程来说,它又是独立的,每个线程有自己的 ThreadLocal 变量,其他线程不能访问。

SecurityContextHolder 的一些解析:

在 Web Application 中,使用 ThreadLoacl 存储 SecurityContextHolder 策略提供安全的上下文,Spring security 通过 SecurityContextPersistenceFilter 这个过滤器将上下文存储在 HttpSession 中,用于恢复上下文到 SecurityContextHolder 中,因为一个请求完成后,Spring security 将清除 SecurityContextHolder, 为了安全起见,Spring 不建议直接与 HttpSession 交互,都是通过 SecurityContextHolder 进行替代。

官方文档对于这几个类的总结:

SecurityContextHolder, to provide access to the SecurityContext.
SecurityContext, to hold the Authentication and possibly request-specific security information.
Authentication, to represent the principal in a Spring Security-specific manner.
GrantedAuthority, to reflect the application-wide permissions granted to a principal.
UserDetails, to provide the necessary information to build an Authentication object from your application’s DAOs or other source of security data.
UserDetailsService, to create a UserDetails when passed in a String-based username (or certificate ID or the like).

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值