SpringSecurity从入门到源码分析

SpringSecurity从入门到源码分析

  • 1.Spring Security的概述
    • 1.1 相关术语
    • 1.2 常用术语
    • 2.2 常用单词
    • 2.4 环境搭建
  • 2.Spring Security基本使用
    • 2.1 使用默认账户登录
    • 2.2在配置文件中配置登录密码
    • 2.3 使用WebSecurityConfigurerAdapter配置密码
    • 2.4 开放内嵌框架
    • 2.5 自定义表单登录
  • 3. Spring Security的高级使用
    • 3.1 深入跨站请求伪造
      • 3.1.1 SRF的概念
      • 3.1.2 CSRF的原理
      • 3.1.3 判断出现跨域的标准
      • 3.1.4 CSRF的防御
      • 3.1.5 form表单如何添加token
      • 3.1.6 ajax请求如何添加token
      • 3.1.7 如何关闭 CSRF 防御机制
    • 3.2 完成网站自动登录
    • 3.3 保存凭证到数据库
    • 3.4 使用UserDetailsService动态验证
    • 3.5 用户密码加密
    • 3.6 自定义异常处理handler
    • 3.7 会话管理
      • 3.7.1 理解会话
      • 3.7.2 防御会话固定攻击
      • 3.7.3 会话过期处理
      • 3.7.4 保证单一登录
    • 3.8 AuthenticationException异常
    • 3.9 使用json方式处理登录结果
    • 3.10 Spring Security的滤器
    • 3.11 自定义验证码过滤器
    • 3.12自定义认证流程
  • 4.spring security 过滤器加载流程分析
    • 4.1 JavaWeb三大组件
    • 4.2 springboot程序是如何加载过滤器的?
    • 4.3 spring security过滤器链的初始化
    • 4.4 spring security过滤器执行流程
  • 5.用户名和密码登录认证流程分析
  • 6.手机号登录认证分析
  • 7.权限校验流程分析
    • 7.1 ExceptionTranslationFilter
    • 7.2 TokenAuthenticationFilter
    • 7.3 FilterSecurityInterceptor
    • 7.4 AbstractSecurityInterceptor
    • 7.5 WebExpressionVoter

1.Spring Security的概述

1.1 相关术语

Spring Security 是 Spring 家族中的一个安全管理框架,Spring Security 的两大核心功能就是认证(authentication)和授权(authorization)。

1.2 常用术语

  • 认证 :对身份进行校验。
  • 授权 :对你的身份进行授予权限。
  • 用户 :主要包括用户名称、用户密码和当前用户所拥有的角色信息,可用于实现认证操作。
  • 角色 :主要包括角色名称、角色描述和当前角色所拥有的权限信息,可用于实现授权操作。

2.2 常用单词

认证 :authentication

授权 :authorization

用户 :user

角色 :role

登录 :login

注销 :logout

2.4 环境搭建

创建一个普通的SpringBoot web程序即可,然后添加如下依赖:SpringBoot版本(2.6.2)

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
            <optional>trueoptional>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <optional>trueoptional>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.21version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.5.0version>
        dependency>

        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>5.8.9version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>

2.Spring Security基本使用

启动项目 访问你的地址 http://127.0.0.1:port 就会跳转到一个登录页面,需要进行登录 默认账户是user,密码会在控制台进行输出。

2.1 使用默认账户登录

在控制台中会打印如下信息:用户名就是:user

Using generated security password: 3017f88b-1700-415a-bb84-ab0dbad5af3b

SpringSecurity从入门到源码分析_第1张图片

2.2在配置文件中配置登录密码

spring:
  security:
    user:
      name: admin
      password: admin
      roles: admin

2.3 使用WebSecurityConfigurerAdapter配置密码

@EnableWebSecurity
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter  {
    // 1.直接设置用户,密码,权限 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password("admin").roles("admin");
    }
    
    //  2.在容器中注入一个bean,然后添加用户,InMemoryUserDetailsManager可以添加多个用户,这种方式是基于内存的
    //  @EnableWebSecurity中添加了@Configuration注解,所以这个类可以看成是一个配置类来使用
    @Bean
    public UserDetailsService  userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("admin").password("admin").roles("admin").build());
        return manager;
    }
    
    // 3.在容器中写一个bean,实现 UserDetailsService 接口,这样就可以根据数据库来实现,达到用户权限动态化
}

2.4 开放内嵌框架

项目中如果用到iframe嵌入网页,然后用到Spring Security,请求就会被拦截,如果你打开F12开发者控制台,你可能就会发现这样一句报错:Refused to display ‘http://localhost:8080/user/add’ in a frame because it set ‘X-Frame-Options’ to ‘deny’.

解决方案1:

@Override
protected void configure(HttpSecurity http) throws Exception {
    //关闭X-Frame-Options响应头
    http.headers().frameOptions().disable();
}

解决方案2:

@Override
protected void configure(HttpSecurity http) throws Exception {
    //设置X-Frame-Options响应头为SAMEORIGIN
    http.headers().frameOptions().sameOrigin();
}

2.5 自定义表单登录

1.准备两个html页面

SpringSecurity从入门到源码分析_第2张图片

登录成功的页面:

DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<h1>welcomeToTheHomePageh1>
<form th:action="@{/logout}" method="post">
    <input  type="submit" value="退出登录">
form>
body>
html>

登录页面:

DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="zh">

	<head>
		<meta charset="utf-8">
		<title>title>
	head>
	<style type="text/css">
		#wrapper{
			width: 1000px;
		}
		#login_form{
			position: relative;
			left: 50%;
			bottom: 25%;
			width: 400px;
			height: 280px;
			border-radius: 5px;
			border: 1px solid #dadada;
			background-color: #55ff00;
		}
		#create {
			width: 380px;
			margin-left: 10px;
			height: 40px;
			color: #ffffff;
			border-radius: 5px;
			background-color: #009FEF;
			border: none;
			cursor: pointer;
			position: absolute;
			bottom: 10px;
		}
		input{
			width: 380px;
			height: 40px;
			outline: none;
			border: 1px solid #dadada;
			border-radius: 5px;
			margin-left: 5px;
			margin-top: 20px;
			padding: 5px;
		}
	style>
	<body>
		<div id="wrapper" >
		 <div id="login_form" >
		   <form th:action="@{/login}" method="post">
		     <input type="text" name="username"  placeholder="用户名" id="username"/>
			 <br>
		     <input type="password" name="password" placeholder="密码" id="password" />
			 <br>
		     <button id="create">登录button>
		   form>
		 div>
		div>
	body>
html>

  1. LoginFroward: 控制页面跳转
@Controller
@RequestMapping("/security")
public class LoginFroward {

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "home/login";
    }

    @RequestMapping("/success")
    public String success(){
        return "home/index";
    }


}

3.配置WebSecurityConfig

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;

/**
 * @author huyu
 * @date 2023-03-11
 * @since 1.0
 **/
@EnableWebSecurity
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter  {
    @Override
    protected void configure(HttpSecurity http) throws Exception {


        //设置X-Frame-Options响应头为SAMEORIGIN
        http.headers().frameOptions().sameOrigin();
        //放行不用权限的资源(去登录页面当然不需要用权限,否则你都看不到登录界面,还怎么登录,所以去登录界面必须放行)
        http.authorizeRequests().antMatchers("/security/toLogin","/login").permitAll();
        //拦截需要权限的资源(拦截所有请求,要想访问,登录的账号必须拥有admin角色才行)
        http.authorizeRequests().antMatchers("/**").hasAnyRole("admin").anyRequest().authenticated();
        //设置自定义登录界面
        http.formLogin()//启用表单登录
                .loginPage("/security/toLogin")//登录页面地址,只要你还没登录,默认就会来到这里
                .loginProcessingUrl("/login")//登录处理程序,Spring Security内置控制器方法
                .usernameParameter("username")//登录表单form中用户名输入框input的name名,不修改的话默认是username
                .passwordParameter("password")//登录表单form中密码框输入框input的name名,不修改的话默认是password
                .defaultSuccessUrl("/security/success")//登录认证成功后默认转跳的路径
                //.successForwardUrl("/main")//登录成功跳转地址,使用的是请求转发
                .failureForwardUrl("/security/toLogin")//登录失败跳转地址,使用的是请求转发
                .permitAll().and()
                .cors().disable();

        //设置自定义登出界面
        http.logout()//启用退出登录
                .logoutUrl("/logout")//退出处理程序,Spring Security内置控制器方法
                .logoutSuccessUrl("/security/toLogin")//退出成功跳转地址
                .invalidateHttpSession(true)//清除当前会话
                .deleteCookies("JSESSIONID")//删除当前Cookie
                .permitAll();
    }




    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password("admin").roles("admin");

    }

    // 需要放行的静态资源
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**");
        web.ignoring().antMatchers("/img/**");
        web.ignoring().antMatchers("/js/**");
        web.ignoring().antMatchers("/favicon.ico");
    }

}

表单登录会由UsernamePasswordAuthenticationFilter.attemptAuthentication()进行身份验证和权限验证。

我们先在这里添加一个默认的密码处理器,这里我们先不加密,因为我在表单登录的时候,没有配置这个报错,所以先加上,估计是版本有点高的原因。其实这个类最主要的用处就是,使用某种加密方式,将用的密码进行加密后再和系统中的密码进行比对,因为一般用户的密码都比较敏感,不能直接存入到数据库中,如果数据库出现安全事故,那么用户的密码会被全部泄漏。

@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

    public DefaultPasswordEncoder() {
        this(-1);
    }

    /**
     * @param strength the log rounds to use, between 4 and 31
     *
     */
    public DefaultPasswordEncoder(int strength) {

    }

    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }
    // 不做任何编码处理
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(rawPassword.toString());
    }
}

3. Spring Security的高级使用

3.1 深入跨站请求伪造

3.1.1 SRF的概念

CSRF跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性,你可以这样来理解:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

3.1.2 CSRF的原理

假设:其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,用户C为Web A网站的合法用户。

  • 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
  • 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
  • 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
  • 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
  • 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

3.1.3 判断出现跨域的标准

当一个URL的 请求协议,域名,端口号 任意一个不同都会产生跨域

当前页面 被请求的页面 是否跨域 跨域原因
http://www.compass.com http://www.compass.com/index.html 端口号,域名,协议都相同
https://www.compass.com http://www.compass.com/get/1 协议不同
https://www.compass.com:80 https://www.compass.com:81/get/1 端口号不同
https://www.compass.cq:80 https://www.compass.com/get/1 域名不同
https://www.compass.blog.com https://www.kafuka.blog.com/get/1 子域不同

3.1.4 CSRF的防御

目前防御 CSRF 攻击主要有三种策略:验证 HTTP Referer 字段;在请求地址中添加 token 并验证(Spring Security采用);在 HTTP 头中自定义属性并验证。

(1)验证 HTTP Referer 字段

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站。因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。

这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。

然而,这种方法并非万无一失。Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。

即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 CSRF 攻击,拒绝合法用户的访问。

(2)在请求地址中添加 token 并验证

CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 ,这样就把 token 以参数的形式加入请求了。但是,在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉,通常使用的方法就是在每次页面加载时,使用 javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 html 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token。

该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址。由于系统也会在这个地址后面加上 token,黑客可以在自己的网站上得到这个 token,并马上就可以发动 CSRF 攻击。为了避免这一点,系统可以在添加 token 的时候增加一个判断,如果这个链接是链到自己本站的,就在后面添加 token,如果是通向外网则不加。不过,即使这个 csrftoken 不以参数的形式附加在请求之中,黑客的网站也同样可以通过 Referer 来得到这个 token 值以发动 CSRF 攻击。这也是一些用户喜欢手动关闭浏览器 Referer 功能的原因。

