Spring Security小教程 Vol 3. 身份验证的入口-AbstractAuthenticationProcessingFilter

前言

结合上一期我们介绍的AuthenticationManager为入口的身份验证的核心模块,我们这次讨论的是为了使SpringSecurity对Spring Web项目提供支持,作为验证请求入口的\color{red}{AbstractAuthenticationProcessingFilter}

第三期 Authentication核心简介

本期的任务清单

  1. AbstractAuthenticationProcessingFilter的依赖组件;
  2. AbstractAuthenticationProcessingFilter依赖组件的主要职责和相关设计动机。

1. AbstractAuthenticationProcessingFilter处理Request及与AuthenticationManager交互的流程

AbstractAuthenticationProcessingFilter的主要职责和依赖组件

了解AbstractAuthenticationProcessingFilter大概是干嘛的的最简单的方法就是直接去读api-doc。

Abstract processor of browser-based HTTP-based authentication requests.

官方文档说的很明白了:处理基于浏览器交互的HTTP验证请求。所以AbstractAuthenticationProcessingFilter的职责也就非常明确——处理所有HTTP Request和Response对象,并将其封装成AuthenticationMananger可以处理的Authentication。并且在身份验证成功或失败之后将对应的行为转换为HTTP的Response。同时还要处理一些Web特有的资源比如Session和Cookie。总结成一句话,就是替AuthenticationMananger把所有和Authentication没关系的事情全部给包圆了。


雷锋AbstractAuthenticationProcessingFilter

继续读JavaDoc可以得知AbstractAuthenticationProcessingFilter为了完成组织上交代的与浏览器和HTTP请求的验证任务。它将大任务拆成了几个子任务并交给了以下组件完成:

  1. AuthenticationManager用于处理身份验证的核心逻辑;
  2. AuthenticationSuccessHandler用于处理验证成功的后续流程;
  3. AuthenticationFailureHandler用于处理失败的后续流程;
  4. 在验证成功后发布一个名为InteractiveAuthenticationSuccessEvent的事件通知给到应用上下文,用于告知身份验证已经成功;
  5. 因为是基于浏览器所以相关的会话管理行为交由 SessionAuthenticationStrategy来进行实现。
  6. 文档上还有一点没有写出来的是,如果用户开启了类似“记住我”之类的免密码登录,AbstractAuthenticationProcessingFilter还有一个名为RememberMeServices来进行管理。
AbstractAuthenticationProcessingFilter主要组件

AbstractAuthenticationProcessingFilter的验证流程

AbstractAuthenticationProcessingFilter本质上还是个Filter,其核心的业务入口方法就是doFilter方法:


官方API文档

这里我们先设置一个问题,然后带着问题去分析AbstractAuthenticationProcessingFilter的doFilter都是怎么设计解决这些问题的?

  1. 怎么判断当前的请求是需要被验证访问的?
  2. 如何进行身份验证?
  3. 如何进行会话验证?动机是什么?
  4. 成功和失败的后续流程都在干什么?
  5. Remember-Me功能实现流程是什么?
  6. 如何在其他服务中监听验证成功的事件?

问题1. 怎么判断当前的请求是需要被验证访问的?

在正式进行身份之前,doFilter会通过Security中的\color{red}{RequestMatcher}。尝试查找是否有匹配记录。
我们回顾下之前我们写过的访问控制的代码:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                // inde.html对应的url允许所任人访问
                .antMatchers("/").permitAll()
                // user.html对应的url,则需要用户有USER的角色才可以访问
                .antMatchers("/user").hasRole("USER")
                .and()
                .formLogin();
    }

其中的matcher的规则便会在这个流程中预先被检查,如果需要进行身份验证则会进行写一个阶段:对请求进行必要的身份验证。

问题2. 如何进行身份验证?

doFilter中通过调用自己的attemptAuthentication方法,但并不进行身份验证的逻辑处理,而是委托AuthenticationManager去完成相关的身份验证流程。AbstractAuthenticationProcessingFilter将HttpServletRequest包装成了Authentication对象与核心的AuthenticationManager进行交互。这样的设计可以使AuthenticationManager不感知外部的Web环境,从而使Security不仅可以支持Web应用,同时也可以被所有Java应用进行使用——只要客制化外部参与并将其封装成Authentication与AuthenticationManager的进行身份验证。


身份验证委托与AuthenticationManager进行实现

这里还需要注意的是在AuthenticationManager中实际完成身份验证任务并不是AuthenticationManager它自己身。而是将相关的任务针对每一种身份验证协议的AuthenticationProvider去完成相关的身份验证工作。


官方文档attemptAuthentication方法

问题3. 如何进行会话验证管理?动机是什么?

会话验证策略

第一个概念会话验证\color{red}{SessionAuthentication}是什么的一个概念?我们知道HTTP的请求实际上是无状态的,浏览器为了使HTTP之间能使用同一会话进行操作,在Java Web中通常会将Web容器(特指Tomcat)的JSESSIONID写入请求的cookie中一同发送。

cookie中的JSESSIONID

