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