Spring Security原理解析(一)

前言

看了网上各种关于Spring Security原理解析的文章,大部分都是一上来就贴源码的,我个人觉得一来就贴源码是非常不好的行为,一篇好的教程,讲了什么和怎么讲是一样重要的,相信很多人都有这种经历,想简单明了的了解下这个框架的原理、作用、使用方法而搜索相关文章,结果全是大段大段的源码,我相信没几个人愿意看下去,本文主旨在于用不贴代码的方式给不了解Spring Security的读者快速理解Spring Security的工作流程,本篇并不讲Spring Security怎么使用,相信读者在了解了基本流程以后再学习怎么使用会有事半功倍的效果。本文的参考资料主要来源于Spring Security的官方文档。

必要知识

相信读到这个文章的人已经有了Spring和Servlet的相关知识,不过由于很多程序员已经面向Spring开发了太久,或者一开始就是用Spring来开发,可能已经忘记或者不了解Servlet的相关知识,作为接下来知识点的必要基础,在这复习一下Servlet的相关知识。对Servlet很了解的话可以跳过。

Servlet

简单来说Servlet就是一组接口,我们编写的类实现这组接口,当Tomcat、Jboss等实现了Servlet规范的服务器程序在运行的时候,会根据URL来加载相应的我们编写的类。早期没有Spring等框架的时候,开发Java服务器程序全是靠手写一个个实现了Servlet的类,然后在WEB-INF文件夹下的web.xml文件里将这些将这些Servlet类和对应的URL进行映射,然后将这些Servlet类和web.xml一起打成一个war包放到Tomcat等容器的webapp目录下,这样容器在运行的时候监听到有请求发过来后,就能根据事先写好的映射关系,通过反射的方式加载我们写的Servlet类然后执行里面的业务逻辑。

Filter

Filter和Servlet一样也是一组接口,实现这组接口的类也像Servlet一样,在web.xml中配置好和URL的映射关系后被打包进war包,容器会根据URL来加载对应的Filter,Filter会在Servlet之前执行,于是我们可以在这里写一些过滤和校验的代码,在请求进入Servlet之前对请求进行校验和处理。Filter的处理流程如下图所示,多个Filter构成了一条过滤链,只有通过了所有Filter请求才能进入Servlet,而Spring Security正是利用了Filter来完成各种校验功能。

正文

Spring Security的基本原理

DelegatingFilterProxy

从必要知识里我们知道了Filter的工作原理,在Spring中使用自定义的Filter有个问题那就是Filter必须在Servlet容器启动前就注册好,但是Spring使用ContextLoaderListener来加载Spring Bean,这就像先有鸡还是先有蛋的问题,于是Spring设计了一个Filter的代理DelegatingFilterProxy,在Servlet容器中注册这个代理,将过滤的工作拦截下来转发给Spring容器中的过滤器。

FilterChainProxy和SecurityFilterChain

在实际代码中Spring并没有在DelegatingFilterProxy中直接实例化和调用自定义的Filter,而是构建了一个过滤链FilterChainProxy,FilterChainProxy内部又调用了SecurityFilterChain,我们自定义的Filter会被加到SecurityFilterChain上,这一点和Tomcat的设计很像,这样做的好处是确定了一个起点,当你要排除Spring Security的bug时,在FilterChainProxy中添加调试点是一个很好的起点。

这样设计还有个好处就是在Servlet容器中,仅根据URL调用过滤器。 但是,FilterChainProxy可以利用RequestMatcher接口,根据HttpServletRequest中的任何内容确定调用,比原生的Servlet更灵活。
还有一点是FilterChainProxy可以构建多条SecurityFilterChain, 你的应用程序可以为不同的情况提供完全独立的配置,如下图所示。

不过在使用SecurityFilterChain中要注意,FilterChainProxy使用第一个匹配上的SecurityFilterChain。例如如果请求的URL是/ api / messages /,则它将首先与SecurityFilterChain0的/ api / 模式匹配,因此即使SecurityFilterChainn也能匹配上,也会仅调用SecurityFilterChain0。 如果请求的URL是/ messages /,则在SecurityFilterChain0的/ api / 模式下将不匹配。

Security Filters