在Spring Security中,“GET”, “HEAD”, “TRACE”, "OPTIONS"四类请求可以直接通过,并不会被CsrfFilter过滤器过滤,会被直接放行,但是对于其他过滤器该过滤的还是会过滤的,除去上面四类,包括POST都要被验证携带token才能通过。

(3)在 HTTP 头中自定义属性并验证

这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。这样解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去。

然而这种方法的局限性非常大。XMLHttpRequest 请求通常用于 Ajax 方法中对于页面局部的异步刷新,并非所有的请求都适合用这个类来发起,而且通过该类请求得到的页面不能被浏览器所记录下,从而进行前进,后退,刷新,收藏等操作,给用户带来不便。另外,对于没有进行 CSRF 防护的遗留系统来说,要采用这种方法来进行防护,要把所有请求都改为 XMLHttpRequest 请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。

3.1.5 form表单如何添加token

如果您使用的是thymeleaf,如果使用的是thymeleaf,那么form action会帮我们自动加上csrf 隐藏域,我们不用特殊处理。
如果自己想要设置,我们也可以使用隐藏域自己设置,一般我们不会设置这个,默认就有你设置他干啥,参考代码如下:

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

3.1.6 ajax请求如何添加token

如果您使用的是thymeleaf,则可以直接在head标签内加上一个隐藏域即可。

<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});

3.1.7 如何关闭 CSRF 防御机制

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...
    ...
    //关闭CSRF跨站点请求仿造保护
    http.csrf().disable();
}

3.2 完成网站自动登录

自动登录是将用户的登录信息存在用户浏览器的cookie中,当用户下次登录时,实现动态自动登录的一种机制。

spring security 提供了两种非常好的令牌:

  • 利用散列算法加密用户的登录信息并生成令牌
  • 数据库持久性存储机制用的持久令牌

我们可以在登录接口返回后获取到返回的加密字符,然后保存起来,下次直接带上即可,可以为form表单添加一个隐藏域,然后添加上remember-me参数value就是登录接口返回的那个 remember-me cookie。

SpringSecurity从入门到源码分析_第3张图片

如果我想要关闭浏览器,下次再打开浏览器,权限管理系统会自动根据我上次的登录状态进行登录,这就是登录常用的“自动登录功能”,要想实现自动登录功能,我们需要实现两处关键配置就能使用了,具体操作如下:

打开 login.html 修改自动登录的name为remember-me,这是一个默认名称,可以修改,但是一般我们就叫这个名

<div class="form-group form-check">
    <input type="checkbox" class="form-check-input" id="autoLogin" name="remember-me">
    <label class="form-check-label" for="autoLogin">自动登录label>
div>

配置 SecurityConfig 开启自动登录功能

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...
    ...
    //开启记住我功能(自动登录)
    http.rememberMe()
        .rememberMeParameter("remember-me")//表单参数名,默认参数是remember-me
        .rememberMeCookieName("remember-me")//浏览器存的cookie名,默认是remember-me
        .tokenValiditySeconds(60*60*24*30);//保存30两天,默认是两周
}

3.3 保存凭证到数据库

自动登录功能方便是大家看得见的,但是安全性却令人担忧。因为cookie毕竟是保存在客户端的,很容易盗取,而且 cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。那么这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。 此外,Spring Security还提供了remember-me的另一种相对更安全的实现机制:在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录时,用cookie中的加密串,到数据库表中验证,如果通过,自动登录才算通过。这样,自动登录功能的安全性就有了保证,因此,我们需要在数据库中创建一张用于保存自动登录信息的表,这张表是固定的,包括名称、字段等信息,都不能修改,否则会认识失败。

CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

接下来,我们需要配置一下,告诉Spring Security使用哪一个dataSource来操作这个表

//数据源是我们默认配置的数据源,直接注入进来就行
@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    return jdbcTokenRepository;
}

3.4 使用UserDetailsService动态验证

之前我们介绍的都是要么写在配置文件中,要么写在内存中,这样根本就不合理,因为内存中的数据一旦断电,或是应用重启就没了,不能持久化,而且大量数据存放在内存中也是不合理的,接下来让我们使用UserDetailsService来从数据库中取出密码,然后再进行比对。

1.配置类

@EnableWebSecurity
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter  {

    @Autowired
    private SysUserDetailsService sysUserDetailsService;


    @Override
    protected void configure(HttpSecurity http) throws Exception {


        //设置X-Frame-Options响应头为SAMEORIGIN
        http.headers().frameOptions().sameOrigin();
        //放行不用权限的资源(去登录页面当然不需要用权限,否则你都看不到登录界面,还怎么登录,所以去登录界面必须放行)
        http.authorizeRequests().antMatchers("/security/toLogin","/login").permitAll();
        //拦截需要权限的资源(拦截所有请求,要想访问,登录的账号必须拥有admin角色才行)
        // hasAuthority:标识不变,hasAnyRole:在标识前面加上ROLE_
        http.authorizeRequests().antMatchers("/**").hasAuthority("admin").anyRequest().authenticated();
        //设置自定义登录界面
        http.formLogin()//启用表单登录
                .loginPage("/security/toLogin")//登录页面地址,只要你还没登录,默认就会来到这里
                .loginProcessingUrl("/login")//登录处理程序,Spring Security内置控制器方法
                .usernameParameter("username")//登录表单form中用户名输入框input的name名,不修改的话默认是username
                .passwordParameter("password")//登录表单form中密码框输入框input的name名,不修改的话默认是password
                .defaultSuccessUrl("/security/success")//登录认证成功后默认转跳的路径
                //.successForwardUrl("/main")//登录成功跳转地址,使用的是请求转发
                .failureForwardUrl("/security/toLogin")//登录失败跳转地址,使用的是请求转发
                .permitAll().and()
                .cors().disable();

        //设置自定义登出界面
        http.logout()//启用退出登录
                .logoutUrl("/logout")//退出处理程序,Spring Security内置控制器方法
                .logoutSuccessUrl("/security/toLogin")//退出成功跳转地址
                .invalidateHttpSession(true)//清除当前会话
                .deleteCookies("JSESSIONID")//删除当前Cookie
                .permitAll();
    }

    // 添加自定义用户加载实现
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(sysUserDetailsService);
    }

    // 需要放行的静态资源
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**");
        web.ignoring().antMatchers("/img/**");
        web.ignoring().antMatchers("/js/**");
        web.ignoring().antMatchers("/favicon.ico");
    }


}

2.自定义用户接口

public interface SysUserDetailsService extends UserDetailsService {

    SysUser selectSysUserByName(String username);
}

3.对自定义用户接口进行实现

@Service
public class SysUserDetailsServiceImpl implements SysUserDetailsService {



    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        SysUser sysUser = selectSysUserByName(username);

        //获取该用户所对应的所有角色,当查询用户的时候级联查询其所关联的所有角色,用户与角色是多对多关系
        //如果这个用户没有所对应的角色,也就是一个空集合,那么在登录的时候会报 403 没有权限异常,切记这点
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        List<SysRole> sysRoles = sysUser.getRoleList();
        for (SysRole sysRole : sysRoles) {
            authorities.add(new SimpleGrantedAuthority(sysRole.getRoleCode()));
        }

        //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证
        //org.springframework.security.core.userdetails.User实现了UserDetails对象,是SpringSecurity内置认证对象
       return   User.withUsername(sysUser.getUsername()).password("{noop}" + sysUser.getPassword()).authorities(authorities).build();

    }


    @Override
    public SysUser selectSysUserByName(String username) {
        // 先查询出用户(后面在数据库的操作,这里假设是从数据库中查询出的用户信息)
        SysUser sysUser = new SysUser();
        sysUser.setId("1634540885829984258");
        sysUser.setCreateTime(new Date());
        sysUser.setUpdateTime(new Date());
        sysUser.setIsDeleted(0);
        sysUser.setUsername("admin");
        sysUser.setPassword("admin");
        sysUser.setName("卡夫卡");
        sysUser.setPhone("17785888950");
        sysUser.setHeadUrl("https://blog.csdn.net/m0_46188681?spm=1001.2101.3001.5343");
        sysUser.setDeptId(101L);
        sysUser.setPostId(202L);
        sysUser.setDescription("");
        sysUser.setStatus(1);
        sysUser.setPostName("java开发工程师");
        sysUser.setDeptName("IT部");


        // 查询出用户对应的权限信息(后面在数据库的操作,这里假设是从数据库中查询出的用户角色信息)
        List<SysRole> sysRoles = new ArrayList<>();
        SysRole sysRole = new SysRole();
        sysRole.setId("1634540885829984256");
        sysRole.setCreateTime(new Date());
        sysRole.setUpdateTime(new Date());
        sysRole.setIsDeleted(0);
        sysRole.setRoleName("系统管理员");
        sysRole.setRoleCode("admin");
        sysRole.setDescription("系统的最高管理员,拥有最大的权限");
        sysRoles.add(sysRole);

        sysUser.setRoleList(sysRoles);

        return sysUser;

    }
}

2.用户实体和角色实体

// 权限实体
@Data
@TableName("sys_role")
public class SysRole {

    private static final long serialVersionUID = 1L;
    /**
     *主键id
     **/
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;
    /**
     *创建时间
     **/
    @TableField("create_time")
    private Date createTime;
    /**
     *更新时间
     **/
    @TableField("update_time")
    private Date updateTime;
    /**
     *逻辑删除 默认效果 0 没有删除 1 已经删除
     **/
    @TableLogic  
    @TableField("is_deleted")
    private Integer isDeleted;

    /**
     *角色名称
     **/
    @TableField("role_name")
    private String roleName;
    /**
     *角色代码
     **/
    @TableField("role_code")
    private String roleCode;
    /**
     *角色描述
     **/
    @TableField("description")
    private String description;

}
// 用户实体
@Data
@TableName("sys_user")
public class SysUser   {
	
	private static final long serialVersionUID = 1L;
	/**
	 *主键id
	 **/
	@TableId(value = "id", type = IdType.ASSIGN_ID)
	private String id;
	/**
	 *创建时间
	 **/
	@TableField("create_time")
	private Date createTime;
	/**
	 *更新时间
	 **/
	@TableField("update_time")
	private Date updateTime;
	/**
	 *逻辑删除 默认效果 0 没有删除 1 已经删除
	 **/
	@TableLogic
	@TableField("is_deleted")
	private Integer isDeleted;

	/**
	 *用户名
	 **/
	@TableField("username")
	private String username;
	/**
	 *密码
	 **/
	@TableField("password")
	private String password;
	/**
	 *姓名
	 **/
	@TableField("name")
	private String name;
	/**
	 *电话
	 **/
	@TableField("phone")
	private String phone;
	/**
	 *头像地址
	 **/
	@TableField("head_url")
	private String headUrl;
	/**
	 *部门id
	 **/
	@TableField("dept_id")
	private Long deptId;
	/**
	 *岗位id
	 **/
	@TableField("post_id")
	private Long postId;
	/**
	 *描述
	 **/
	@TableField("description")
	private String description;
	/**
	 *1正常使用,0停止使用
	 **/
	@TableField("status")
	private Integer status;

	/**
	 *权限
	 **/
	@TableField(exist = false)
	private List<SysRole> roleList;
	/**
	 *岗位
	 **/
	@TableField(exist = false)
	private String postName;
	/**
	 *部门
	 **/
	@TableField(exist = false)
	private String deptName;

}

3.5 用户密码加密

我们数据库中存放的密码肯定不能存放明文,因为这样不太安全,密码是敏感信息,如果密码泄漏,会对用户的隐私,以及金钱等造成巨大的影响,即使是内部人员,也不知道用户的密码是什么最好,因为这样才能保证用户的密码是安全的。

