Spring Security 源码分析八:Spring Security 过滤链二 - Demo 例子

前言

本文是对 Spring Security Core 4.0.4 Release 进行源码分析的系列文章之一;

本文为作者的原创作品,转载需注明出处;

简介

本章,笔者将试图通过一个 Spring Security 的 Demo 来简单描述一下 Spring Security 过滤链是如何工作的;该例子中将会分别创建两个 SecurityFilterChain,一个是 /web/**,一个是 /rest/** 的过滤链;

Demo

该 Demo 是基于 Spring Boot Web Security 所构建而来;根据当前项目需要,版本稍低 1.3.8.RELEASE;

POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<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.shangyang.spring</groupId>
<artifactId>security-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>

<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.8.RELEASE</version>
</parent>

<!-- Add typical dependencies for a web application -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<properties>
<java.version>1.8</java.version>
</properties>

<!-- Package as an executable jar -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

DemoApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Controller
@EnableAutoConfiguration
public class DemoApplication {

...

@Configuration
@EnableWebSecurity
@Order(1)
static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER").and()
.withUser("manager").password("password").roles("MANAGER");

}

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

http.antMatcher("/web/**") // the filter chain defined for web request
.authorizeRequests()
.antMatchers("/web/report/**").hasRole("MANAGER")
.anyRequest().authenticated()
.and()
.formLogin()
// login 的相对路径必须与 security chain 的的相对路径吻合,这里是 /web/**;注意 login 分两步,一步是 Getter 会到 login.html,另外一步是从 login.html -> post -> /web/login/
.loginPage("/web/login")
// 允许访问
.permitAll();

}
}

@Configuration
@EnableWebSecurity
@Order(2)
static class RestSecurityConfig extends WebSecurityConfigurerAdapter {

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

http.antMatcher("/rest/**") // the filter chain defined for web request;
.csrf().disable() // rest 请求无需 CSRF Token;
.authorizeRequests()
.antMatchers("/rest/hello").hasRole("USER")
.anyRequest().authenticated()
.and()
.httpBasic();

}
}

public static void main(String[] args) throws Exception {

SpringApplication.run(DemoApplication.class, args);
}

}

通过 WebSecurityConfigurerAdapter 配置实现了两个 Spring Security 过滤链,一个是 /web/**,一个是 /rest/** 的过滤链;从名称上可以清晰的看到,一个过滤链是用来处理 web request,另外一个过滤链用来处理 restful request

  • web request 过滤链
    使用 Form Login 的方式进行登录验证;
    用户需要由 MANAGER 角色才能访问相关资源;
    使用 CSRF token filter 进行验证;
    ……

  • restful request 过滤链
    使用 Basic Auth 的方式进行登录验证;
    用户需要由 USER 角色才能访问相关资源;
    不使用 CSRF token filter 进行验证;
    ……

Web 登录

走 login form 的流程,针对该流程,Spring Security 会提供基于 URL /web/login/ 的 post 请求的方法,用来验证用户基于表单的认证请求;

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" class="uk-height-1-1">
<head>
<meta charset="UTF-8"/>
<title>OAuth2 SSO Demo</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/2.26.3/css/uikit.gradient.min.css"/>
</head>

<body class="uk-height-1-1">

<div class="uk-vertical-align uk-text-center uk-height-1-1">
<div class="uk-vertical-align-middle" style="width: 250px;">
<h1>Login Form</h1>

<p class="uk-text-danger" th:if="${param.error}">
Login failed ...
</p>

<p class="uk-text-success" th:if="${param.logout}">
Logout succeeded.
</p>

<form class="uk-panel uk-panel-box uk-form" method="post" th:action="@{/web/login}">
<div class="uk-form-row">
<input class="uk-width-1-1 uk-form-large" type="text" placeholder="Username" name="username"
value="user"/>
</div>
<div class="uk-form-row">
<input class="uk-width-1-1 uk-form-large" type="password" placeholder="Password" name="password"
value="password"/>
</div>
<div class="uk-form-row">
<button class="uk-width-1-1 uk-button uk-button-primary uk-button-large">Login</button>
</div>
</form>
</div>
</div>
</body>
</html>

注意,这里 form 表单的提交必须使用链接 /web/login

1
<form class="uk-panel uk-panel-box uk-form" method="post" th:action="@{/web/login}">

如果,将表单的提交链接错误的写成了 /login

1
<form class="uk-panel uk-panel-box uk-form" method="post" th:action="@{/login}">

如果是这样设置的话,用户登录以后将会报错,报错提示根据相关链接 /login 找不到对应的 Controller;根本原因是,/login 访问链接不能命中 /web/** 安全链,而 login form 既处理表单的部分是通过该安全链中的一个 Filter 既是LoginFilter来处理的,而该 Filter 的拦截链接在初始化的时候,其拦截地址,既是 loginUrl 必须匹配 /web/** 的规则,所以,初始化后,loginUrl 的初始值为 /web/login

Restful 登录