f10@t's blog

Spring安全技术-Spring Security使用及总结(一)

字数统计: 2.5k阅读时长: 12 min
2021/05/09

前面总结了Spring Data JPA,它主要是持久层的一些操作集合。这篇总结一下Spring Security,它也是Spring的一个子项目,它提供了认证、授权、会话管理、密码管理、缓存管理等功能,功能要比常见的Shiro更加丰富,并且支持OAuth2。这篇总结一下Spring Security在认证中的应用,包括:基于内存的用户储存、基于JDBC的用户储存、基于LDAP的用户储存、自定义用户详情服务。

Demo准备

​ 我们直接在上一篇的代码基础上添加认证的功能。首先添加Maven依赖--Spring Boot Security Starter

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
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>lzw.xidian</groupId>
<artifactId>JPADemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>JPADemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

​ 这时候其实Spring就已经为我们添加了一个基础的基于HTTP basic的认证了,其用户名为user、密码在日志中。我们直接启动应用:

image-20210509140929235

​ 再次访问应用就可以看到认证页面了:

​ 日志中也可以看到这样的描述:

1
Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4e50ae56, org.springframework.security.web.context.SecurityContextPersistenceFilter@7604198a, org.springframework.security.web.header.HeaderWriterFilter@4fc6e776, org.springframework.security.web.csrf.CsrfFilter@f2fb225, org.springframework.security.web.authentication.logout.LogoutFilter@5d601832, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@184afb78, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@640c216b, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@1c68d0db, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@615e83ac, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@314b7945, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@464aeb09, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@9be7319, org.springframework.security.web.session.SessionManagementFilter@1f7e52d1, org.springframework.security.web.access.ExceptionTranslationFilter@1689527c, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@5773d271]

​ 可以看到Spring使用了一系列的类来完成这个功能,并且提示Will secure any request with,即你对这个应用的任何请求都会被认证。

基础配置

​ 如果要进一步扩展我们的认证功能,我们首先先需要为Spring Security定义一个配置类,这个类需要继承WebSecurityConfigurerAdapter这个类,这个类的官方描述是这样的:

1
2
3
4
5
6
Provides a convenient base class for creating a WebSecurityConfigurer instance. The implementation allows customization by overriding methods.
Will automatically apply the result of looking up AbstractHttpConfigurer from SpringFactoriesLoader to allow developers to extend the defaults. To do this, you must create a class that extends AbstractHttpConfigurer and then create a file in the classpath at "META-INF/spring.factories" that looks something like:
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer

If you have multiple classes that should be added you can use "," to separate the values. For example:
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer, sample.OtherThatExtendsAbstractHttpConfigurer

​ 即我们需要扩展这个抽象类来进行进一步的自定义,即创建了拦截器链(FilterChain),我们为应用添加一个config包,并写一个配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package lzw.xidian.jpademo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
* @author lzwgiter
* @since 2021/05/09
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

​ 其中包含了两个注解@Configuration@EnableWebSecurity,前一个用来说明这个类是一个配置类,后一个注解的说明是这样的:

1
Add this annotation to an @Configuration class to have the Spring Security configuration defined in any WebSecurityConfigurer or more likely by extending the WebSecurityConfigurerAdapter base class and overriding individual methods: ......

​ 即这个注解添加到被@Configuration注解标注的类上时,就会使用被WebSrcurityConfigurer类的或者扩展了WebSecurityConfigurerAdapter类且复写了相应方法的、提供的安全认证方法。那我们就是后者了,下面我们去复写这个抽象类的对应方法来实现不同方式的认证:

基于内存的用户存储

这个方式比较适合于只有有限的少数用户,且这些用户信息不怎么会变化,那这个方式就比较方便了。代码也很简单,我们直接在上面的配置类里面改就行:

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
package lzw.xidian.jpademo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
* @author lzwgiter
* @since 2021/05/09
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("test123")
.authorities("SUPER_USER")
.and()
.withUser("float")
.password("float")
.authorities("ROLE_USER");
}
}

我们定义了两个用户,现在再启动看看,随便输入个信息:

显然需要正确的用户名和密码了,使用上面的两个就不会显示Bad credentials了。

缺点

  • 代码中出现了用户名和密码,显然不是很安全
  • 扩展性太差(每次添加新用户还得改代码重新部署)

基于JDBC的用户存储