这里我们使用MD5加密算法,MD5加密算法是不可逆的,也就是说我们的用户密码在保存到数据库的时候,就先进行加密,那么用户在进行登录的时候,我们先把用户的密码加密,然后在去和数据库中的密码做比较。那么自然能比对成功。

MD5加密工具类

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public final class MD5 {

    /**
     * md5加密算法,不可逆,只能加密,不能解密
     * @param strSrc
     * @return java.lang.String
     * @author compass
     * @date 2023/3/11 22:03
     * @since 1.0.0
     **/
    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f'};
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }


}

我们只需要实现PasswordEncoder即可,然后实现 encode(加密方法)和matches(密码比对方法)即可,然后我们把这个bean添加到容器中即可。

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 

* t密码的处理方法类型 *

* * @author compass * @since 2019-11-08 */
@Component public class DefaultPasswordEncoder implements PasswordEncoder { public DefaultPasswordEncoder() { this(-1); } /** * @param strength the log rounds to use, between 4 and 31 */ public DefaultPasswordEncoder(int strength) { } @Override public String encode(CharSequence rawPassword) { return MD5.encrypt(rawPassword.toString()); } /** * 自定义密码加密 * @param rawPassword 数据库密码 * @param encodedPassword 用户密码 * @return boolean * @author comapss * @date 2023/3/11 22:10 * @since 1.0.0 **/ @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(encode(rawPassword.toString())); } }

我们的SysUserDetailsServiceImpl需要先把密码改成我们加密后的密码,这样才能比对成功,我们这里使用的是模拟数据库的操作,所以真实开发的时候,在用户注册的时候,使用 DefaultPasswordEncoder.encode 方法把密码加密一下就ok。

   @Override
    public SysUser selectSysUserByName(String username) {
        // ...
        // 这里把密码加密一下(原始密码就是admin)
        sysUser.setPassword("21232f297a57a5a743894a0e4a801fc3");
         // ...
        return sysUser;

    }

因为之前 SysUserDetailsServiceImpl.loadUserByUsername 我们设置的不需要加密,还需要改一下之前的密码,之前的密码加了前缀{noop}表示不需要加密,我们现在把他去掉

     @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

       // ...
       return   User.withUsername(sysUser.getUsername()).password( sysUser.getPassword())
                .authorities(authorities).build();
         // ...

    }

其实spring security 编码工厂也提供了很多的加密方式,我们也可以使用他内置的加密方式。在PasswordEncoderFactories这个工厂中。

public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256",
				new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());
		return new DelegatingPasswordEncoder(encodingId, encoders);
	}

那个这个加密方式在什么时候注入到容器中的呢?首先spring是一个大的容器,而且他有很多的声明周期,大多数时间点都可以获取到spring容器,一般都是获取到spring容器,然后向这个容器中添加一个bean ,这个spring容器也就是 ApplicationContext 对象,在最开始的时候默认的是 BCryptPasswordEncoder 进行加密,如果我们有实现接口那么就在WebSecurityConfigurerAdapter.setApplicationContext中进行重新设置,在setApplicationContext方法上使用@Autowired把对象注入进来,然后拿到spring容器对象,把我们的密码加密对象放进去。

然后我们在WebSecurityConfig中添加进去

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(sysUserDetailsServiceImpl).passwordEncoder(passwordEncoder);
}

我们在我们自定义加密的地方打个断点,然后执行一些,可以看到确实来到这个方法加密后再进行比对的,而且加密后和我们数据库存入的密码也是一致的,这样比对成功,登录也就没有什么问题。

SpringSecurity从入门到源码分析_第4张图片

3.6 自定义异常处理handler

有时候,用户的密码虽然正确,但是权限不够,不能访问或操作我们系统的其他资源,我们可以给出友好的提示,我们可以这样配置。


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        // 随便修改一下,让admin用户没有权限,看看是否成功返回我们想要的json数据
        http.authorizeRequests().antMatchers("/**").hasAuthority("user").anyRequest().authenticated();

        //异常处理,我们可以自定义一个类,然后实现AccessDeniedHandler接口进行处理,当然也可以使用函数式编程实现
        http.exceptionHandling().accessDeniedHandler(defaultAccessDeniedHandler);
	  // ...


    }

自定义实现类DefaultAccessDeniedHandler

@Component
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员增加权限!\",\"code\":\"403\"}");
        out.flush();
        out.close();
    }
}

可以清楚的看到,我们的权限确实不足,而且也给出了合理的json提示信息。测试完记得改回去。

image-20230311225213793

3.7 会话管理

3.7.1 理解会话

会话就是无状态的http实现用户可维护的一种解决方案,http本身无状态的,使得用户在与服务器交互的过程中,每一次请求都没有关联性。也就意味我们没有办法识别用户是否之前已经授权过,session的出现就是为了解决这个问题的。服务器通过与用户绑定每个请求都携带一个id类的信息,而id又可以关联用户信息为了让用户都携带同一个id,于是cookie就成了很好的载体,当用户首次访问系统,会为该用户生成一个sessionid,每次用户在cookie中带上sessionid就可以识别这个用户之前是否访问过。

3.7.2 防御会话固定攻击

防御会话固定工具的方式很简单,只需要在用户登录之后生成新的session即可,在继承WebSecurityConfigurerAdapter时 SpringSecurity时就已经开启该配置了。

我们可以在 WebSecurityConfig 进行配置

)  @Override
    protected void configure(HttpSecurity http) throws Exception {
    // ...
     http.sessionManagement().sessionFixation().newSession();
    // ...
}

http.sessionManagement().sessionFixation():配置选项如下:

  • none:不做任何变动,登录后继续使用之前的session
  • newSession:登录之后创建一个新的session
  • changeSessionId:创建后新建一个session,并且将之前session的旧数据复制过来
  • migrateSession:不创建新的会话,而是使用Servlet容器提供的会话固定保护

3.7.3 会话过期处理

1.配置会话过期需要跳转的页面


 @Override
protected void configure(HttpSecurity http) throws Exception {
    ...
  http.sessionManagement().invalidSessionUrl("/sessionOut")
   ...
}

2.配置会话过期需要处理的策略

  
 @Override
protected void configure(HttpSecurity http) throws Exception {
    ...
  http.sessionManagement().sessionAuthenticationStrategy((authentication, request, response) -> {
            
        });
   ...
}

默认情况下如果在30分钟内没有操作,那就session失效,会话过期,需要重新登录,当然我们可以手动配置。最小单位为1分钟,如果小于一分钟也会被默认设置为1分钟,这是spring bean的配置策略。

// 单位为秒
server.session.timeout = 60;

3.7.4 保证单一登录

有时候我们为了安全,也可以设置同一个账户,只能同时有一个人在线,我们只需要简单的配置就能实现。

第一种:单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...
    ...
    //单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
    http.sessionManagement().maximumSessions(1).expiredUrl("/toLogin");
}

第二种:单用户登录,如果有一个登录了,同一个用户在其他地方不能登录

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...
    ...
    //单用户登录,如果有一个登录了,同一个用户在其他地方不能登录
    http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
}
// 如果在别的地方登录则会抛出异常 SessionAuthenticationException

3.8 AuthenticationException异常

在用户认证的过程中肯定会出现异常,如果出现异常我们如何给出友好的提示信息呢?我们可以看下AuthenticationException的子类都定义了那些异常。

UsernameNotFoundException 用户找不到

BadCredentialsException 坏的凭据

AccountStatusException 用户状态异常它包含如下子类

AccountExpiredException 账户过期

LockedException账户锁定

DisabledException 账户不可用

CredentialsExpiredException 证书过期

SessionAuthenticationException 别地方登录异常

我们可以在 3.5 的自定义异常处理handler中判断异常类型,给出对应的提示信息,这里不再做过多的演示。

3.9 使用json方式处理登录结果

有时候我们并不算跳转到指定页面,而是返回对应的json数据由前端去处理,那么我们就看使用successHandler和failureHandler这个两个处理回调函数进处理。


        //设置X-Frame-Options响应头为SAMEORIGIN
        http.headers().frameOptions().sameOrigin();
        //放行不用权限的资源(去登录页面当然不需要用权限,否则你都看不到登录界面,还怎么登录,所以去登录界面必须放行)
        http.authorizeRequests().antMatchers("/security/toLogin","/login").permitAll();
        //拦截需要权限的资源(拦截所有请求,要想访问,登录的账号必须拥有admin角色才行)
        // hasAuthority:标识不变,hasAnyRole:在标识前面加上ROLE_
        http.authorizeRequests().antMatchers("/**").hasAuthority("admin").anyRequest().authenticated();
        //设置自定义登录界面
        http.formLogin()//启用表单登录
                .loginPage("/security/toLogin")//登录页面地址,只要你还没登录,默认就会来到这里
                .loginProcessingUrl("/login")//登录处理程序,Spring Security内置控制器方法
                .usernameParameter("username")//登录表单form中用户名输入框input的name名,不修改的话默认是username
                .passwordParameter("password")//登录表单form中密码框输入框input的name名,不修改的话默认是password
                .permitAll().and()
                .cors().disable();

        //  登录失败处理回调 
        http.formLogin().failureHandler((req, res, e) -> {
            // 这里验证一下只能一个地方登录的提示信息是否能正常捕获到,如果是其他信息,请做一个设计捕获更多信息
            String message = "{\"code\":\"500\",\"message\":\"身份验证失败\",\"data\":\"false\"}";
            if (e instanceof SessionAuthenticationException){
                message = "{\"code\":\"500\",\"message\":\"一个账号不能同时在多个地方登录\",\"data\":\"false\"}";
            }
            res.setContentType("application/json;charset=utf-8");
            PrintWriter out = res.getWriter();
            out.write(message);
            out.flush();
            out.close();

        // 登录成功处理回调
        }).successHandler((req, res, e) -> {
            res.setContentType("application/json;charset=utf-8");
            PrintWriter out = res.getWriter();
            out.write("{\"code\":\"200\",\"message\":\"登录成功\",\"data\":\"true\"}");
            out.flush();
            out.close();
        });

SpringSecurity从入门到源码分析_第5张图片

3.10 Spring Security的滤器

其实我们之前配置的HttpSecurity http实际上就是在配置 Spring Security 的过滤器链,诸如CSRF,CORS表单登录等,每个配装器对应一个过滤器链。

