首页 技术 正文
技术 2022年11月15日
0 收藏 948 点赞 4,642 浏览 32074 个字

一、认证的两种方式的介绍

1. 基于Session的认证方式

在之前的单体架构时代,我们认证成功之后都会将信息存入到Session中,然后响应给客户端的是对应的Session中数据的key,客户端会将这个key存储在cookie中,之后都请求都会携带这个cookie中的信息,结构图如下

但是随着技术的更新迭代,我们在项目架构的时候更多的情况下会选择前后端分离或者分布式架构,那么在这种情况下基于session的认证方式就显露了很多的不足,列举几个明显的特点:

  1. cookie存储的内容有限制4k
  2. cookie的有效范围是当前域名下,所以在分布式环境下或者前后端分离的项目中都不适用,及时要用也会很麻烦
  3. 服务端存储了所有认证过的用户信息

2.基于Token的认证方式

相较于Session对需求的兼容,基于Token的方式便是我们在当下项目中处理认证和授权的实现方式的首先了,Token的方式其实就是在用户认证成功后便把用户信息通过加密封装到了Token中,在响应客户端的时候会将Token信息传回给客户端,当下一次请求到来的时候在请求的Http请求的head的Authentication中会携带token

二、SSO和OAuth2.0流程浅析

前面介绍了下认证信息的实现方式,接下来看下我们在分布式环境下会经常碰到的两种解决方案SSO和OAuth2.0

1.SSO

SSO也就是我们经常听到的单点登录,是我们在分布式环境下认证实现的解决方案,具体流程如下

2.OAuth2.0

单点登录解决的分布式系统中统一认证的问题,还有一种情况是一个新的系统用户就需要去注册一个账号,用户管理的账号越多越麻烦,为了解决这个问题,当前系统就期望使用你的其他系统的资料来作为认证的信息,比如 微信,QQ,微博等,这时候就该OAuth2.0,实现流程如下

三、SpringSecurity介绍

1.基础环境准备

    我们要深入的研究SpringSecurity的原理,那么我们首先需要搭建这样的一个环境,在现在的实际开发中使用SpringBoot来构建项目的占比越来越高,所以我们应该要通过SpringBoot来研究SpringSecurity的原理,但是SpringSecurity的封装比较厉害,所以对于源码的分析会不是很友好,所以我们会先基于XML构架的方式来介绍,然后再在SpringBoot项目中来研究。 

1.1基于XML文件的方式构建项目

     我们通过Spring+SpringMVC+SpringSecurity来构建基于XML的项目

pom.xml

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId>
<artifactId>SpringSecurityDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging> <name>GpSpringSecurityDemo Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties> <dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
</dependencies> <build>
<finalName>GpSpringSecurityDemo</finalName>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port> <!-- 访问端口-->
<path>/</path> <!-- 访问路径-->
</configuration>
</plugin>
</plugins>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

 WEB.XML

<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app version="2.5" id="WebApp_ID" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>Archetype Created Web Application</display-name> <!-- 初始化spring容器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener> <!-- post乱码过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 前端控制器 -->
<servlet>
<servlet-name>dispatcherServletb</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- contextConfigLocation不是必须的, 如果不配置contextConfigLocation,
springmvc的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServletb</servlet-name>
<!-- 拦截所有请求jsp除外 -->
<url-pattern>/</url-pattern>
</servlet-mapping> <!-- 配置过滤器链 springSecurityFilterChain名称固定-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping></web-app>

login.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>登录管理</h1><form action="/login" method="post">
账号:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录"><br>
</form>
</body>
</html>