Security Filters即是我们用于定义各种过滤规则的过滤器,通过SecurityFilterChain的API插入到FilterChainProxy中。过滤器的顺序很重要。各过滤器按先后顺序依次执行,以下是各过滤器的作用顺序:

  • ChannelProcessingFilter

  • ConcurrentSessionFilter

  • WebAsyncManagerIntegrationFilter

  • SecurityContextPersistenceFilter

  • HeaderWriterFilter

  • CorsFilter

  • CsrfFilter

  • LogoutFilter

  • OAuth2AuthorizationRequestRedirectFilter

  • Saml2WebSsoAuthenticationRequestFilter

  • X509AuthenticationFilter

  • AbstractPreAuthenticatedProcessingFilter

  • CasAuthenticationFilter

  • OAuth2LoginAuthenticationFilter

  • Saml2WebSsoAuthenticationFilter

  • UsernamePasswordAuthenticationFilter

  • ConcurrentSessionFilter

  • OpenIDAuthenticationFilter

  • DefaultLoginPageGeneratingFilter

  • DefaultLogoutPageGeneratingFilter

  • DigestAuthenticationFilter

  • BearerTokenAuthenticationFilter

  • BasicAuthenticationFilter

  • RequestCacheAwareFilter

  • SecurityContextHolderAwareRequestFilter

  • JaasApiIntegrationFilter

  • RememberMeAuthenticationFilter

  • AnonymousAuthenticationFilter

  • OAuth2AuthorizationCodeGrantFilter

  • SessionManagementFilter

  • ExceptionTranslationFilter

  • FilterSecurityInterceptor

  • SwitchUserFilter

处理异常

Spring在FilterChainProxy中自动插入了一个名为ExceptionTranslationFilter的Filter,这个Filter用来处理登录和认证异常,将AccessDeniedException和AuthenticationException转换为HTTP响应。

具体的处理流程为:
1.ExceptionTranslationFilter调用FilterChain.doFilter(request,response)来调用应用程序的其余部分。
2.如果用户未通过身份验证或它是AuthenticationException,则启动身份验证。此时会清除SecurityContextHolder,HttpServletRequest保存在RequestCache中。等用户成功进行身份验证后,将使用RequestCache重播原始请求。
3.如果它是AccessDeniedException,则拒绝访问。 调用AccessDeniedHandler来处理被拒绝的访问。

如果应用程序未引发AccessDeniedException或AuthenticationException,则ExceptionTranslationFilter不执行任何操作。

ExceptionTranslationFilter的伪代码如下所示:

try {
    filterChain.doFilter(request, response); 
} catch (AccessDeniedException | AuthenticationException e) { 
    if (!authenticated || e instanceof AuthenticationException) { 
    startAuthentication(); 
    } else { 
    accessDenied(); 
    } 
}

认证方式

Spring Security是一个非常复杂且庞大的框架,涵盖了几乎所有的认证方式,包括一些已经不推荐使用的方式,想要在一篇文章里囊括所有的认证方式不太可能,即是可能,读者也很难坚持读下去,所以接下来只挑选了最常用的基于用户名和密码的认证方式来说明Spring Security的认证流程。

Spring Security提供了以下内置机制,用于从HttpServletRequest中读取用户名和密码:

  1. Form Login 使用表单登录方式
  2. Basic Authentication 使用Http Basic方式
  3. Digest Authentication 使用摘要身份验证方式
    需要注意的是不应该在现代应用程序中使用摘要式身份验证,因为它必须以纯文本,加密或MD5格式存储密码,所有这些存储格式都被认为是不安全的,所以在这里并不写怎么通过摘要验证。

表单认证

首先我们来看如何通过表单验证,在此之前需要了解如何将用户重定向到登录表单的过程:

1.首先,用户向未经授权的资源/ private发出未经身份验证的请求。

2.Spring Security的FilterSecurityInterceptor表示拒绝访问,通过抛出AccessDeniedException拒绝了未经身份验证的请求。

3.由于未对用户进行身份验证,因此ExceptionTranslationFilter会启动“开始身份验证”,并使用配置的AuthenticationEntryPoint将重定向发送到登录页面。 在大多数情况下,AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint的实例。

4.浏览器将请求将其重定向到的登录页面。

5.登录页面。

提交用户名和密码后,UsernamePasswordAuthenticationFilter会对用户名和密码进行身份验证,UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,因此此图看起来应该非常相似。