默认内置的过滤器如下,执行顺序如下:

  1. ChannelProcessingFilter:ChannelProcessingFilter 通常是用来过滤哪些请求必须用 https 协议, 哪些请求必须用 http 协议, 哪些请求随便用哪个协议都行。它主要有两个属性:
  • ChannelDecisionManager 用来判断请求是否符合既定的协议规则。它维护了一个 ChannelProcessor 列表 这些ChannelProcessor 是具体用来执行 ANY_CHANNEL 策略 (任何通道都可以), REQUIRES_SECURE_CHANNEL 策略 (只能通过https 通道), REQUIRES_INSECURE_CHANNEL 策略 (只能通过 http 通道)。

  • FilterInvocationSecurityMetadataSource 用来存储 url 与 对应的ANY_CHANNEL、REQUIRES_SECURE_CHANNEL、REQUIRES_INSECURE_CHANNEL 的映射关系。

  • ChannelProcessingFilter 通过 HttpScurity#requiresChannel() 等相关方法引入其配置对象 ChannelSecurityConfigurer 来进行配置

  1. SecurityContextPersistenceFilter:该 Filter 的主要作用是为了加载 SecurityContext 到 SecurityContextHolder 中。

  2. LogoutFilter:该 Filter 的作用主要是对登出操作做处理,我们可以自定义 LogoutHandles 来处理登出逻辑,并且可以配置 LogoutSuccessHandle 或者指定 logoutSuccessUrl。

  3. X509AuthenticationFilter:处理X509认证,NO

  4. AbstractPreAuthenticatedProcessingFilter:AbstractPreAuthenticatedProcessingFilter 处理处理经过预先认证的身份验证请求的过滤器的基类,其中认证主体已经由外部系统进行了身份验证。目的只是从传入请求中提取主体上的必要信息,而不是对它们进行身份验证。你可以继承该类进行具体实现并通过 HttpSecurity#addFilter 方法来添加个性化的AbstractPreAuthenticatedProcessingFilter 。

  5. UsernamePasswordAuthenticationFilte:处理用户以及密码认证的核心过滤器。认证请求提交的username和 password,被封装成token进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。

  6. OpenIDAuthenticationFilter:基于OpenID 认证协议的认证过滤器。你需要在依赖中依赖额外的相关模块才能启用它。

  7. DefaultLoginPageGeneratingFilte:生成默认的登录页。默认 /login 。

  8. DefaultLogoutPageGeneratingFilter:生成默认的退出页。默认 /logout 。

  9. ConcurrentSessionFilter:处理session有效期

  10. DigestAuthenticationFilter:Digest身份验证是 Web 应用程序中流行的可选的身份验证机制 。DigestAuthenticationFilter 能够处理 HTTP 头中显示的摘要式身份验证凭据。你可以通过 HttpSecurity#addFilter() 来启用和配置相关功能。

  11. BearerTokenAuthenticationFilter:处理OAuth2认证的AccessToken

  12. BasicAuthenticationFilter:和Digest身份验证一样都是Web 应用程序中流行的可选的身份验证机制 。 BasicAuthenticationFilter 负责处理 HTTP 头中显示的基本身份验证凭据。这个 Spring Security 的 Spring Boot 自动配置默认是启用的 。

  13. RequestCacheAwareFilter:用于用户认证成功后,重新恢复因为登录被打断的请求。当匿名访问一个需要授权的资源时。会跳转到认证处理逻辑,此时请求被缓存。在认证逻辑处理完毕后,从缓存中获取最开始的资源请求进行再次请求。RequestCacheAwareFilter 通过 HttpScurity#requestCache() 及相关方法引入其配置对象 RequestCacheConfigurer 来进行配置。

  14. SecurityContextHolderAwareRequestFilter:用来 实现j2ee中 Servlet Api 一些接口方法, 比如 getRemoteUser 方法、isUserInRole 方法,在使用 Spring Security 时其实就是通过这个过滤器来实现的。SecurityContextHolderAwareRequestFilter 通过 HttpSecurity.servletApi() 及相关方法引入其配置对象 ServletApiConfigurer 来进行配置。

  15. JaasApiIntegrationFilter:适用于JAAS (Java 认证授权服务)。如果 SecurityContextHolder 中拥有的 Authentication 是一个 JaasAuthenticationToken,那么该 JaasApiIntegrationFilter 将使用包含在 JaasAuthenticationToken 中的 Subject 继续执行 FilterChain。

  16. RememberMeAuthenticationFilter:实现记住我功能的过滤器

  17. AnonymousAuthenticationFilter:该 Filter 的作用是对匿名用户做一些处理,比如:设置 principal 和 authorities

  18. SessionManagementFilter:该 Filter 的作用是管理 Session。

  19. ExceptionTranslationFilter:该 Filter 的作用是对于任意的 AccessDeniedException 类型的异常和 AuthenticationException 类型异常的处理。

  20. FilterSecurityInterceptor:该 Filter 的作用主要是做认证和授权拦截,比如我们配置了对某个 url 限制某种角色能够访问,就是在这里进行鉴权,判断是否放行。

  21. SwitchUserFilter:负责用户上下文角色切换

如何添加过滤器链:在WebSecurityConfig.configure()中可以使用http对象进行添加,添加的几种方式如下:

// 1.添加在指定过滤器之后
public HttpSecurity addFilterAfter(Filter filter, Class<? extends Filter> afterFilter)
// 2.添加在指定过滤器之前
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
// 3.添加在指定过滤器偏移量后添加
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter)    
// 4.添加在指定过滤器的通一个位置
public HttpSecurity addFilterAt(Filter filter, Class<? extends Filter> atFilter)
// 5.添加一个SpringSecurity扩展的过滤器,该方法确保自动处理Filters的顺序
public HttpSecurity addFilter(Filter filter)

3.11 自定义验证码过滤器

1.引入依赖

   <dependency>
            <groupId>com.github.pengglegroupId>
            <artifactId>kaptchaartifactId>
            <version>2.3.2version>
   dependency>

2.配置一个验证码bean

    // 配置一个验证码生成器
    @Bean
    public Producer captcha(){
        Properties captchaConfig = new Properties();
        // 验证码宽度
        captchaConfig.setProperty("captcha.image.width","150");
        // 验证码高度
        captchaConfig.setProperty("captcha.image.height","50");
        // 验证码随机字符
        captchaConfig.setProperty("captcha.textproducer.char.string","0123456789");
        // 验证码长度
        captchaConfig.setProperty("kaptcha.textproducer.char.length","5");

        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(new Config(captchaConfig));

        return defaultKaptcha;
    }

3.写一个生成验证码的接口

@Controller
public class CaptchaController {

    @Autowired
    private Producer captchaProducer;

    public static final String  CAPTCHA_SESSION_KEY= "captcha";

    @RequestMapping("/captcha.jpg")
    public void getVerificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 设置响应文件类型
        response.setContentType("image/jpg");
        // 创建验证码文本
        String captchaText = captchaProducer.createText();
        // 将验证码放到session
        request.getSession().setAttribute(CAPTCHA_SESSION_KEY,captchaText);
        // 创建验证码图
        BufferedImage image = captchaProducer.createImage(captchaText);
        // 获取响应流
        ServletOutputStream outputStream = response.getOutputStream();
        // 输出到响应流
        ImageIO.write(image,"jpg",outputStream);
        // 刷新通道
        outputStream.flush();

    }
}

4.定义一个验证码异常类

import javax.naming.AuthenticationException;
public class VerificationCodeException extends AuthenticationException {
    public VerificationCodeException(){
        super("验证码校验失败");
    }
}

5.给验证码接口放行

        //放行不用权限的资源(去登录页面当然不需要用权限,否则你都看不到登录界面,还怎么登录,所以去登录界面必须放行)
        http.authorizeRequests().antMatchers("/security/toLogin","/login","/captcha.jpg").permitAll();

6.自定义过滤器校验逻辑

import com.compass.security.controller.CaptchaController;
import com.compass.security.execption.VerificationCodeException;
import lombok.SneakyThrows;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class VerificationCodeFilter extends OncePerRequestFilter {

    public static final String LOGIN_REQUEST_URI = "/login";

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 如果接口是登录接口,则进行校验,其余的放行
        if (request.getRequestURI().equals(LOGIN_REQUEST_URI)){
            try {
                checkCode(request,response);
            }catch (VerificationCodeException e){
                e.printStackTrace();
                throw e;
            }
        }else {
            filterChain.doFilter(request,response);
        }
    }

    private void checkCode(HttpServletRequest request, HttpServletResponse response) throws VerificationCodeException {
        // 获取前端传递的验证码
        String parameterCode = request.getParameter(CaptchaController.CAPTCHA_SESSION_KEY);

        // 从session中获取验证码
        String serverCode = request.getSession().getAttribute(CaptchaController.CAPTCHA_SESSION_KEY)+"";

        if (serverCode.equals(parameterCode)){
            // 无论成功还是失败,删除掉验证码
            request.getSession().removeAttribute(CaptchaController.CAPTCHA_SESSION_KEY);
        }else {
            // 验证码不正确,抛出异常
            throw new VerificationCodeException();
        }


    }
}

7.将自定义的校验过滤器添加到过滤器链上

    // 新增一个验证码处理器,在表单登录验证之前进行验证
    http.addFilterBefore(verificationCodeFilter, UsernamePasswordAuthenticationFilter.class);

8.修改一下前端登录页面的

<body>
<div id="wrapper">
    <div id="login_form">
        <form th:action="@{/login}" method="post">
            <input type="text" name="username" placeholder="用户名" id="username"/>
            <br>
            <input type="password" name="password" placeholder="密码" id="password"/>
            <br>
            <input type="text" name="captcha" id="captcha"/>
            <img id="img-captcha" src="/captcha.jpg">
            <br>
            <button id="create">登录button>
        form>
    div>
div>
body>

// 新增的css
   #captcha {
        width: 150px;
    }

    #img-captcha {
        position: absolute;
        right: 20px;
        top: 165px;
        border-radius: 5px;
    }

如果输入的验证码不正确,则会调回登录页面,并且控制台也打印了验证码没验证过的信息。其实这个验证码应该实现点击,更换,其实我们可以,给图片增加一个点击时间,参数带一个时间戳,这样每次都会新生成一个验证码。这里就不去实现了。

SpringSecurity从入门到源码分析_第6张图片

3.12自定义认证流程

Spring Security并没有柔和整个认证流程,而是提供一个抽象类AbstractUserDetailsAuthenticationProvider,然后进行认证。

我们来看下他的子类核心方法 DaoAuthenticationProvider ,也就是UserDetailsService自定义用户来源的校验类

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	/**
	 * The plaintext password used to perform PasswordEncoder#matches(CharSequence,
	 * String)} on when the user is not found to avoid SEC-2056.
	 */
	private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

	private PasswordEncoder passwordEncoder;

	/**
	 * The password used to perform {@link PasswordEncoder#matches(CharSequence, String)}
	 * on when the user is not found to avoid SEC-2056. This is necessary, because some
	 * {@link PasswordEncoder} implementations will short circuit if the password is not
	 * in a valid format.
	 */
	private volatile String userNotFoundEncodedPassword;

	private UserDetailsService userDetailsService;

	private UserDetailsPasswordService userDetailsPasswordService;
    // 设密码加密encoder
	public DaoAuthenticationProvider() {
		setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
	}

    // 附加认证,也就是比对加密的密码和数据库密码进行比对的方法
	@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			this.logger.debug("Failed to authenticate since no credentials provided");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}

	@Override
	protected void doAfterPropertiesSet() {
		Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
	}

    // 查询一个用户是否存在,如果存在返回UserDetails
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}
    // 此方法是父类的
    @Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
                // 先检索用户是否存在
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
            // 检测账户是否可用
			this.preAuthenticationChecks.check(user);
            // 附加认证
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            // 检测账户是否可用
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
        // 检测用户名和密码是否过期
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        // 返回一个认证通过的用户信息
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

    // 创建一个通过认证的用户,包含权限信息
	@Override
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		boolean upgradeEncoding = this.userDetailsPasswordService != null
				&& this.passwordEncoder.upgradeEncoding(user.getPassword());
		if (upgradeEncoding) {
			String presentedPassword = authentication.getCredentials().toString();
			String newPassword = this.passwordEncoder.encode(presentedPassword);
			user = this.userDetailsPasswordService.updatePassword(user, newPassword);
		}
		return super.createSuccessAuthentication(principal, authentication, user);
	}

 

}

我们可以实现继承DaoAuthenticationProvider 或者是 抽象类AbstractUserDetailsAuthenticationProvider进行附加认证自定义核心认证逻辑。如果我们确定的是用户信息来源是通过UserDetailsService来获取的,那么可以直接继承DaoAuthenticationProvider 进行附加认证。

我们接下来,修改一下验证码的认证流程,使用继承DaoAuthenticationProvider来完成

