Spring Security 核心服务

 2023-02-15
原文作者:公子奇 原文地址:https://juejin.cn/post/7101115058956009479

现在我们对 Spring Security 体系结构及其核心类有了一个高层次的概述,让我们更仔细地看一两个核心接口及其实现,特别是AuthenticationManagerUserDetailsServiceAccessDecisionManager。在本文档的其余部分中,这些内容会定期出现,所以了解它们是如何配置和操作的非常重要。

一、AuthenticationManager, ProviderManager 和 AuthenticationProvider

AuthenticationManager只是一个接口,因此实现可以是我们选择的任何东西,但它在实践中是如何工作的呢?如果我们需要检查多个身份验证数据库或不同身份验证服务(如数据库和LDAP服务器)的组合,该怎么办?

Spring Security 中的默认实现称为ProviderManager,它不是处理身份验证请求本身,而是委托给一个配置的AuthenticationProvider列表,依次查询每个AuthenticationProvider,看看它是否可以执行身份验证。每个 provider [提供者]都将抛出一个异常或返回一个完全填充的Authentication对象。还记得我们的好朋友UserDetailsUserDetailsService吗?如果没有,回到上一章,刷新你的记忆。验证身份验证请求最常用的方法是加载相应的UserDetails,并根据用户输入的密码检查加载的密码。这是DaoAuthenticationProvider使用的方法(见下文)。加载的UserDetails对象——特别是它包含的授权权限——将在构建完整的Authentication对象时使用,该对象从成功的身份验证中返回,并存储在SecurityContext中。

如果你正在使用命名空间,ProviderManager的一个实例将在内部创建和维护,你可以通过使用命名空间身份验证提供者元素将提供者添加到它中(参见命名空间一章)。在这种情况下,您不应该在应用程序上下文中声明ProviderManager bean。但是,如果你不使用命名空间,你可以这样声明:

    <bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
        <constructor-arg>
            <list>
                <ref local="daoAuthenticationProvider"/>
                <ref local="anonymousAuthenticationProvider"/>
                <ref local="ldapAuthenticationProvider"/>
            </list>
        </constructor-arg>
    </bean>

在上面的示例中,我们有三个提供程序。按照显示的顺序(通过使用List隐含了这一点)尝试它们,每个提供者都能够尝试身份验证,或者通过返回null跳过身份验证。如果所有实现都返回null,则ProviderManager将抛出ProviderNotFoundException异常。如果您有兴趣了解更多关于链接提供程序的知识,请参考ProviderManager Javadoc。

身份验证机制(如web表单登录处理过滤器)通过对ProviderManager的引用注入,并调用它来处理身份验证请求。您需要的提供者有时可以与身份验证机制互换,而在其他时候,它们将依赖于特定的身份验证机制。例如,DaoAuthenticationProviderLdapAuthenticationProvider与任何提交简单用户名/密码身份验证请求的机制兼容,因此将用于基于表单的登录或HTTP基本身份验证。另一方面,一些身份验证机制创建的身份验证请求对象只能由单一类型的AuthenticationProvider解释。这方面的一个例子是JA-SIG CAS,它使用了服务票据的概念,因此只能由CasAuthenticationProvider进行身份验证。您不必过于关注这一点,因为如果您忘记注册合适的提供者,在尝试进行身份验证时,您将只会收到ProviderNotFoundException

1.1 在身份验证成功后清除凭据

默认情况下(从Spring Security 3.1开始),ProviderManager将尝试从成功的认证请求返回的认证对象中清除任何敏感的凭据信息。这可以防止像密码这样的信息被保留得超过必要的时间。

当您使用用户对象的缓存(例如,在无状态应用程序中提高性能)时,这可能会导致问题。如果身份验证包含对缓存中的对象(例如UserDetails实例)的引用,并且该对象的凭证已被删除,那么将不再可能根据缓存的值进行身份验证。如果您正在使用缓存,则需要考虑到这一点。一个明显的解决方案是首先复制对象,要么在缓存实现中,要么在创建返回的Authentication对象的AuthenticationProvider中。或者,您可以禁用ProviderManager上的eraseCredentialsAfterAuthentication属性。有关更多信息,请参阅Javadoc。