springsecurity.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd"> <!--
auto-config="true" 表示自动加载SpringSecurity的配置文件
use-expressions="true" 使用Spring的EL表达式
-->
<security:http auto-config="true" use-expressions="true"> <security:intercept-url pattern="/login.jsp" access="permitAll()"></security:intercept-url>
<!--<security:intercept-url pattern="/login.do" access="permitAll()"></security:intercept-url>--> <!--
拦截资源
pattern="/**" 拦截所有的资源
access="hasAnyRole('role1')" 表示只有role1这个角色可以访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"></security:intercept-url> <!--
配置认证信息
login-page="/login.jsp" 自定义的登录页面
login-processing-url="/login" security中处理登录的请求
default-target-url="/home.jsp" 默认的跳转地址
authentication-failure-url="/failure.jsp" 登录失败的跳转地址 <security:form-login
login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/home.jsp"
authentication-failure-url="/failure.jsp"
/>-->
<!-- 配置退出的登录信息
<security:logout logout-url="/logout"
logout-success-url="/login.jsp" />
<security:csrf disabled="true"/>-->
</security:http> <!-- 设置认证用户来源 noop:SpringSecurity中默认 密码验证是要加密的 noop表示不加密 -->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="zhang" password="{noop}123" authorities="ROLE_USER"></security:user>
<security:user name="lisi" password="{noop}123" authorities="ROLE_ADMIN"></security:user>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager></beans>

springmvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd"> <!-- 配置扫描路径-->
<context:component-scan base-package="com.ghy.security.controller"
use-default-filters="false">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
</beans>
log4j.properties
log4j.rootCategory=debug, stdoutlog4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[GP] %p [%t] %C.%M(%L) | %m%n

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd"> <import resource="classpath:springsecurity.xml"/></beans>

  

 2.原理分析

      首先启动我们构建的项目然后访问 http://localhost:8080/index.html 因为访问的是非匿名访问的页面,所以会直接调整到系统默认的登陆界面,那么如果我们要使用自定义的登陆界面应该怎么去实现?还有我们要探究下进入默认登陆界面前整个SpringSecurity到底做了什么事情?所以我们可以从下面这三方面来深入的研究

  1. 系统启动springSecurity做了啥
  2. 页面是怎么出来的
  3. 认证流程是怎么实现的

想要搞清楚上面问题,首先我们要请求一个web请求的基本流程如下

 

请求从客户端发起(例如浏览器),然后穿过层层 Filter,最终来到 Servlet 上,被 Servlet 所处理。那么权限管理框架在设计的时候肯定也是基于Filter来实现的。那么SpringSecurity的处理结构应该是这样的

有了这个基本的认知后我们就可以来具体的分析SpringSecurity的核心原理了,为了便于大家的理解,我们先从客户端发送请求开始到显示登陆界面这条流程开始来分析。

  2.1.第一次请求的流程梳理

       基于XML的方式分析DelegatingFilterProxy

 当我们在浏览器地址栏发送http://localhost:8080/index.html的时候在服务端会被 web.xml 中配置的DelegatingFilterProxy这个过滤器拦截

 

  <!-- 配置过滤器链 springSecurityFilterChain名称固定-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

 所以请求的流程图应该是这样的

     但是要注意 DelegatingFilterProxy 并不是SpringSecurity提供给我们的哦,而是Spring框架之前就提供的有的,DelegatingFilterProxy就是一个对于servlet filter的代理,用这个类的好处主要是通过Spring容器来管理servlet filter的生命周期,还有就是如果filter中需要一些Spring容器的实例,可以通过spring直接注入,另外读取一些配置文件这些便利的操作都可以通过Spring来配置实现。DelegatingFilterProxy继承自GenericFilterBean,在Servlet容器启动的时候回执行GenericFilterBean的init方法,

 

  public final void init(FilterConfig filterConfig) throws ServletException {
Assert.notNull(filterConfig, "FilterConfig must not be null");
this.filterConfig = filterConfig;
PropertyValues pvs = new GenericFilterBean.FilterConfigPropertyValues(filterConfig, this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
Environment env = this.environment;
if (env == null) {
env = new StandardServletEnvironment();
} bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, (PropertyResolver)env));
this.initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
} catch (BeansException var6) {
String msg = "Failed to set bean properties on filter '" + filterConfig.getFilterName() + "': " + var6.getMessage();
this.logger.error(msg, var6);
throw new NestedServletException(msg, var6);
}
}

                     // 另外在init方法中调用了initFilterBean()方法,该方法是GenericFilterBean类是特地留给子类扩展用的 

        this.initFilterBean();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use");
} }

  