@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    @Autowired
    public MyAuthenticationProvider(@Qualifier("sysUserDetailsServiceImpl") UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 实现自定义校验逻辑

        // 调用父类校验逻辑
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

感觉是没什么问题,但是我们不能获取HttpServletRequestrequest对象,其实Authentic还可以携带其他的参数,并发只携带用户名和密码。当然通过spring上下文提供的一个requestHolder也可以获取到request对象,但是我们这里不那样去做。

public interface Authentication extends Principal, Serializable {

   // ...
   // 允许携带任意对象 
	Object getDetails();
    // ...

}

一个认证流程包含多个AuthenticationProvider,那么这些AuthenticationProvider都是由ProviderManager进行管理的。而ProviderManager是由UsernamePasswordAuthenticationFilte进行调用的,也就是所有的AuthenticationProvider包含的Authentication都来源于UsernamePasswordAuthenticationFilte。

/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.web.authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

/**
 * Processes an authentication form submission. Called
 * {@code AuthenticationProcessingFilter} prior to Spring Security 3.0.
 * 

* Login forms must present two parameters to this filter: a username and password. The * default parameter names to use are contained in the static fields * {@link #SPRING_SECURITY_FORM_USERNAME_KEY} and * {@link #SPRING_SECURITY_FORM_PASSWORD_KEY}. The parameter names can also be changed by * setting the {@code usernameParameter} and {@code passwordParameter} properties. *

* This filter by default responds to the URL {@code /login}. * * @author Ben Alex * @author Colin Sampaleanu * @author Luke Taylor * @since 3.0 */ public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //... @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); username = (username != null) ? username : ""; username = username.trim(); String password = obtainPassword(request); password = (password != null) ? password : ""; // 生成一个基本的Authentication UsernamePasswordAuthenticationToken authRequest = new UsernamePassword Token(username, password); // 为该Authentication设置详细信息 setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } // 设置附带信息 protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } //... }

UsernamePasswordAuthenticationFilte并没有设置用户详细信息的流程,而是通过 AuthenticationDetailsSource进行构建的。

public interface AuthenticationDetailsSource<C, T> {
	T buildDetails(C context);
}

在UsernamePasswordAuthenticationFilte中使用的是WebAuthenticationDetailsSource,他使用的是WebAuthenticationDetails,是一个基于web认证的。携带用户的sessionId和IP地址。

现在我们来完整的写一下这个流程。

1.继承WebAuthenticationDetails获取到requset,然后判断验证码

提供public class MyWebAuthenticationDetails extends WebAuthenticationDetails {

    private static final long serialVersionUID = 322681241320288933L;
    private boolean verificationCodeResult;

    public boolean getVerificationCodeResult( ) {
        return verificationCodeResult;
    }

    public MyWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        checkCode(request);
    }

    private void checkCode(HttpServletRequest request)   {
        // 获取前端传递的验证码
        String parameterCode = request.getParameter(CaptchaController.CAPTCHA_SESSION_KEY);

        // 从session中获取验证码
        String serverCode = request.getSession().getAttribute(CaptchaController.CAPTCHA_SESSION_KEY) + "";

        if (serverCode.equals(parameterCode)) {
            // 无论成功还是失败,删除掉验证码
            request.getSession().removeAttribute(CaptchaController.CAPTCHA_SESSION_KEY);
             this.verificationCodeResult = true;
        } else {
            // 验证码不正确
            this.verificationCodeResult = false;
        }
    }

}

2.写一个继承AuthenticationDetailsSource,将MyWebAuthenticationDetailst提供给AuthenticationDetailsSource

@Component
public class MyWebAuthenticationDetailsSource  implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new MyWebAuthenticationDetails(context);
    }
}

3.在校验流程中加入验证码校验

@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    @Autowired
    public MyAuthenticationProvider(@Qualifier("sysUserDetailsServiceImpl") UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    @SneakyThrows
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 实现自定义校验逻辑
        MyWebAuthenticationDetails details = (MyWebAuthenticationDetails)authentication.getDetails();
        // 如果验证码不正确,抛出异常
        if (!details.getVerificationCodeResult()){
            throw new VerificationCodeException();
        }

        // 调用父类校验逻辑
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

4.在WebSecurityConfig中进行配置

    @Autowired
    private MyAuthenticationProvider myAuthenticationProvider;

    @Autowired
    private MyWebAuthenticationDetailsSource myWebAuthenticationDetailsSource;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        // 新增自定义的 DetailsSource
        http.formLogin() .authenticationDetailsSource(myWebAuthenticationDetailsSource)
       // ...
    }

     @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 提供自定义身份校验者
        auth.authenticationProvider(myAuthenticationProvider);
    }            
               

至此,我们就完成了自定义处理校验流程的逻辑。当然之前基于过滤器校验的逻辑需要注释掉,避免出现不必要的问题。


1.AuthenticationManager 身份验证管理器 接收一个认证对象 Authentication

2.Authentication是一个接口,其中接口中定义了如下方法
getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
getCredentials(),密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
getPrincipal(),最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。

3.ProviderManager实现了接口AuthenticationManager
它是 AuthenticationManager 的一个实现类,提供了基本的认证逻辑和方法;它包含了一个 List<AuthenticationProvider> 对象,通过 AuthenticationProvider 接口来扩展出不同的认证提供者(Spring Security默认提供的实现类不能满足需求的时候可以扩展AuthenticationProvider 覆盖supports(Class<?> authentication) 方法)4.ProviderManager中的authenticate方法中会迭代所有实现了AuthenticationProvider的对象 会判断当前的认证对象Authentication是否可以被AuthenticationProvider所处理,如果能的话就调用AuthenticationProvider中的authenticate方法进行认证

5.
    
    

SpringSecurity从入门到源码分析_第7张图片

4.spring security 过滤器加载流程分析

4.1 JavaWeb三大组件

学过servlet的同学都知道,我们的javaweb的三大组件,分别是servlet,filter,Listener,现在我们来回顾一下他们的作用,不管框架怎么变,都是基于这些基础的技术进行封装的,让我们的程序更加的好友,适配性强,扩展性强的效果。

Servlet:

Servlet 是 SUN 推出的一套规范,定义了一套处理请求和发送响应的接口。一般认为Servlet其实就是一个遵循Servlet规范开发的java类,Serlvet是由服务器调用的,运行在服务器端。

Tomcat 是Web应用服务器,是一个Servlet/JSP容器。 Tomcat 作为 Servlet 容器,负责处理客户请求,把请求传送给 Servlet,并将 Servlet 的响应传送回给客户,而 Servlet 是一种运行在支持 Java 语言的服务器上的组件。

SpringSecurity从入门到源码分析_第8张图片

Filter

Filter(过滤器)用于拦截用户请求,在服务器作出响应前,可以在拦截后修改request和response。可以实现一次编码,多处应用。Filter不像Servlet,它不能产生一个请求或者响应,它只是修改对某一资源的请求,或者修改从某一的响应。

Web开发人员通过Filter技术,对web服务器管理的所有web资源。例如实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。例如对Jsp, Servlet, 静态图片文件或静态 html 文件等进行拦截,从而实现一些特殊的功能。

Filter主要的作用有两个:

  • 拦截修改请求:在HttpServletRequest到达Servlet之前,拦截客户的HttpServletRequest。根据需要检查HttpServletRequest,也可以修改HttpServletRequest头和数据。
  • 拦截修改响应:在HttpServletResponse到达客户端之前,拦截HttpServletResponse。根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据。

SpringSecurity从入门到源码分析_第9张图片

Listener
web监听器是一种Servlet中的特殊的类,它们能帮助开发者监听web中的特定事件,比如ServletContext,HttpSession,ServletRequest的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控。

SpringSecurity从入门到源码分析_第10张图片

Spring Security正是基于Filter对请求进行拦截处理,实现认证授权功能,filter有对访问权限控制的权利,而springsecurity就是基于他来进行实现整个认证流程的,springsecurity说到底,就是一堆过滤器链,挨个进行执行处理,如果中途你没有权限,或者是认证不成功,那么就给你返回认证失败的结果。

4.2 springboot程序是如何加载过滤器的?

我们先来看下springboot程序如何配置进去一个filter,他配置的方式有很多种。我们现在挨个来看下。

1.@ServletComponentScan+@WebFilter

自定义一个filter类

@WebFilter(filterName = "testFilter", urlPatterns = "/")
public class TestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("doFilter");
    }

    @Override
    public void destroy() {
        System.out.println("destroy");
    }
}

在启动类或配置类中加上@ServletComponentScan,也就是自定义filter所在包位置

@ServletComponentScan("compass.jwt.com.filter")

2.使用 SpringBoot 提供的 FilterRegistrationBean

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<LoggerFilter> filterRegistrationBean() {
        FilterRegistrationBean<LoggerFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new TestFilter());	// 这里可以使用 new,也可以在 Filter 上加 @Component 注入进来
        bean.addUrlPatterns("/");
        bean.setName("loggerFilter");
        bean.setOrder(1);	// 值越小,优先级越高
        return bean;
    }
		
	// 可以写多个 FilterRegistrationBean
}

3.直接在 Filter 上使用 @Component 注解

@Component
@Order(-1)	// 可以指定优先级,不填的话默认为最小的优先级
// 注意:这种方式默认会过滤所有的请求
public class HelloFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init HelloFilter");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("HelloFilter doFilter");
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

4.使用 DelegatingFilterProxyRegistrationBean 注册已经注册为 Bean 的 Filter

@Configuration
public class FilterConfig {

    @Bean
    public DelegatingFilterProxyRegistrationBean delegatingFilterProxyRegistrationBean() {
    	// 构造器参数填的就是 targetBeanName,即 Filter 在 IoC 容器中的 Bean 名称
        DelegatingFilterProxyRegistrationBean helloFilter = new DelegatingFilterProxyRegistrationBean("helloFilter");
        helloFilter.addUrlPatterns("/hello");
        return helloFilter;
    }
}
  

spring security 就是用的DelegatingFilterProxyRegistrationBean的这种方式。

4.3 spring security过滤器链的初始化

首先在springboot启动的时候会从soring.factories配置中加载需要注册的bean,然后注册到容器中,而SecurityFilterAutoConfiguration中就在里面配置了。从名字上就可以看出,这个是配置springsecurity的过滤器链的自动配置类。

SpringSecurity从入门到源码分析_第11张图片

在这个自动配置类中会注册一个DelegatingFilterProxyRegistrationBean

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {

	private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;

	@Bean
	@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
	public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
			SecurityProperties securityProperties) {
		DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
				DEFAULT_FILTER_NAME);
		registration.setOrder(securityProperties.getFilter().getOrder());
		registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
		return registration;
	}

	private EnumSet<DispatcherType> getDispatcherTypes(SecurityProperties securityProperties) {
		if (securityProperties.getFilter().getDispatcherTypes() == null) {
			return null;
		}
		return securityProperties.getFilter().getDispatcherTypes().stream()
				.map((type) -> DispatcherType.valueOf(type.name()))
				.collect(Collectors.toCollection(() -> EnumSet.noneOf(DispatcherType.class)));
	}

}

DelegatingFilterProxyRegistrationBean 注册成功后,该过滤器就被加载了到了注册器中。

注册器注册了所以的过滤器后,在AbstractFilterRegistrationBean中会调用addRegistration()方法,这个方法会调用 getFilter()会为每个过滤器生成代理对象 DelegatingFilterProxy,本质上来说DelegatingFilterProxy就是一个Filter,其间接实现了Filter接口,但是在doFilter中其实调用的从Spring 容器中获取到的代理Filter的实现类。

// DelegatingFilterProxyRegistrationBean	
@Override
	public DelegatingFilterProxy getFilter() {
		return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {

			@Override
			protected void initFilterBean() throws ServletException {
				// Don't initialize filter bean on init()
			}

		};
	}

