Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。

Spring Security 对 Web 资源的保护是靠 Filter 实现的。当初始化 Spring Security 时,会创建一个名为 springSecurityFilterChain 的 Servlet 过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了 javax.servlet.Filter,因此外部的请求会经过此类。

FilterChainProxy 是一个代理,真正起作用的是 FilterChainProxy 中 SecurityFilterChain 所包含的各个 Filter。这些 Filter 作为 Bean 被 Spring 管理,它们是 Spring Security 核心,他们不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理。

Spring Security 功能的实现主要是由一系列过滤器链相互配合完成:

    (1) SecurityContextPersistenceFilter:是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
    (2) UsernamePasswordAuthenticationFilter:用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
    (3) FilterSecurityInterceptor: 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问;

这里重点讨论两个概念:身份认证和授权,它们也是 Spring Security 的主要职责。

在 Spring Boot 出现之前,Spring Security 已经发展了很多年,但那时相较于强大的 Shiro,它一直不温不火。因为和 Shiro 相比,在 SSH (Spring+Struts+Hibernate) / SSM (Spring+SpringMVC+MyBatis) 中整合 Spring Security 是一件很繁琐的事情。

Spring Boot 给 Spring Security 提供了自动化配置方案 (spring-boot-starter-security),可以零配置使用 Spring Security。

1. 创建 Spring Boot 项目

    项目名称:SpringbootWeb02
    Spring Boot 版本:2.6.6

2. 添加 Thymeleaf 模板

    1) 修改 pom.xml

1         <project ... >
 2             ...
 3             <dependencies>
 4                 ...
 5 
 6                 <dependency>
 7                     <groupId>org.springframework.boot</groupId>
 8                     <artifactId>spring-boot-starter-thymeleaf</artifactId>
 9                 </dependency>
10 
11                 ...
12             </dependencies>
13 
14             ...
15         </project>

        在IDE中项目列表 -> SpringbootWeb02 -> 点击鼠标右键 -> Maven -> Reload Project

    2) 创建模板文件

        Thymeleaf 模板的默认位置在 resources/templates 目录下,默认的后缀是 html,即只要将 HTML 页面放在“classpath:/templates/”下,Thymeleaf 就能自动进行渲染。

        (1) 创建 src/main/resources/templates/common.html 文件

1             <div th:fragment="fragment-header" id="fragment-header-id">
 2                 <p>Header</p>
 3             </div>
 4             <div th:fragment="fragment-banner" id="fragment-banner-id">
 5                 <p>Banner</p>
 6                 <hr />
 7             </div>
 8             <div th:fragment="fragment-footer(var)" id="fragment-footer-id">
 9                 <hr />
10                 <p th:text="${var}">Footer</p>
11             </div>

             注:若 src/main/resources/templates 目录不存在,手动创建各级目录,下同。

        (2) 创建 src/main/resources/templates/home.html 文件  

1             <!DOCTYPE html>
 2             <html lang="en" xmlns:th="http://www.thymeleaf.org">
 3             <head>
 4                 <meta charset="UTF-8">
 5                 <title>Home</title>
 6             </head>
 7             <body>
 8 
 9             <div th:replace="common::fragment-header"></div>
10 
11             <div th:replace="common::fragment-banner"></div>
12 
13             <div id="content" th:style="'min-height: 480px;'">
14                 <h3>Home Page</h3>
15 
16                 <p>Message: <span th:text="${message}"></span></p>
17             </div>
18 
19             <div th:replace="common::fragment-footer(var='Copyright 2020')"></div>
20 
21             </body>
22             </html>

        (3) 修改 src/main/java/com/example/controller/IndexController.java 文件

1             package com.example.controller;
 2 
 3             import org.springframework.ui.Model;
 4             import org.springframework.stereotype.Controller;
 5             import org.springframework.web.bind.annotation.RequestMapping;
 6             import org.springframework.web.bind.annotation.ResponseBody;
 7 
 8             @Controller
 9             public class IndexController {
10                 @ResponseBody
11                 @RequestMapping("/test")
12                 public String test() {
13                     return "Test Page";
14                 }
15 
16                 @RequestMapping("/home")
17                 public String home(Model model) {
18                     model.addAttribute("message", "Spring Boot Thymeleaf Demo");
19                     return "home";
20                 }
21             }

    3) 运行

Edit Configurations

            Click "+" add new configuration -> Select "Maven"

                Command line: clean spring-boot:run
                Name: SpringbootWeb02 [clean,spring-boot:run]

            -> Apply / OK

        Click Run "SpringbootWeb02 [clean,spring-boot:run]"

            ...

            Spring boot web project



        访问 http://localhost:9090/home

3. 添加 Spring Security

    1) 修改 pom.xml

1         <project ... >
 2             ...
 3             <dependencies>
 4                 ...
 5 
 6                 <dependency>
 7                     <groupId>org.springframework.boot</groupId>
 8                     <artifactId>spring-boot-starter-security</artifactId>
 9                 </dependency>