一般情况下用户信息都是保存在数据库中的,下面我们实践一下。这里有两种方式来定义用户信息的表。第一种方式是按照Spring Security自己的要求来进行定义,但是这种方式就要你的数据表的每一个字段都按照它的要去来进行定义。当然你的表的字段可能需要另外起名字,所以还有第二种方法来进行用户数表的定义。

死定义方式

Spring Security自己定义的查找用户信息表的语句如下:

1
2
3
public static final String DEF_USER_BY_USERNAME_QUERY = "select username,password,enabled from users where username = ?";
public static final String DEF_AUTHORTIES_BY_USERNAME_QUERY = "select username, authority from authorities where username = ?";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = select g.id, g.group_name, ga.authority from groups g, group_members gm,group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";

上面这三条语句都是Spring Security自己定义的:

所以我们只要按照他的要求定义表和字段就行了:

我们插入一条数据:

1
INSERT INTO `users` (`username`, `password`, `enabled`) VALUES ('admin', 'admin', '1');

然后定义我们的配置类:

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
package lzw.xidian.jpademo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import javax.sql.DataSource;

/**
* @author lzwgiter
* @since 2021/05/09
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource);
}
}

其中数据源dataSource为自动装配,Spring容器会自动根据你application.properties(yaml)文件的方式来注入DataSource。然后就可以正常认证了。

自定义方式

首先在数据库中重新新建两张表,一张用户信息表,另一张为用户权限定义表:

我们分别插入一条数据:

1
2
3
4
// demo_users 密码使用了10轮的bcrypt加密
INSERT INTO `demo_users` (`name`, `password`, `enabled`) VALUES ('float', '$2a$10$w5PYFG4dgLrPBy6KNT3R/.tQB5n7lxraKncYXk6.JEt98TWufug5y', '1');
// demo_users_authorities
INSERT INTO `demo_users_authorities` (`username`, `authority`) VALUES ('float', 'ROLE_USER');

修改我们的配置类:

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
package lzw.xidian.jpademo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;

import javax.sql.DataSource;

/**
* @author lzwgiter
* @since 2021/05/09
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select name, password, enabled from demo_users where name = ?"
)
.authoritiesByUsernameQuery(
"select username, authority from demo_users_authorities where username = ?"
)
.passwordEncoder(new BCryptPasswordEncoder());
}
}

注意一下,如果是自定义的话,Spring Security 5.0开始会要求密码必须加密处理,所以需要带一个passwordEncoder。这里我使用的是bCrypt强哈希处理,默认的轮数是10,我们可以在线加密后储存到数据库中做测试:

官方提供了一些加密的模块,并且其中一些已经弃用了(StandardPasswordEncoder、NoOpPasswordEncoder),所以建议使用如下的模块:

  • BCryptPasswordEncoder: 使用bcrypt强哈希加密

  • Pdbkf2PasswordEncoder: 使用PBKDF2加密

  • SCryptPasswordEncoder:使用scrypt加密

或者你自己也可以实现PasswordEncoder接口来定义加密方式。

基于LDAP的用户存储

LDAP这里不做解释,其实用法和前两个没太大区别。因为我懒得搭LDAP的服务器哈哈哈,所以这里只记录下用法,配置类代码如下:

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
package lzw.xidian.jpademo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;

import javax.sql.DataSource;

/**
* @author lzwgiter
* @since 2021/05/09
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}")
.passwordCompare()
.passwordEncoder(new BCryptPasswrdEncoder())
.passwordAttribute("passcode")
.contextSource()
.url("ldap://xxx.com:389/dc=xxx,dc=xxx");
}
}

LDAP这块详细也可以看官方给出的例子:Getting Started | Authenticating a User with LDAP (spring.io)

自定义用户认证

(待续)

参考学习

《Spring 实战(第5版)》【美】克雷格·沃斯-著 张卫滨-译

[Getting Started | Authenticating a User with LDAP (spring.io)](

CATALOG
  1. 1. Demo准备
  2. 2. 基础配置
    1. 2.1. 基于内存的用户存储
      1. 2.1.1. 缺点
    2. 2.2. 基于JDBC的用户存储
      1. 2.2.1. 死定义方式
      2. 2.2.2. 自定义方式
    3. 2.3. 基于LDAP的用户存储
    4. 2.4. 自定义用户认证
  3. 3. 参考学习