然后会在AbstractConfiguredSecurityBuilder中的configure()方法中遍历所有的实现了SecurityConfigurerAdapter的bean去调用configure(HttpSecurity http)方法,将所有的过滤器添加到spring security的过滤器链上。

SpringSecurity从入门到源码分析_第12张图片

标记红色字体的都是我自定义的过滤器,其余的是spring security自自己注册的过滤器。添加完成后,将这些过滤器再封装为DefaultSecurityFilterChain。 添加到这个web过滤器中去。

这一系列流程都是在 AbstractConfiguredSecurityBuilder的doBuild()方法中完成的

	protected final O doBuild() throws Exception {
		synchronized (configurers) {
			buildState = BuildState.INITIALIZING;

			beforeInit();
			init();

			buildState = BuildState.CONFIGURING;

			beforeConfigure();
			configure();

			buildState = BuildState.BUILDING;

			O result = performBuild();

			buildState = BuildState.BUILT;

			return result;
		}
	}

**首先注意一点:**security的过滤器我们尽量自己new创建对象,不要交给spring管理,不然在web整个filter会出现一次,在security过滤器链上还会出现一次。

4.4 spring security过滤器执行流程

请求来时,首先来到的就是OncePerRequestFilter,在这中OncePerRequestFilter会调用internalDoFilter()方法执行所有的过滤器操作。

    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {

        if( Globals.IS_SECURITY_ENABLED ) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            try {
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedExceptionAction<Void>() {
                        @Override
                        public Void run()
                            throws ServletException, IOException {
                            internalDoFilter(req,res);
                            return null;
                        }
                    }
                );
            } catch( PrivilegedActionException pe) {
                Exception e = pe.getException();
                if (e instanceof ServletException)
                    throw (ServletException) e;
                else if (e instanceof IOException)
                    throw (IOException) e;
                else if (e instanceof RuntimeException)
                    throw (RuntimeException) e;
                else
                    throw new ServletException(e.getMessage(), e);
            }
        } else {
            internalDoFilter(request,response);
        }
    }

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();

                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();

                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {
                throw e;
            } catch (Throwable e) {
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                throw new ServletException(sm.getString("filterChain.filter"), e);
            }
            return;
        }

        // We fell off the end of the chain -- call the servlet instance
        try {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }

            if (request.isAsyncSupported() && !servletSupportsAsync) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                        Boolean.FALSE);
            }
            // Use potentially wrapped request from this point
            if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse) &&
                    Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[]{req, res};
                SecurityUtil.doAsPrivilege("service",
                                           servlet,
                                           classTypeUsedInService,
                                           args,
                                           principal);
            } else {
                servlet.service(request, response);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.servlet"), e);
        } finally {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(null);
                lastServicedResponse.set(null);
            }
        }
    }

可以看到 springSecurityFilterChain 已经注册进来了,第一个就是我们的字符处理过滤器,解决web乱码的问题,一起servlet开发的时候我们就需要手动配置,现在框架在过滤器中帮我们完成了。

SpringSecurity从入门到源码分析_第13张图片

其实 springSecurityFilterChain 本身上是一个 DelegatingFilterProxy,而DelegatingFilterProxy是一个实现了FIlter接口的类。

最终循环到springSecurityFilterChain 的时候就会来到DelegatingFilterProxy这个类,然后调用invokeDelegate()寻找一个

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		// Lazily initialize the delegate if necessary.
		Filter delegateToUse = this.delegate;
		if (delegateToUse == null) {
			synchronized (this.delegateMonitor) {
				delegateToUse = this.delegate;
				if (delegateToUse == null) {
					WebApplicationContext wac = findWebApplicationContext();
					if (wac == null) {
						throw new IllegalStateException("No WebApplicationContext found: " +
								"no ContextLoaderListener or DispatcherServlet registered?");
					}
					delegateToUse = initDelegate(wac);
				}
				this.delegate = delegateToUse;
			}
		}

		// Let the delegate perform the actual doFilter operation.
		invokeDelegate(delegateToUse, request, response, filterChain);
	}

可以看到我们注册在springSecurityFilterChain 的filter都在里面

SpringSecurity从入门到源码分析_第14张图片

然后通过一个虚拟的filterChain进行挨个调用FilterChainProxy,VirtualFilterChain是FilterChainProxy的一个内部类。具体源码如下

	private static class VirtualFilterChain implements FilterChain {
		private final FilterChain originalChain;
		private final List<Filter> additionalFilters;
		private final FirewalledRequest firewalledRequest;
		private final int size;
		private int currentPosition = 0;

		private VirtualFilterChain(FirewalledRequest firewalledRequest,
				FilterChain chain, List<Filter> additionalFilters) {
			this.originalChain = chain;
			this.additionalFilters = additionalFilters;
			this.size = additionalFilters.size();
			this.firewalledRequest = firewalledRequest;
		}

		@Override
		public void doFilter(ServletRequest request, ServletResponse response)
				throws IOException, ServletException {
			if (currentPosition == size) {
				if (logger.isDebugEnabled()) {
					logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
							+ " reached end of additional filter chain; proceeding with original chain");
				}

				// Deactivate path stripping as we exit the security filter chain
				this.firewalledRequest.reset();

				originalChain.doFilter(request, response);
			}
			else {
				currentPosition++;

				Filter nextFilter = additionalFilters.get(currentPosition - 1);

				if (logger.isDebugEnabled()) {
					logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
							+ " at position " + currentPosition + " of " + size
							+ " in additional filter chain; firing Filter: '"
							+ nextFilter.getClass().getSimpleName() + "'");
				}

				nextFilter.doFilter(request, response, this);
			}
		}
	}

可以看到我们的注册的所有spring security filter都在里面,建议大家把断点打在VirtualFilterChain中的doFilter中,这样可以看到security的每个过滤器的执行流程。

SpringSecurity从入门到源码分析_第15张图片

然后开始这个顺序一步一步的执行。直到这些过滤器链都执行完毕,那么整个请求流程也就结束了。

5.用户名和密码登录认证流程分析

首先我们来看下认证流程的一些核心组件。

SecurityContextHolder:

Spring Security身份验证模型的核心是SecurityContextHolder(安全上下文持有者),它包含了SecurityContext(安全上下文)。SecurityContextHolder是 Spring Security 存储已认证用户详细信息的地方。

SpringSecurity从入门到源码分析_第16张图片

可以通过访问SecurityContextHolder希望获取认证过的用户信息:

默认情况下,SecurityContextHolder使用ThreadLocal来存储信息,这意味着SecurityContext始终可用于同一线程中,即使SecurityContext未显式作为参数传递给这些方法。ThreadLocal如果在处理当前主体的请求后注意清除线程,则以这种方式使用是非常安全的。Spring Security 的FilterChainProxy确保SecurityContext始终清除 。

Authentication

public interface Authentication extends Principal, Serializable {
	//获取 授权信息 ,用户权限集合 => 可用于访问受保护资源时的权限验证
	Collection<? extends GrantedAuthority> getAuthorities();
	//凭据 这通常是一个密码
	Object getCredentials();
	// 被认证主体的身份。在带有用户名和密码的身份验证请求的情况下,这将是用户名。
	Object getPrincipal();
	//是否被认证。
	boolean isAuthenticated();
	//认证结果设置。
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

其中Authentication包含:

  • principal- 识别用户。使用用户名/密码进行身份验证时,这通常是UserDetails.

  • credentials- 通常是密码。在许多情况下,这将在用户通过身份验证后清除,以确保它不会泄露。

  • authorities- GrantedAuthoritys是授予用户的高级权限。一些示例是角色或权限值。

GrantedAuthority

GrantedAuthority表示已授权的权限信息,比如角色或权限值。

GrantedAuthority接口只提供了一个方法getAuthority,默认实现类为SimpleGrantedAuthority。

public interface GrantedAuthority extends Serializable {
    String getAuthority();
}

GrantedAuthority可以从该Authentication.getAuthorities()方法中获得。这种方法提供了一个Collection的GrantedAuthority对象。此类权限通常是“角色”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。这些角色稍后会配置为 Web 授权、方法授权和域对象授权。Spring Security 的其他部分能够解释这些权限,并期望它们存在。当使用基于用户名/密码的身份验证时GrantedAuthority,通常由UserDetailsService查询并设置。

AuthenticationManager

AuthenticationManager是定义Security过滤器如何执行身份验证的接口,提供了一个authenticate方法用于认证。

public interface AuthenticationManager {
     /*   authenticate()方法主要做三件事:
	 *   如果验证通过,返回Authentication(通常带上authenticated=true)。
	 *   认证失败抛出AuthenticationException
	 *   如果无法确定,则返回null
	 */
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

AuthenticationManage最常见的实现类是ProviderManager。

ProviderManager

ProviderManager实现了AuthenticationManager接口, 重写了authenticate方法。

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();
         // 遍历实现了 AuthenticationProvider接口的实现类,通 supports(Class authentication)判断是否由他进行处理
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
                //  调用AuthenticationProvider进行认证
				result = provider.authenticate(authentication);

				if (result != null) {
                    //  执行 authentication details 的拷贝逻辑
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
              // 如果发生 AccountStatusException 或 InternalAuthenticationServiceException 异常,
              //  则会通过Spring事件发布器AuthenticationEventPublisher 发布异常事件。
				prepareException(e, authentication);
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
	
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}



		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}

在ProviderManager的authenticate方法中,轮训成员变量List providers。该providers中如果有一个AuthenticationProvider的supports函数返回true,那么就会调用该AuthenticationProvider的authenticate函数认证,如果认证成功则整个认证过程结束。如果不成功,则继续使用下一个合适的AuthenticationProvider进行认证,只要有一个认证成功则为认证成功。

AuthenticationEntryPoint

AuthenticationEntryPoint用于发送从客户端请求凭据的HTTP响应。大概就是发生异常时,可以设置自己的response返回。

在ExceptionTranslationFilter这个过滤器中,如果出现了异常,那么就判断是否是AuthenticationException如果是则调用handleSpringSecurityException(request, response, chain, ase);进行处理。

例如自定义实现AuthenticationEntryPoint这个接口

@Slf4j
@Component
public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)  {
        String message = StringUtils.isBlank(authException.getMessage()) ?"匿名用户没有权限访问该功能":authException.getMessage();
        ResponseUtil.response(response, ResponseUtil.error(message));
    }


}

// 在配置类中添加AuthenticationEntryPointhandler
// 匿名用户访问失败处理器 (抛出的异常需要在ExceptionTranslationFilter过滤器后面他才能处理)
 http.exceptionHandling().authenticationEntryPoint(authenticationEntryPointHandler);

AbstractAuthenticationProcessingFilter

这是定义了认证模板的一个filter,比如我们的密码认证过滤器UsernamePasswordAuthenticationFilter就是继承了他。

主要在该过滤器中定义了,认证,认证成功,认证失败,策略处理的操作

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
             // 失败处理 
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// 失败处理
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
          // 成功处理
		successfulAuthentication(request, response, chain, authResult);
	}

现在我们再来梳理一遍整个认证过程。