DelegatingFilterProxy的initFilterBean方法

 protected void initFilterBean() throws ServletException {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
if (this.targetBeanName == null) {

                                         // 获取的是在web.xml文件中设置的DelegatingFilterProxy过滤器对应的                                          // filterName【springSecurityFilterChain】 

                    this.targetBeanName = this.getFilterName();
}

                                     // 获取IoC容器对象 

                WebApplicationContext wac = this.findWebApplicationContext();
if (wac != null) {

                                            // 获取委托处理请求的过滤器,在此处获取的是 FilterChainProxy 

                    this.delegate = this.initDelegate(wac);
}
} }
}

上面的方法首先获取在web.xml中配置的FilterName的值也就是 springSecurityFilterChain,然后再获取Spring的IoC容器对象,如果容器对象不为空,然后执行this.initDelegate(wac);方法

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {

                    // 获取的值为 springSecurityFilterChain 

        String targetBeanName = this.getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");

                    // 从SpringIoC容器中根据 springSecurityFilterChain 这个名称获取bean对象                   // 所以在web.xml中我们配置的FilterName必须得是springSecurityFilterChain否则的话是获取不到的

        Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
if (this.isTargetFilterLifecycle()) {

                   // 如果有设置targetFilterLifecycle为true就会执行委托过滤器的init方法 

            delegate.init(this.getFilterConfig());
} return delegate;
}

  

通过上面这个流程init()初始化过程就结束了,通过上面源码的解析我们能够发现DelegatingFilterProxy这个过滤器在初始的时候从Spring容器中获取了 FilterChainProxy 这个过滤器链的代理对象,并且把这个对象保存在了DelegatingFilterProxy的delegate属性中。那么当请求到来的时候会执行DelegatingFilterProxy的doFilter方法,那么我们就可以来看下这个方法里面又执行了什么

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Filter delegateToUse = this.delegate;

                   // if中的代码已经在初始化阶段完成了 

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

                 // 核心流程就是该代码 

        this.invokeDelegate(delegateToUse, request, response, filterChain);
}

  invokeDelegate是doFilter中的核心代码,字面含义就是调用委托对象。从具体源码来看也确实如此

protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {

             // 调用FilterChainProxy的doFilter方法

     delegate.doFilter(request, response, filterChain);
}

  通过上面的代码分析我们发现,当一个请求到来的时候先通过DelegatingFilterProxy来拦截,但是DelegatingFilterProxy不处理具体的逻辑,而是将具体的处理操作交给了delegate过滤器来处理也就是FilterChainProxy来处理。

     FilterChainProxy分析        通过上面的分析我们也能发现FilterChainProxy在整个请求过程中的位置了,如下     

至于FilterChainProxy怎么来的会在介绍系统初始化的时候会介绍到这块儿的内容的。对于FilterChainProxy怎么处理请求的,根据上面的内容我们知道我们可以直接看doFilter方法即可

 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
this.doFilterInternal(request, response, chain);
} finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
} else {
//跟进
this.doFilterInternal(request, response, chain);
} }

 

 private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

                      // 对request对象做了一次防火墙检查,校验提交的方式是否合法【post,get等】                     // 校验请求的地址是否在黑名单中 

        FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);

                   // 获取处理这个请求的过滤器链中的所有的过滤器

        List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
if (filters != null && filters.size() != 0) {

                             // 如果有过滤器则构建一个虚拟的过滤器链路,然后执行 

            FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
} else {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list"));
} fwRequest.reset();

                           // 如果没有要执行的过滤器 则通过外部的过滤器链执行下一个servlet 过滤器来处理 

            chain.doFilter(fwRequest, fwResponse);
}
}

 上面方法中的核心代码第一个是this.getFilters((HttpServletRequest)fwRequest);在这个方法要注意一个概念就是在SpringSecurity中可以存在多个过滤器链,而每个过滤器链又可以包含多个过滤器

 

 