10 
11                 ...
12             </dependencies>
13 
14             ...
15         </project>

        在IDE中项目列表 -> SpringbootWeb02 -> 点击鼠标右键 -> Maven -> Reload Project

        运行并访问 http://localhost:9090/test,自动跳转到 http://localhost:9090/login, 默认用户名是 user,密码在项目启动时输出在控制台,格式如下:

            Using generated security password: 61c29bfd-0f2b-4a35-a27c-c569f9b6f02d

            This generated password is for development use only. Your security configuration must be updated before running your application in production.

        可以设置自定义用户名和密码,在 application.properties 中配置:

            spring.security.user.name=admin
            spring.security.user.password=123456
            spring.security.user.roles=admin

        注: application.properties 的 admin 配置后,默认用户名 user 将失效。   

    2) 默认安全保护

         Spring Boot 项目(本文版本 2.6.6)导入 spring-boot-starter-security 包后,http://localhost:9090/test 就处于默认安全保护状态,要关闭或暂停这种默认的安全保护状态,需要修改 src/main/java/com/example/App.java 里的启动类,修改代码如下:

1             package com.example;
 2 
 3             import org.springframework.boot.SpringApplication;
 4             import org.springframework.boot.autoconfigure.SpringBootApplication;
 5             import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 6             import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
 7 
 8             @SpringBootApplication
 9             @EnableAutoConfiguration( exclude = { SecurityAutoConfiguration.class } )
10             public class App {
11                 public static void main(String[] args) {
12                     SpringApplication.run(App.class, args);
13                     System.out.println("Spring boot example04 project");
14                 }
15             }

        以上代码中,新增了 @EnableAutoConfiguration 注解,所以要重新开启保护状态,只需注释掉这一行。

        还有一种方法也可以达到这种效果,在 Spring Boot 的启动类中的注解 @SpringBootApplication 上加入排除 Spring Boot 安全组件的配置,即:

            @SpringBootApplication( exclude = { SecurityAutoConfiguration.class } )

        如果不熟悉 @SpringBootApplication 注解的原理,建议使用 @EnableAutoConfiguration 注解。

4. 自定义配置 Spring Security

    自定义配置 Spring Security 可以通过配置 WebSecurityConfigurerAdapter 的扩展类来实现,比如创建 WebSecurityConfig 类:

1         @Configuration
 2         public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 3 
 4             @Override
 5             protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 6                 super.configure(auth);
 7             }
 8 
 9             @Override
10             public void configure(WebSecurity web) throws Exception {
11                 super.configure(web);
12             }
13 
14             @Override
15             protected void configure(HttpSecurity http) throws Exception {
16                 super.configure(http);
17             }
18             
19         }

    1) 认证管理器配置方法

        void configure(AuthenticationManagerBuilder auth) 用来配置认证管理器 AuthenticationManager。所有 UserDetails 相关的由它处理,包含 PasswordEncoder 密码编码处理。

    2) 核心过滤器配置方法

        void configure(WebSecurity web) 用来配置 WebSecurity。而 WebSecurity 是基于 Servlet Filter 用来配置 springSecurityFilterChain。
        
        而 springSecurityFilterChain 又被委托给了 Spring Security 核心过滤器 Bean DelegatingFilterProxy。
        
        相关逻辑可以在 WebSecurityConfiguration 中找到,一般不会过多来自定义 WebSecurity,使用较多的使其ignoring() 方法用来忽略 Spring Security 对静态资源的控制。

    3) 安全过滤器链配置方法

        void configure(HttpSecurity http) 这个是我们使用最多的,用来配置 HttpSecurity 。 HttpSecurity 用于构建一个安全过滤器链 SecurityFilterChain,SecurityFilterChain 最终被注入核心过滤器 。
        
        (1) HttpSecurity 默认配置

protected void configure(HttpSecurity http) throws Exception {
        
                http
                    .authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .httpBasic();
            }

            上面是 Spring Security 在 Spring Boot 中的默认配置。通过以上的配置,应用具备了以下的功能:

                a) 所有的请求访问都需要被授权;
                b) 使用 form 表单进行登陆(默认路径为/login);
                c) 防止 CSRF 攻击、 XSS 攻击;
                d) 启用 HTTP Basic 认证。

        (2) HttpSecurity 常用配置

            HttpSecurity 使用了 builder 的构建方式来灵活制定访问策略,最早基于 XML 标签对 HttpSecurity 进行配置,现在使用 javaConfig 方式。常用配置如下:

方法

描述

openidLogin()

用于基于 OpenId 的验证

headers()

将安全标头添加到响应,比如说简单的 XSS 保护

cors()

配置跨域资源共享(CORS)

sessionManagement()

允许配置会话管理

portMapper()