1.请求来到 OncePerRequestFilter 然后执行到 springSecurityFilterChain
2. security第一个过滤器就是WebAsyncManagerIntegrationFilter
主要是使用SecurityContextRepository在session中保存或更新一个
SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
3.来到 AbstractAuthenticationProcessingFilter,使用 attemptAuthentication(request, response);进行认证
 4. 来到我们继承了UsernamePasswordAuthenticationFilter的子类进行认证 然后我们使用ProviderManager.authenticate(authenticationToken)进行认证 ProviderManager
 5. 循环所有的ProviderManager调用supports(Class<?> authentication)进行判断是否可以进行处理跟传入的Authentication有关
    6.来到AbstractUserDetailsAuthenticationProvider 的authenticate方法  通过this.getUserDetailsService().loadUserByUsername(username)获取到UserDetails对象
    6.0 调用子类DaoAuthenticationProvideradditionalAuthenticationChecks()方法检密码检测
    6.1 调用check()方法检测用户是否锁定,过期,禁用
    6.3 postAuthenticationChecks.check(user);判断证书是否过期
    6.4 认证成功就创建一个UsernamePasswordAuthenticationToken。里面包含用户信息,用户权限等内容
   7.回到AbstractAuthenticationProcessingFilter 调用successfulAuthentication成功回调
    如果失败就调用 unsuccessfulAuthentication(request, response, failed); 我们自定义成功的返回值,比如成功就返回token信息等
  

SpringSecurity从入门到源码分析_第17张图片

6.手机号登录认证分析

实现手机登录,首先需要用到如下几个核心类:

AuthenticationProvider: 认证者

  • authenticate(): 核心认证方法
  • supports():判断该token是否可以被这个Provider所处理

AbstractAuthenticationToken: 认证标识,里面可以是需要被认证所校验的内容,如手机号,验证码等内容,已经认证后的相关信息

AbstractAuthenticationProcessingFilter:基于浏览器的一个抽象认证过滤器,我们可以再此处封装好认证的信息,以及参数合法性判断,通过构造指定处理的请求地址和请求方式

  • attemptAuthentication():核心认证token,返回一个AbstractAuthenticationToken
  • requiresAuthentication():判断当前请求是否可以被认证

SecurityConfigurerAdapter:Security核心配置类,可以配置多个,然后实现多种登录方式

1.根据上面4.4分析的请求来到springSecurityFilterChain ,然后执行到VirtualFilterChain的doFilter()方法,然后挨个执行过滤器。

2.如果是AbstractAuthenticationProcessingFilter的子类filter首先会判断该请求是否可以被当前filter所处理。如图所示:

SpringSecurity从入门到源码分析_第18张图片

3.经过所有filter的处理,发现没一个能处理的,然后来到我们自定义的MobileAuthenticationFilter,他继承AbstractAuthenticationProcessingFilter,判断是否能处理的标准就是我们自定义的MobileAuthenticationFilter类,调用他的父类构造方法,指定处理的请求地址和请求方式。

4.经过第三步的验证,发现可以处理,那么就调用子类的 attemptAuthentication(request, response)方法进行处理,然后我们在自定义的filter中进行参数的验证,验证完毕后封装一个自定义的 MobileAuthenticationToken,他是AbstractAuthenticationToken的子类,我们自定义的这个token其实是一个Authentication类型,因为AbstractAuthenticationToken实现了Authentication。

5.调用 this.getAuthenticationManager()进行认证,可以看到我们的认证者有3个,分别是匿名用户,微信登录认证,手机号登录认证。判断是由那个认证者处理的关键就是传入我们继承了AbstractAuthenticationToken的子类。因为我们自定义实现AuthenticationProvider接口认证者是有supports()方法的。接下来的流程就和用户名和密码登录处理一样了。

SpringSecurity从入门到源码分析_第19张图片

微信登录我们这里就不在进行源码分析了。是跟手机号登录差不多的一个原理。我接下来就给大家画一张图,方便大家理解。

SpringSecurity从入门到源码分析_第20张图片

7.权限校验流程分析

我设计的一个权限规则,因为这是前台的系统。规则比较简单,如果是后台系统规则稍微复杂,后面会写一个后台权限系统,敬请期待,规则如下:

  • /api/**: CONSUMER角色才能访问,主要是提供给第三方调用
  • /person/**:PERSON角色才能访问。主要是针对于已经登录的个人用户
  • /unit/**:UNIT角色才能访问,表示已经登录的单位用户
  • /public/**,/common/**,/download/**,/upload/**, /error/**,/weChat/**,/user/**:针对于这些接口,表示不需要登录,可直接访问。VISITOR表示匿名用户,因为我自定义了一个匿名用户filter,我不喜欢security的匿名用户规则,所以自定义的。

经过前面的分析,相信大家对于security已经有了初步的了解,知道了运行流程,以及如何对用户信息进行认证,既然用户已经认证过了,如何判断他是否有权限访问我们对于的系统资源,这就是接下来我们要分析的内容。

首先我们来看一下核心接口和类:

ConfigAttribute:是一个接口,其作用为存储相关访问控制的规则。

public interface ConfigAttribute extends Serializable {
	String getAttribute();
}

而这些访问规则可以通过配置类或者注解方式来配置。比如HttpSecurity的默认配置为所有的请求都需要被认证,也可以添加自定义的访问控制规则。

protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests((requests) -> {
            ((AuthorizedUrl)requests.anyRequest()).authenticated();
        });
        http.formLogin();
        http.httpBasic();
    }

比如可以通过注解方式进行访问控制:

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")

WebExpressionConfigAttribute是ConfigAttribute的常用实现类,并实现了EvaluationContextPostProcessor接口,他的主要作用是存储我们配置的表达式及其处理器。

/**
 * Simple expression configuration attribute for use in web request authorizations.
 *
 * @author Luke Taylor
 * @since 3.0
 * 用于 Web 请求授权的简单表达式配置属性。
 */
class WebExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<FilterInvocation> {

	// 表达式 比如
	private final Expression authorizeExpression;
	// el表达式内容处理处理器
	private final EvaluationContextPostProcessor<FilterInvocation> postProcessor;

	WebExpressionConfigAttribute(Expression authorizeExpression,
			EvaluationContextPostProcessor<FilterInvocation> postProcessor) {
		this.authorizeExpression = authorizeExpression;
		this.postProcessor = postProcessor;
	}

FilterInvocation: FilterInvocation类,主要是保存与 HTTP 过滤器关联的对象,包括请求、响应、连接器链。

public class FilterInvocation {

	static final FilterChain DUMMY_CHAIN = (req, res) -> {
		throw new UnsupportedOperationException("Dummy filter chain");
	};

	private FilterChain chain;

	private HttpServletRequest request;

	private HttpServletResponse response;

	public FilterInvocation(ServletRequest request, ServletResponse response, FilterChain chain) {
		Assert.isTrue(request != null && response != null && chain != null, "Cannot pass null values to constructor");
		this.request = (HttpServletRequest) request;
		this.response = (HttpServletResponse) response;
		this.chain = chain;
	}
}

InterceptorStatusToken:InterceptorStatusToken是FilterSecurityInterceptor拦截器beforeInvocation处理返回的对象,主要包含了反映了安全拦截的状态,最终afterInvocation方法会对他进行最终的处理。

public class InterceptorStatusToken {

	private SecurityContext securityContext;

	private Collection<ConfigAttribute> attr;

	private Object secureObject;

	private boolean contextHolderRefreshRequired;

	public InterceptorStatusToken(SecurityContext securityContext, boolean contextHolderRefreshRequired,
			Collection<ConfigAttribute> attributes, Object secureObject) {
		this.securityContext = securityContext;
		this.contextHolderRefreshRequired = contextHolderRefreshRequired;
		this.attr = attributes;
		this.secureObject = secureObject;
	}
}

RunAsManager: RunAsManager是一个接口,主要定义了对Authentication进行替换的方法。在极少数情况下,用户可以使用不同的Authentication替换SecurityContext中的Authentication。

public interface RunAsManager {
	Authentication buildRunAs(Authentication authentication, Object object, Collection<ConfigAttribute> attributes);
	boolean supports(ConfigAttribute attribute);
	boolean supports(Class<?> clazz);
}

AccessDecisionManager:是访问决策管理器,做出最终的访问控制(授权)决定,他是一个接口,有众多的实现类。每一个具体的实现类可以表示为一种决策。

image-20230321205449274

ConsensusBased:AccessDecisionManager的实现这之一,少数服从多数授权访问决策方案,那就是授予权限和拒绝权限相等时的逻辑。其实,该决策器也考虑到了这一点,所以提供了 allowIfEqualGrantedDeniedDecisions 参数,用于给用户提供自定义的机会,其默认值为 true,即代表允许授予权限和拒绝权限相等,且同时也代表授予访问权限。

UnanimousBased :最严格的的授权决策器。要求所有 AccessDecisionVoter 均返回肯定的结果时,才代表授予权限。

AccessDecisionManager:只要任一 AccessDecisionVoter 返回肯定的结果,便授予访问权限。

先了解这三个决策管理器,混个眼熟,后面流程分析的时候会一一继续解析。

AbstractSecurityInterceptor:在FilterSecurityInterceptor中,会调用父类的beforeInvocation(filterInvocation)方法进行处理,最终返回一个InterceptorStatusToken对象,它就是spring security处理鉴权的入口。

	protected InterceptorStatusToken beforeInvocation(Object object) {
		Assert.notNull(object, "Object was null");
		// 1. 判断object是不是FilterInvocation
		if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
			throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName()
					+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
					+ getSecureObjectClass());
		}
		// 2. 获取配置的访问控制规则 any request =》authenticated ,没有配置,return null
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
		if (CollectionUtils.isEmpty(attributes)) {
			Assert.isTrue(!this.rejectPublicInvocations,
					() -> "Secure object invocation " + object
							+ " was denied as public invocations are not allowed via this interceptor. "
							+ "This indicates a configuration error because the "
							+ "rejectPublicInvocations property is set to 'true'");
			if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Authorized public object %s", object));
			}
			publishEvent(new PublicInvocationEvent(object));
			return null; // no further work post-invocation
		}
		// 3. 判断认证对象Authentication是否为null
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
					"An Authentication object was not found in the SecurityContext"), object, attributes);
		}
		// 4. 获取Authentication对象
		Authentication authenticated = authenticateIfRequired();
		if (this.logger.isTraceEnabled()) {
			this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
		}
		// Attempt authorization
		// 5. 进行授权判断
		attemptAuthorization(object, attributes, authenticated);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
		}
		// 6. 发布授权成功
		if (this.publishAuthorizationSuccess) {
			publishEvent(new AuthorizedEvent(object, attributes, authenticated));
		}

		// Attempt to run as a different user
		// 7. 对Authentication进行再处理,这里没有处理,直接返回null
		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
		if (runAs != null) {
			SecurityContext origCtx = SecurityContextHolder.getContext();
			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
			SecurityContextHolder.getContext().setAuthentication(runAs);

			if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
			}
			// need to revert to token.Authenticated post-invocation
			return new InterceptorStatusToken(origCtx, true, attributes, object);
		}
		this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
		// no further work post-invocation
		// 8. 返回InterceptorStatusToken
		return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);

	}

在beforeInvocation方法中的核心方法为attemptAuthorization,它会调用授权管理器进行决策,当失败发生异常时,会抛出异常。

	/**
	 * 授权判断
	 *
	 * @param object        filter invocation [GET /test]
	 * @param attributes 配置的URL放行、需要验证路径等配置
	 * @param authenticated 认证对象
	 */
	private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
			Authentication authenticated) {
		try {
			// 1. 调用授权管理器进行决策
			this.accessDecisionManager.decide(authenticated, object, attributes);
		} catch (AccessDeniedException ex) {
			// 2. 访问被拒绝。抛出AccessDeniedException异常
			if (this.logger.isTraceEnabled()) {
				this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,
						attributes, this.accessDecisionManager));
			} else if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
			}
			// 3. 发布授权失败事件
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
			throw ex;
		}
	}

首先来看下我们的security链中都有哪些过滤器链,大部分的过滤器链我们都见过了,上面都分析过了,现在我们从ExceptionTranslationFilter开始分析。

SpringSecurity从入门到源码分析_第21张图片

7.1 ExceptionTranslationFilter