那么在第一次访问到来的时候会选择哪个过滤器链然后又包含哪些具体的过滤器呢?

可以看到此处有15个过滤器。 通过vfc.doFilter(fwRequest, fwResponse);我们可以发现是被这15过滤器按照固定先后顺序来执行的。

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

                                    // size=15 currentPosition会从0~15 一个个的取出过滤器 

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

                            // 调用取出来的过滤器的doFilter方法,此处有用到设计模式中的责任链模式 

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

 

ExceptionTranslationFilter 

 在整个过滤器链中,ExceptionTranslationFilter是倒数第二个执行的过滤器,它的作用是通过catch处理下一个Filter【也就是FilterSecurityInterceptor】或应用逻辑产生的异常

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res; try {

                             // 进入下一个过滤器 FilterSecurityInterceptor中 

            chain.doFilter(request, response);
this.logger.debug("Chain processed normally");
} catch (IOException var9) {
throw var9;
} catch (Exception var10) {

                             // FilterSecurityInterceptor有显示的抛出异常 就处理 

            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
} if (ase == null) {
if (var10 instanceof ServletException) {
throw (ServletException)var10;
} if (var10 instanceof RuntimeException) {
throw (RuntimeException)var10;
} throw new RuntimeException(var10);
} if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10);
} this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
} }

  

handleSpringSecurityException返回就是处理下一个Filter抛出的异常

private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
this.logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception);
this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception);
} else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) {
this.logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception);
this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
} else {
this.logger.debug("Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception);
this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
}
} }

关键方法:sendStartAuthentication 

  

  protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication((Authentication)null);

                    // 保存本次请求的request信息,当认证成功会跳转到这个请求的地址 

        this.requestCache.saveRequest(request, response);
this.logger.debug("Calling Authentication entry point.");
this.authenticationEntryPoint.commence(request, response, reason);
}

AuthenticationEntryPoint【认证入口点】中的commence方法 

  

public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
if (this.useForward) {
if (this.forceHttps && "http".equals(request.getScheme())) {
redirectUrl = this.buildHttpsRedirectUrlForRequest(request);
} if (redirectUrl == null) {
String loginForm = this.determineUrlToUseForThisRequest(request, response, authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
} RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
} else {

                           // 获取重定向的地址 默认是 http://localhost:8080/login 

            redirectUrl = this.buildRedirectUrlToLoginPage(request, response, authException);
}

                        // 获取重定向的策略 然后发送重定向的地址

        this.redirectStrategy.sendRedirect(request, response, redirectUrl);
}

 sendRedirect

 public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
String redirectUrl = this.calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Redirecting to '" + redirectUrl + "'");
}

                     // 重定向跳转 如果是自定义的登陆界面 那么此处就直接跳转到自定义的认证界面了。

        response.sendRedirect(redirectUrl);
}

  此处重定向的地址是 …/login 该请求会被DefaultLoginPageGeneratingFilter过滤器拦截,具体看下面对DefaultLoginPageGeneratingFilter的介绍

FilterSecurityInterceptor   FilterSecurityInterceptor是SpringSecurity过滤器链中的最后一个过滤器,作用是先判断是否身份验证,然后在做权限的验证。第一次访问的时候处理的在doFilter中的方法的关键代码如下:

 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}

  decide方法在做投票选举,第一次的时候回抛出AccessDeniedException异常,而抛出的异常会被ExceptionTranslationFilter中的catch语句块捕获,进而执行handleSpringSecurityException方法。