允许配置一个 PortMapper(HttpSecurity#(getSharedObject(class))),其他提供 SecurityConfigurer 的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security 使用一个 PortMapperImpl 映射 HTTP 端口 8080 到 HTTPS 端口8443,HTTP 端口 80 到 HTTPS 端口 443

jee()

配置基于容器的预认证。 在这种情况下,认证由 Servlet 容器管理

x509()

配置基于x509的认证

rememberMe

允许配置“记住我”的验证

authorizeRequests()

允许基于使用 HttpServletRequest 限制访问

requestCache()

允许配置请求缓存

exceptionHandling()

允许配置错误处理

securityContext()

在 HttpServletRequests 之间的 SecurityContextHolder 上设置SecurityContext的管理。 当使用 WebSecurityConfigurerAdapter 时,这将自动应用

servletApi()

将 HttpServletRequest 方法与在其上找到的值集成到 SecurityContext 中。 当使用 WebSecurityConfigurerAdapter 时,这将自动应用

csrf()

添加 CSRF 支持,使用 WebSecurityConfigurerAdapter 时,默认启用

logout()

添加退出登录支持。当使用 WebSecurityConfigurerAdapter 时,这将自动应用。默认情况是,访问URL “/logout”,使 HTTP Session 无效来清除用户,清除已配置的任何 #rememberMe()身份验证,清除 SecurityContextHolder,然后重定向到 “/login?success”

anonymous()

允许配置匿名用户的表示方法。 当与 WebSecurityConfigurerAdapter 结合使用时,这将自动应用。 默认情况下,匿名用户将使用 org.springframework.security.authentication.AnonymousAuthenticationToken 表示,并包含角色 “ROLE_ANONYMOUS”

formLogin()

指定支持基于表单的身份验证。如果未指定 FormLoginConfigurer#loginPage(String),则将生成默认登录页面

oauth2Login()

根据外部 OAuth 2.0 或 OpenID Connect 1.0 提供程序配置身份验证

requiresChannel()

配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射

httpBasic()

配置 Http Basic 验证

addFilterBefore()

在指定的 Filter 类之前添加过滤器

addFilterAt()

在指定的 Filter 类的位置添加过滤器

addFilterAfter()

在指定的 Filter 类的之后添加过滤器

and()

连接以上策略的连接器,用来组合安全策略。实际上就是 "而且" 的意思

 

5. HttpSecurity 配置实例

    1) 创建 src/main/java/com/example/config/WebSecurityConfig.java 文件

1         package com.example.config;
 2 
 3         import org.springframework.beans.factory.annotation.Autowired;
 4         import org.springframework.context.annotation.Configuration;
 5         import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 6         import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 7         import org.springframework.security.core.userdetails.UserDetailsService;
 8         import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 9 
10         @Configuration
11         public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
12 
13             @Autowired
14             private UserDetailsService userDetailsService;
15 
16             @Override
17             protected void configure(AuthenticationManagerBuilder auth) throws Exception {
18                 auth.userDetailsService(userDetailsService);
19             }
20 
21             @Override
22             protected void configure(HttpSecurity http) throws Exception {
23                 // 配置认证
24                 http.authorizeRequests().anyRequest().authenticated()
25 
26                     .and()
27                     .formLogin()
28                     .loginPage("/login") // 自定义登录页面
29                     .loginProcessingUrl("/login/post") // 登录访问路径
30                     .defaultSuccessUrl("/home").permitAll()    // 登陆成功之后跳转地址
31 
32                     .and()
33                     .csrf().disable(); // 关闭 csrf 保护功能,默认是开启的
34 
35             }
36 
37         }

    2) 创建 src/main/resources/templates/login.html 文件

1         <!DOCTYPE html>
 2         <html lang="en">
 3         <head>
 4             <meta charset="UTF-8">
 5             <title>Login</title>
 6         </head>
 7         <body>
 8         <h3>Login</h3>
 9         <p> </p>
10         <form action="/login/post" method="post">
11             <p><input type="text" placeholder="Username" name="username" value="admin"></p>
12             <p><input type="password" placeholder="Password" name="password" value="123456"></p>
13             <p><input type="submit" value="Submit"></p>
14         </form>
15         </body>
16         </html>

        注:如果开启 csrf 功能,需要在 <form></form> 标记内添加如下 Thymeleaf 模板代码。

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

    3) 修改 src/main/java/com/example/controller/IndexController.java 文件

1         package com.example.controller;
 2 
 3         import org.springframework.ui.Model;
 4         import org.springframework.stereotype.Controller;
 5         import org.springframework.web.bind.annotation.RequestMapping;
 6         import org.springframework.web.bind.annotation.ResponseBody;
 7 
 8         @Controller
 9         public class IndexController {
10             @ResponseBody
11             @RequestMapping("/test")
12             public String test() {
13                 return "Test Page";
14             }
15 
16             @RequestMapping("/home")
17             public String home(Model model) {
18                 model.addAttribute("message", "Spring Boot Thymeleaf Demo");
19                 return "home";
20             }
21 
22             @RequestMapping("/login")
23             public String login() {
24                 return "login";
25             }                
26         }

    访问 http://localhost:9090/home,自动跳转到 http://localhost:9090/login