1.2 DaoAuthenticationProvider

Spring Security 实现的最简单的认证提供者是DaoAuthenticationProvider,它也是 Spring Security 最早支持的认证提供者之一。它利用UserDetailsService(作为一个DAO)来查找用户名、密码和授权权限。它通过将UsernamePasswordAuthenticationToken中提交的密码与UserDetailsService加载的密码进行比较来验证用户。配置提供商非常简单:

    <bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
        <property name="userDetailsService" ref="inMemoryDaoImpl"/>
        <property name="passwordEncoder" ref="passwordEncoder"/>
    </bean>

PasswordEncoder是可选的。PasswordEncoder提供了从配置的UserDetailsService返回的UserDetails对象中显示的密码的编码和解码。这将在下面详细讨论。

二、UserDetailsService 实现

正如本参考指南前面提到的,大多数身份验证提供者都利用了UserDetailsUserDetailsService接口。回想一下UserDetailsService的契约是一个单一方法:

    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

返回的UserDetails是一个接口,它提供了 getters,保证提供非空的身份验证信息,例如用户名、密码、授予的权限以及用户帐户是启用还是禁用。大多数身份验证提供者将使用UserDetailsService,即使用户名和密码实际上没有用作身份验证决策的一部分。他们可能只使用返回的UserDetails对象作为其授权权限信息,因为其他一些系统(如LDAP或X.509或CAS等)已经承担了实际验证凭据的责任。

由于UserDetailsService的实现非常简单,因此用户应该很容易使用自己选择的持久性策略检索身份验证信息。话虽如此,Spring Security 确实包括了几个有用的基础实现,我们将在下面讨论。

2.1 In-Memory Authentication 内存中的身份验证

创建自定义UserDetailsService实现,从所选的持久性引擎提取信息,这很容易使用,但许多应用程序不需要这样的复杂性。如果您正在构建一个原型应用程序或刚刚开始集成 Spring Security,而又不想花时间配置数据库或编写UserDetailsService实现,则尤其如此。对于这种情况,一个简单的选择是使用安全名称空间中的user-service元素:

    <user-service id="userDetailsService">
    <!-- 密码前缀为{noop},以指示DelegatingPasswordEncoder应该使用NoOpPasswordEncoder。这对于生产是不安全的,但可以使读取样品更容易。通常密码应该使用BCrypt进行散列 -->
      <user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
      <user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
    </user-service>

它还支持使用外部属性文件:

    <user-service id="userDetailsService" properties="users.properties"/>

属性文件应该包含表单中的条目

    username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]

例如

    jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
    bob=bobspassword,ROLE_USER,enabled

2.2 JdbcDaoImpl

Spring Security 还包括一个UserDetailsService,它可以从 JDBC 数据源获得身份验证信息。在内部使用了Spring JDBC,因此它避免了仅用于存储用户详细信息的全功能对象关系映射器(ORM)的复杂性。如果您的应用程序确实使用 ORM 工具,您可能更喜欢编写一个自定义UserDetailsService来重用您可能已经创建的映射文件。返回到JdbcDaoImpl,配置示例如下所示:

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>
    ​
    <bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
        <property name="dataSource" ref="dataSource"/>
    </bean>

你可以通过修改上面显示的DriverManagerDataSource来使用不同的关系数据库管理系统。您还可以使用从 JNDI 获得的全局数据源,就像使用任何其他 Spring 配置一样。

2.3 Authority Groups 权限组

默认情况下,JdbcDaoImpl在假定权限直接映射到用户的情况下加载单个用户的权限(请参阅数据库模式附录)。另一种方法是将权限划分为组,并将组分配给用户。有些人喜欢用这种方法来管理用户权限。有关如何启用组权限的更多信息,请参阅JdbcDaoImpl Javadoc。组模式也包含在附录中。

三、案例

3.1 环境

    spring=5.2.12
    spring-security=5.3.6
    tomcat=8.5
    jdk=1.8

说明:这里 Tomcat 版本一定要求为 8.5 以上,由于我这里使用的 Spring 版本为 5.x,若使用低版本的 Tomcat 项目会报以下异常:

    java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.changeSessionId()Ljava/lang/String