DefaultLoginPageGeneratingFilter      DefaultLoginPageGeneratingFilter该过滤器的作用从字面含义也能够看出来就是生成默认登陆界面的一个过滤器

 private void init(UsernamePasswordAuthenticationFilter authFilter, AbstractAuthenticationProcessingFilter openIDFilter) {

                  // 初始化方法中定义的有要拦截的 登陆页面的Url 

        this.loginPageUrl = "/login";
this.logoutSuccessUrl = "/login?logout";
this.failureUrl = "/login?error";
if (authFilter != null) {
this.formLoginEnabled = true;
this.usernameParameter = authFilter.getUsernameParameter();
this.passwordParameter = authFilter.getPasswordParameter();
if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
this.rememberMeParameter = ((AbstractRememberMeServices)authFilter.getRememberMeServices()).getParameter();
}
} if (openIDFilter != null) {
this.openIdEnabled = true;
this.openIDusernameParameter = "openid_identifier";
if (openIDFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
this.openIDrememberMeParameter = ((AbstractRememberMeServices)openIDFilter.getRememberMeServices()).getParameter();
}
} }

  然后在doFilter方法中有判断请求的地址是否是’/login’,如果是的话就拦截,否则就放过

 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
boolean loginError = this.isErrorPage(request);
boolean logoutSuccess = this.isLogoutSuccess(request);
if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {

                           // 不是默认进入登陆界面的请求放过 

            chain.doFilter(request, response);
} else {

                              // 生成默认页面的html页面 

            String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");

                              // 响应请求,中断了过滤器链的执行

            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
}
}

  

3.基于SpringBoot方式的分析

基于配置文件的方式我们清楚了DelegatingFilterProxy是如何处理的,下面分析下在SpringBoot中也是如何处理的

        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

 

基于SpringBoot的自动装配我们知道整合第三方框架要加载的信息是在spring.factories中,如下

其中有三个我们需要关注的会自动装配的配置类

org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguratio n,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoCo nfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfig uration,\

  这三个中和DelegatingFilterProxy有关系的是第三个 SecurityFilterAutoConfiguration 所以我们就直接先来看这个

@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;
}

  进去之后看下结构,我们发现有个ServletContextInitializer,这个是上下文初始化的接口,我们进入看下

进入发现只有一个入口onStartup,先他的RegistraitonBean实现

@FunctionalInterface
public interface ServletContextInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}

 

public final void onStartup(ServletContext servletContext) throws ServletException {
String description = this.getDescription();
if (!this.isEnabled()) {
logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
} else {

                        // 进入register方法中查看 

            this.register(description, servletContext);
}
}

进入DynamicRegistrationBean中查看(不会找实现的可以看我上面的类关系图)

protected final void register(String description, ServletContext servletContext) {

                    // 此处会添加对应的过滤器,也就是DelegatingFilterProxy  

        D registration = this.addRegistration(description, servletContext);
if (registration == null) {
logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
} else {
this.configure(registration);
}
}

先查看下addRegistration方法 进入的是AbstractFilterRegistrationBean它中的方法 

protected Dynamic addRegistration(String description, ServletContext servletContext) {
Filter filter = this.getFilter();

                     // 改过滤器会被添加到servletContext容器中,那么当有满足添加的请求到来的时候就会触发该过滤器拦截

        return servletContext.addFilter(this.getOrDeduceName(filter), filter);
}

 然后再看getFilter方法,可以看到真实的创建了一个DelegatingFilterProxy对象并且指定的名声是springSecurityFilterChain也就是DelegatingFilterProxy代理的Spring容器的Filter的名称

  public DelegatingFilterProxy getFilter() {
           //这个过滤器名称就是外面写死的那个
//这new的玩意其实就和前面XML文件配置的springSecurityFilterChain过滤器一样
      return new DelegatingFilterProxy(this.targetBeanName, this.getWebApplicationContext()) {

protected void initFilterBean() throws ServletException {
}
};
}

 找到过滤器后我们向上返回一级,servletContext.addFilter(this.getOrDeduceName(filter), filter);把过滤器加载到上下文中去,然后再向上返一级看下this.configure(registration)方法,注意这个方法我们要进入AbstractFilterRegistrationBean中查看

 