1.当用户提交其用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken,这是一种身份验证类型。

2.接下来将UsernamePasswordAuthenticationToken传递到AuthenticationManager进行身份验证。 AuthenticationManager的详细信息取决于用户信息的存储方式。

3.如果身份验证失败,则认证失败,清除SecurityContextHolder,

RememberMeServices.loginFail被调用。如果RememberMe未配置,则为空,AuthenticationFailureHandler被调用。

4.如果身份验证成功,则认证成功,SessionAuthenticationStrategy被通知有新的登录,在SecurityContextHolder上设置该认证信息,RememberMeServices.loginFail被调用。如果RememberMe未配置,则为空,ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent,AuthenticationSuccessHandler被调用。通常这是一个SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它将重定向到ExceptionTranslationFilter保存的请求。

默认情况下,Spring Security表单登录处于启用状态。 但是,一旦提供了任何基于servlet的配置,就必须显式提供基于表单的登录。

Http Basic认证

让我们看一下HTTP基本身份验证在Spring Security中如何工作。首先,我们看到WWW-Authenticate标头被发送回未经身份验证的客户端。

1.首先,用户向未经授权的资源/ private发出未经身份验证的请求。

2.Spring Security的FilterSecurityInterceptor表示认证失败,通过抛出AccessDeniedException拒绝了未经身份验证的请求。

3.由于用户未通过身份验证,因此ExceptionTranslationFilter会启动“开始身份验证”。配置的AuthenticationEntryPoint是BasicAuthenticationEntryPoint的实例,该实例发送WWW-Authenticate标头。RequestCache通常是一个NullRequestCache,它不保存请求,因为客户端能够重播它最初请求的请求。

当客户端收到WWW-Authenticate标头时,它知道应该使用用户名和密码重试。 以下是正在处理的用户名和密码的流程。

1当用户提交其用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken,这是一种身份验证类型。

2接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager进行身份验证。 AuthenticationManager的详细信息取决于用户信息的存储方式。

3如果身份验证失败,则认证失败,清除SecurityContextHolder,RememberMeServices.loginFail被调用。如果RememberMe未配置,则为空,调用AuthenticationEntryPoint触发WWW-Authenticate重新发送。
4如果身份验证成功,则认证成功,在SecurityContextHolder上设置认证身份信息,RememberMeServices.loginSuccess被调用。如果RememberMe未配置,则为空,BasicAuthenticationFilter调用FilterChain.doFilter(request,response)继续进行其余的应用程序逻辑。

Spring Security的HTTP基本身份验证支持默认为启用。 但是,一旦提供了任何基于servlet的配置,就必须显式提供HTTP Basic。

储存机制

读取到的用户身份信息可以使用以下几种方式进行储存:

  • Simple Storage with In-Memory Authentication

  • Relational Databases with JDBC Authentication

  • Custom data stores with UserDetailsService

  • LDAP storage with LDAP Authentication

In-Memory Authentication

Spring Security的InMemoryUserDetailsManager实现了UserDetailsService,以支持对在内存中检索到的基于用户名/密码的身份验证。 InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。 当配置为接受用户名/密码进行身份验证时,Spring Security使用基于UserDetails的身份验证。

JDBC Authentication

Spring Security的JdbcDaoImpl实现了UserDetailsService,以提供对使用JDBC检索的基于用户名/密码的身份验证的支持。 JdbcUserDetailsManager扩展了JdbcDaoImpl以通过UserDetailsManager接口提供对UserDetails的管理。 当配置为接受用户名/密码进行身份验证时,Spring Security使用基于UserDetails的身份验证。

UserDetails

UserDetails由UserDetailsService返回。 DaoAuthenticationProvider验证UserDetails,然后返回身份验证,该身份验证的主体是已配置的UserDetailsService返回的UserDetails。

UserDetailsService

DaoAuthenticationProvider使用UserDetailsService检索用户名,密码和其他用于使用用户名和密码进行身份验证的属性。Spring Security提供UserDetailsService的内存中和JDBC实现。

密码编码

Spring Security的servlet支持通过与PasswordEncoder集成来安全地存储密码。可以通过公开一个PasswordEncoder Bean来定制Spring Security使用的PasswordEncoder实现。
(一口气写了这么多,需要休息一阵了,未完待续)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容