而服务端中通过中的JSESSIONID是通过request.getSession().getId()获取的,这样便使本来无状态的HTTP通过客户端的cookie中JSESSIONID与服务端的Session关联了起来。
但是从安全角度来说这样匹配机制存在许多问题,最简单的问题就是Session id在整个会话失效之间是不会变更的,这样就可以通过身份验证通过后获取了Session id从而通过其他客户端伪造cookie与服务端进行交互。有兴趣的同学可以针对这问题去了解下客户端cookie、服务端session以及一些CSRF攻击的介绍。本人比较推荐这篇http://hengyunabc.github.io/slides/cookie-and-session-and-csrf.html#1

针对不同的会话管理策略场景,Security也提供了相应的实现,有机会再单独开一篇单独来介绍相关的策略。这边就先了解下,在完成了AuthenticationManager的身份验证后,还会对其进行必要的会话验证。


提供的几种会话验证策略

问题4. 成功和失败的后续流程都在干什么?

验证成功与失败流程控制

验证成功之后AuthenticationManager会返回一个通过UserDetail构造并且附带上了所有授权信息的Authentication对象。
而验证失败的话则会抛出\color{red}{AuthenticationException},AbstractAuthenticationProcessingFilter捕获异常之后进行进行验证失败的处理。
成功的后续操作最主要的一个操作便是,通过SecurityContextHolder将本次验证之后的Authentication对象塞到当前的SecurityContext中。在后续的操作中如需要使用到Authentication身份信息,则可以直接通过SecurityContextHolder去获取。

     //成功后设置上下文二
    SecurityContextHolder.getContext().setAuthentication(authResult);

     //后续操作可以从上下文中获取身份信息
     SecurityContextHolder.getContext().getAuthentication();

然后再通过ApplicationEventPublisher发送验证成功的事件信息供其他相关监听器进行相关操作。

操作失败就简单了,既然成功是重新将最新的Authentication对象塞到SecurityContext上下文中,失败便是直接清空了上下文,让其Authentication对象变得“一无所有”。

而其他的对于request的额外操作则可以分别通过\color{red}{AuthenticationSuccessHandler}\color{red}{AuthenticationFailureHandler}两个接口去设置相关操作。

AuthenticationSuccessHandler

那么哪些工作属于验证成功后还需要额外操作的呢?举个最简单的例子,在用户想访问一个受限的资源,他首先被重定向掉了登录页面让其输入用户名和密码,而在其验证成功之后,那么他是讲指向到指定的某一个页面还是重定向到本次操作本来想访问的受限资源的路径呢?
这些相关的操作便是在AuthenticationSuccessHandler中进行完成的。
同样的如果登录失败需要做一些除了身份验证以外,有需要感知HTTP请求、响应对象的操作,同样的也可以在AuthenticationFailureHandler中进行完成。

问题5. Remember-Me功能实现流程是什么?

浏览器特色服务RememberMe快速登录

Remember-Me是指网站能够在Session之间记住登录用户的身份,具体来说就是我成功认证一次之后在一定的时间内我可以不用再输入用户名和密码进行登录了,系统会自动给我登录。这通常是通过服务端发送一个cookie给客户端浏览器,下次浏览器再访问服务端时服务端能够自动检测客户端的cookie,根据cookie值触发自动登录操作。
实现方式有很多种,一般来说最简单的实现就是将用户名与一些其他字符组合进行编码,然后服务端解码之后提取出相关其中的用户名,通过UserDetailsService获取相关用户信息的用户验证方式。
Spring Security中提供了两种Remember-Me机制进行使用,如果有其他实现方式也可以通过继承AbstractRememberMeServices类进行扩展。请一定牢记Remember-Me机制的现实是依赖浏览器Cookie的,在默认情况下SpringSecurity会将编码后的字符串存于Cookie中的remember-me键位。


RememberMeServices

问题6. 如何在其他服务中监听验证成功的事件

在完整整个验证流程之后,AbstractAuthenticationProcessingFilter还会通过Spring容器的\color{red}{ApplicationEventPublisher}事件发布器发射一个\color{red}{InteractiveAuthenticationSuccessEvent}。如果需要在应用其他监听器上处理相关验证成功后操作。我们可以通过Spring中的@EventListener监听InteractiveAuthenticationSuccessEvent事件便可以实现。
写一个示例,如果我们想在每个用户登录成功后,在控制台打印出登录用户的用户名。

@Component
public class AuthenticationListener {
    @EventListener
    public void register(InteractiveAuthenticationSuccessEvent event)
    {
        //获取登录成功的Authentication对象
        Authentication authentication = event.getAuthentication();
        //打印用户名
        System.out.println("@EventListener注册信息,用户名:"+authentication.getName());
    }
}

结尾

本期花了很大的篇幅介绍了整个Web验证流程的核心组件AbstractAuthenticationProcessingFilter。下一期我们将结合他最常用的实现类UsernamePasswordAuthenticationFilter做一个讲解,希望通过讲解UsernamePasswordAuthenticationFilter实现使大家了解客制化一个验证协议需要注意的细节。
我们下期再见。

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

推荐阅读更多精彩内容