protected void configure(Dynamic registration) {
super.configure(registration);
EnumSet<DispatcherType> dispatcherTypes = this.dispatcherTypes;
if (dispatcherTypes == null) {
T filter = this.getFilter();
if (ClassUtils.isPresent("org.springframework.web.filter.OncePerRequestFilter", filter.getClass().getClassLoader()) && filter instanceof OncePerRequestFilter) {
dispatcherTypes = EnumSet.allOf(DispatcherType.class);
} else {
dispatcherTypes = EnumSet.of(DispatcherType.REQUEST);
}
} Set<String> servletNames = new LinkedHashSet();
Iterator var4 = this.servletRegistrationBeans.iterator(); while(var4.hasNext()) {
ServletRegistrationBean<?> servletRegistrationBean = (ServletRegistrationBean)var4.next();
servletNames.add(servletRegistrationBean.getServletName());
} servletNames.addAll(this.servletNames);
if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) {

                           // 如果没有指定对应的servletNames和urlPartterns的话就使用默认的名称和拦截地址 /* 

            registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, DEFAULT_URL_MAPPINGS);
} else {
if (!servletNames.isEmpty()) {
registration.addMappingForServletNames(dispatcherTypes, this.matchAfter, StringUtils.toStringArray(servletNames));
} if (!this.urlPatterns.isEmpty()) {
registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, StringUtils.toStringArray(this.urlPatterns));
}
} }

 

DEFAULT_URL_MAPPINGS默认的是 /*

 

至此我们看到了在SpringBoot中是通过DelegatingFilterProxyRegistrationBean 帮我们创建了一个DelegatingFilterProxy过滤器并且指定了拦截的地址,默认是 /* ,之后的逻辑就和前面介绍的XML中的就是一样的了,请求会进入FilterChainProxy中开始处理

4.SpringSecurity初始化到底经历了什么

 通过对 第一次请求的流程梳理 会看到一个FilterChainProxy 至于他是啥时候创建的及默认的过滤器链和过滤器是怎么来的,下面就看下SpringSecurity初始化的时候到底做了哪些事情,基于XML的初始化阶段其实就是各种解析器对标签的解析,过程比较繁琐这里我们就不去分析了,我们直接在SpringBoot项目中来分析,在SpringBoot项目中分析SpringSecurity的初始化过程显然我们需要从 spring.factories 中的SecurityAutoConfiguration开始  

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {

        // 定义了一个默认的事件发布器 


@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}

}

该类引入了 SpringBootWebSecurityConfiguration WebSecurityEnablerConfiguration  SecurityDataConfiguration 这三个类

@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({WebSecurityConfigurerAdapter.class})
@ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class})
@ConditionalOnWebApplication(
type = Type.SERVLET
)
public class SpringBootWebSecurityConfiguration {
public SpringBootWebSecurityConfiguration() {
} @Configuration(
proxyBeanMethods = false
)
@Order(2147483642)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
DefaultConfigurerAdapter() {
}
}
}

这个配置的作用是在如果开发者没有自定义 WebSecurityConfigurerAdapter 的话,这里提供一个默认的实现。@ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class}) 如果有自定义的WebSecurityConfigurerAdapter那么这个配置也就不会起作用了 

@Configuration(
proxyBeanMethods = false
)
@ConditionalOnBean({WebSecurityConfigurerAdapter.class})
@ConditionalOnMissingBean(
name = {"springSecurityFilterChain"}
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {
public WebSecurityEnablerConfiguration() {
}
}

最关键的就是@EnableWebSecurity注解了,我们点进去玩玩

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
boolean debug() default false;
}

在这个接口中我们看到做的事情还是蛮多的,导入了三个类型和一个@EnableGlobalAuthentication注解,我们需要重点来看下WebSecurityConfiguration配置类和@EnableGlobalAuthentication注解WebSecurityConfiguration

  public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}

                 // 获取到的就是FilterChainProxy 该对象会被保存到IoC容器中 且name为springSecurityFilterChain 

        return (Filter)this.webSecurity.build();
}

  

 

  

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:8,964
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,486
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,331
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,114
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,747
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,781