后续为了更好的理解 Spring Security,我们项目不使用 Spring Boot,Spring Boot 会把一些细节给封装和处理,对我们理解技术或框架本身不太好。

3.2 创建项目

在 Idea 中使用 Maven 创建一个普通的 JavaWeb 项目,引入相关依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
    </dependencies>

3.3 配置

3.3.1 web.xml
    <!-- 3.配置 servlet -->
    <servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    ​
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:application-context.xml</param-value>
    </context-param>
    ​
    <!-- 2.配置过滤器 在 IOC 中查找 -->
    <filter>
        <filter-name>webSecurityFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>springSecurityFilterChain</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>webSecurityFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ​
    <!-- 1. 初始化,启动 Spring IOC 容器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
3.3.2 springMVC-servlet.xml

说明:把这个文件放在 项目的 webapp/WEB-INF目录下。文件名必须为 上面的web.xml{sevlet-name}-servlet.xml。如此配置的好处就是我们无需在web.xml中去引入配置文件。否则我们就需要在web.xml中的 servlet标签里面再配置<init-param><param-name>contextConfigLocation</param-name><param-value>classpath:myspringmvc.xml</param-value></init-param>

    <!-- 开启扫描 controller -->
    <context:component-scan base-package="com.hz.ss.controller" />
    ​
    <!-- 配置 jsp 解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/" />
        <property name="suffix" value=".jsp" />
    </bean>
3.3.3 application-context.xml
    <!-- 启动注解配置 -->
    <context:annotation-config />
    <!-- 配置注解扫描基础包 -->
    <context:component-scan base-package="com.hz.ss" />
3.3.4 配置 UserDetailsService 实现
    package com.hz.ss.config;
    ​
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    ​
    @EnableWebSecurity
    public class WebSecurityConfig {
    ​
        @Bean
        public UserDetailsService userDetailsService() {
            InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
            manager.createUser(
                    User.withDefaultPasswordEncoder()
                            .username("root")
                            .password("123")
                            .roles("ADMIN")
                            .build()
            );
    ​
            return manager;
        }
    }
3.3.5 创建资源
    package com.hz.ss.controller;
    ​
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    ​
    @Controller
    public class IndexController {
    ​
        @GetMapping("/index")
        public String index() {
            return "index";
        }
    }

3.4 验证及总结

当我们访问资源http://127.0.0.1:8080/hz_ss09/index时,由于未做身份验证自动跳转到默认的登录页。

202301012020419761.png

输入我们上面配置的验证信息,会自动跳转到我们登录前访问的页面。

202301012020428612.png

这里我们来说明下 web.xml 中的 filter 配置 DelegatingFilterProxy

在前文 Spring Security 结构中,我们了解类DelegatingFilterProxy是用来代理javaee和spring的bean,所以在配置 filter 的时候,我们就不能单单配置某一个filter。当我们配置好了这个代理 Filter 之后,当项目启动的时候,会自动调用里面的两个方法,我们来看看源码。

    public class DelegatingFilterProxy extends GenericFilterBean {
        @Override
        protected void initFilterBean() throws ServletException {
            synchronized (this.delegateMonitor) {
                if (this.delegate == null) {
                    // 如果没有指定目标bean名称,则使用过滤器名称
                    if (this.targetBeanName == null) {
                        this.targetBeanName = getFilterName();
                    }
                    // 获取Spring根应用上下文,如果可能的话,尽早初始化委托。
                    // 如果根应用程序上下文将在这个过滤器代理之后启动,我们将不得不求助于惰性初始化。
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac != null) {
                        this.delegate = initDelegate(wac);
                    }
                }
            }
        }
    ​
        protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
            String targetBeanName = getTargetBeanName();
            Assert.state(targetBeanName != null, "No target bean name set");
            Filter delegate = wac.getBean(targetBeanName, Filter.class);
            if (isTargetFilterLifecycle()) {
                delegate.init(getFilterConfig());
            }
            return delegate;
        }
    }

从方法initFilterBean(),我们了解到了,为什么在web.xml中,需要配置初始化参数targetBeanName。若我们不配置该参数,项目运行时,会去IOC中查找filter的bean,此时会查找失败包异常,No bean named 'webSecurityFilter' available