ExceptionTranslationFilter我们已经知道,这是一个异常处理过滤器,也就是我们在授权的过程中如果出现异常,就会被他捕捉到,然后交给AuthenticationEntryPoint来进行处理。

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
			// 1. 请求直接放行
			chain.doFilter(request, response);
		} catch (IOException ex) {
			throw ex;
		} catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			// 2. 捕获后续出现的异常
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			if (securityException == null) {
				securityException = (AccessDeniedException) this.throwableAnalyzer
						.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
			if (securityException == null) {
				rethrow(ex);
			}
			if (response.isCommitted()) {
				throw new ServletException("Unable to handle the Spring Security Exception "
						+ "because the response is already committed.", ex);
			}
			// 3. 处理发生的异常
			handleSpringSecurityException(request, response, chain, securityException);
		}
	}


7.2 TokenAuthenticationFilter

首先我们来看下我们自定义的TokenAuthenticFIlter都做了什么?

/**
 * token 拦截过滤器,主要用于处理用户权限
 * @author huyu
 * @date 2023-03-12
 * @since 1.0
 **/
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private SysUserDetailsService sysUserDetailsService;

    private static final String TOKEN_NAME = "safety-token";

    private AuthenticationURLConfig authenticationURLConfig;

    public TokenAuthenticationFilter(AuthenticationURLConfig authenticationURLConfig,SysUserDetailsService sysUserDetailsService){
        this.authenticationURLConfig = authenticationURLConfig;
        this.sysUserDetailsService = sysUserDetailsService;
    }




    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        //如果是登录接口,直接放行
        if (authenticationURLConfig.getLoginPath().equals(requestURI)) {
            chain.doFilter(request, response);
            return;
        }

        if (getAuthentication(request, requestURI) != null) {
            chain.doFilter(request, response);
        } else {
            ResponseUtil.response(response, ResponseUtil.error("未登录"));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, String requestURI) throws AuthenticationException {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isBlank(token)) {
            token = request.getParameter(TOKEN_NAME);
        }
        //  检测该接口是否可以直接放行
        boolean isNeedAuthToken = false;
        PathMatcher matcher = new AntPathMatcher();
        List<String> publicUrlList = authenticationURLConfig.getPublicUrlList();
        for (String publicURI : publicUrlList) {
            if (matcher.match(publicURI, requestURI)) {
                isNeedAuthToken = true;
                break;
            }
        }

        if (isNeedAuthToken){
            // 对于不需要token校验的接口,创建一个匿名用户
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authenticationURLConfig.getAnonymityUserName(), request, AuthorityUtils.createAuthorityList(authenticationURLConfig.getAnonymityUserRule()));
            authenticationToken.setDetails(request);
            // 将权限信息设置到全局holder
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            return  authenticationToken;
        }

        // 接下来就是需要进行权限判断
        if (StringUtils.isBlank(token)) {
            throw  BusinessExceptionDefinition.TOKEN_EMPTY;
        }

        String username = JwtHelper.getUsername(token);
        UserDetails userDetails = sysUserDetailsService.loadUserByUsername(username);
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities);
        authenticationToken.setDetails(userDetails);
        // 将用户权限信息设置到全局holder
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        return authenticationToken;
    }
}

做的事情很简单,判断那些接口需要授权,那些接口需要直接放行,然后去数据库查询出用户对应的权限信息,封装到全局的SecurityContextHolder中。

7.3 FilterSecurityInterceptor

接下来进入FilterSecurityInterceptor过滤器,他的doFilter方法,首先把请求封装成FilterInvocation,把过滤器链和请求关联起来。,调用的是自身的invoke(FilterInvocation filterInvocation)方法。该方法完成了整个访问控制逻辑。

 public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
       //  将请求,响应,过滤器链关联起来
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}


  /**
	 *  doFilter实际执行的方法
	 * @param filterInvocation 封装了request response 过滤器链的对象
	 */
	public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
		// 1. 如果已经执行过该过滤器,直接放行
		if (isApplied(filterInvocation) && this.observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
			return;
		}
		// first time this request being called, so perform security checking
		// 2. 第一次调用这个请求,所以执行安全检查
		if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
			// 3. 在request中添加__spring_security_filterSecurityInterceptor_filterApplied = true,表示执行了该过滤器
			filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
		// 4. 前置访问控制处理
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
		try {
			// 5. 放行	
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
		} finally {
			super.finallyInvocation(token);
		}
		// 6. 后置处理
		super.afterInvocation(token, null);
	}

7.4 AbstractSecurityInterceptor

在FilterSecurityInterceptor中,会调用父类的beforeInvocation(filterInvocation)方法进行处理,最终返回一个InterceptorStatusToken对象,它就是spring security处理鉴权的入口。

	protected InterceptorStatusToken beforeInvocation(Object object) {
		Assert.notNull(object, "Object was null");
		final boolean debug = logger.isDebugEnabled();
        // 1. 判断object是不是FilterInvocation
		if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
			throw new IllegalArgumentException(
					"Security invocation attempted for object "
							+ object.getClass().getName()
							+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
							+ getSecureObjectClass());
		}
        // 2. 获取配置的访问控制规则 any request =》authenticated ,没有配置,return null
        // 比如我们访问的是:/unit/hello,就会解析成 hasAuthority('UNIT')
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
         // 如果解析的结果为null也会抛出异常
		if (attributes == null || attributes.isEmpty()) {
			if (rejectPublicInvocations) {
				throw new IllegalArgumentException(
						"Secure object invocation "
								+ object
								+ " was denied as public invocations are not allowed via this interceptor. "
								+ "This indicates a configuration error because the "
								+ "rejectPublicInvocations property is set to 'true'");
			}

			if (debug) {
				logger.debug("Public object - authentication not attempted");
			}
             
			publishEvent(new PublicInvocationEvent(object));

			return null; // no further work post-invocation
		}

		if (debug) {
			logger.debug("Secure object: " + object + "; Attributes: " + attributes);
		}
	    // 3. 判断认证对象Authentication是否为null
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(messages.getMessage(
					"AbstractSecurityInterceptor.authenticationNotFound",
					"An Authentication object was not found in the SecurityContext"),
					object, attributes);
		}
       // 4. 获取Authentication对象,如果认证完直接从 SecurityContextHolder 中去取
        // 没有认证则去认证,并且把认证结果放到SecurityContextHolder
		Authentication authenticated = authenticateIfRequired();

		// Attempt authorization
        // 5.进行授权判断,如果中途出现AccessDeniedException异常,则抛出
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}

		if (debug) {
			logger.debug("Authorization successful");
		}
        // 6.发布认证成功的信息
		if (publishAuthorizationSuccess) {
			publishEvent(new AuthorizedEvent(object, attributes, authenticated));
		}

		// Attempt to run as a different user
		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
				attributes);

		if (runAs == null) {
			if (debug) {
				logger.debug("RunAsManager did not change Authentication object");
			}

			// no further work post-invocation
			return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
					attributes, object);
		}
		else {
			if (debug) {
				logger.debug("Switching to RunAs Authentication: " + runAs);
			}
               
			SecurityContext origCtx = SecurityContextHolder.getContext();
			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
			SecurityContextHolder.getContext().setAuthentication(runAs);

			// need to revert to token.Authenticated post-invocation
			return new InterceptorStatusToken(origCtx, true, attributes, object);
		}
	}

通过this.accessDecisionManager.decide()方法的调用就来到了 UnanimousBased ,decide()方法内容如下所示:

getDecisionVoters(),只有2个,一个是我们自定义,一个是web表达式解析,也就是我们在http对象中配置的。

image-20230321213341220

	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> attributes) throws AccessDeniedException {

		int grant = 0;
         // 创建一个单属性list
		List<ConfigAttribute> singleAttributeList = new ArrayList<>(1);
		singleAttributeList.add(null);
         // 每次循环把第0个元素设置为attributes
		for (ConfigAttribute attribute : attributes) {
			singleAttributeList.set(0, attribute);
              //  遍历所有的决策管理器,然后进行投票
			for (AccessDecisionVoter voter : getDecisionVoters()) {
                 // 调用具体决策管理器进行投票
				int result = voter.vote(authentication, object, singleAttributeList);

				if (logger.isDebugEnabled()) {
					logger.debug("Voter: " + voter + ", returned: " + result);
				}

				switch (result) {
				case AccessDecisionVoter.ACCESS_GRANTED:
					grant++;

					break;

				case AccessDecisionVoter.ACCESS_DENIED:
					throw new AccessDeniedException(messages.getMessage(
							"AbstractAccessDecisionManager.accessDenied",
							"Access is denied"));

				default:
					break;
				}
			}
		}

		// To get this far, there were no deny votes
		if (grant > 0) {
			return;
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}

7.5 WebExpressionVoter

// int ACCESS_GRANTED = 1; 允许
// int ACCESS_ABSTAIN = 0; 弃权 
// int ACCESS_DENIED = -1; 拒绝
public int vote(Authentication authentication, FilterInvocation fi,
			Collection<ConfigAttribute> attributes) {
		assert authentication != null;
		assert fi != null;
		assert attributes != null;
         // 解析角色表达式,以及匹配的路径表达式
		WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

		if (weca == null) {
			return ACCESS_ABSTAIN;
		}

		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				fi);
		ctx = weca.postProcess(ctx, fi);
        // 判断有没有权限 ,如果返回的是-1,那么直接就抛出异常了,因为我们使用的UnanimousBased 这个决策管理器
		return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
				: ACCESS_DENIED;
	}

假设WebExpressionVoter匹配成功,还会来到我们自定义的AccessDecisionProcessorHandler,如果他返回的是-1那么也是授权失败的。所以我们这个决策管理器可以动态的去查询数据库,然后决定是否有访问权限,做一个兜底的决策者。

public class AccessDecisionProcessorHandler implements AccessDecisionVoter<FilterInvocation> {

    /**
     *
     * WebExpressionVoter通过后如果此类不通过也是无法访问,可以再此处进行动态权限判断
     * @return int 1:赞成,0:弃权,-1:否决
     * @author huyu
     * @date 2023/3/15 23:47
     * @since 1.0.0
     **/
    @Override
    public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {
        // 拿到当前请求uri
        String requestUrl = object.getRequestUrl();
        String method = object.getRequest().getMethod();
        log.debug("进入自定义鉴权投票器,URI : {} {}", method, requestUrl);
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            String limitsOfAuthority = authority.getAuthority();
            log.info("limitsOfAuthority:{}", limitsOfAuthority);
        }
        return ACCESS_GRANTED;
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

如果中途出现异常,会被ExceptionTranslationFilter处理,然后捕捉到异常,发布出去,仍然后最终调用handleSpringSecurityException()方法进行异常处理。

	private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			logger.debug(
					"Authentication exception occurred; redirecting to authentication entry point",
					exception);

			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            // 如果是匿名用户就抛出匿名用户的异常
			if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
				logger.debug(
						"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
						exception);

				sendStartAuthentication(
						request,
						response,
						chain,
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));
			}
			else {
				logger.debug(
						"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
						exception);
                 // 默认是AccessDeniedHandlerImpl,如果我们配置了自己的handler则会用我们自己的
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}
	}

好的至此,认证流程结束,希望大家有有所收益。

整个security代码源码gitee地址:https://gitee.com/cafu_ankang/security.git

关于security学习的这个文档有md的,我也放在项目的doc目录下了,有需要的朋友可以直接预览。t_user的SQL也在doc目录下。

后面会写一个security复杂的后台管理系统授权,包括菜单,还有接口地址动态授权,敬请期待,码子不易,如果对你有所帮助,给个赞支持一下,谢谢。看一百次,不如自己动手debug一次。

你可能感兴趣的:(java后端,spring,boot,java,security)