在SpringBoot入门篇中曾经介绍过Spring Security的基本使用,但是考虑到实际开发过程中对于Spring Security的严苛要求,于是准备花点时间来深入研究一下SpringSecurity权限框架。

Spring Security介绍

理由

在介绍Spring Security之前需要介绍为什么不选择Shiro。个人觉得有三点:(1)Apache Shiro是一个轻量级的安全管理框架,提供了认证、授权、会话管理、密码管理、缓存管理等功能,而Spring Security则是一个更为复杂的安全管理框架,功能比Shiro更强大,权限控制细粒度更高,对于OAuth2的支持也更友好。(2)Spring Security源自Spring全家桶,因此与Spring框架可以做到无缝整合。(3)SpringBoot中提供了Spring Security的自动化配置方案,使得Spring Security的使用变得更加便捷。

来源

其实Spring Security最早不叫Spring Security ,而是叫Acegi Security,Acegi英文意思是取消,意味着使用Acegi Security框架可以不用担心安全问题。Acegi Security基于Spring,可以帮助开发者为项目管理提供角色、权限等功能,但是由于它的配置较为繁琐,因此很多时候都是作为“备胎”而存在,直到SpringBoot的诞生使得它终于飞上枝头变凤凰,成为Spring安全管理领域的佼佼者。

核心功能

其实对于一个权限管理框架而言,不管是Shiro还是Spring Security,其最核心的功能就是认证和授权。认证可以认为是通常意义上的登陆;而授权就是判断是否具有相应的权限。举一个非常简单的例子,当你想查阅知乎个人信息时,必须经过认证(登陆)通过后才可进行查阅;而当你想阅读严选会员的书籍时,必须开VIP才能阅读,那么它是做到让普通用户看不到,但让VIP用户看的到的呢?这其实就是授权,该资源只授权VIP用户阅读,普通的用户没有得到授权所有就访问不了了。

对于认证来说,Spring Security支持多种不同的认证方式,这些认证方式部分是Spring Security提供的,部分则是第三方标准组织制订的。下面列举一些比较常见的认证方式:(1)IETF RFC 标准认证,如HTTP BASIC authentication headersHTTP Digest authentication headersHTTP X.509 client certificate exchange。(2)跨平台身份验证,如LDAP。(3)基于表单的身份验证,如Form-based authentication。(4)去中心化认证,如OpenID authentication。(5)用户临时以某个身份登录,如Run-as authentication等。

除了这些常见的认证方式之外,还有一些不常用的认证方式,如(1)单点登录,如Jasig Central Authentication Service。(2)记住我登录(允许一些非敏感操作),如Automatic "remember-me" authentication。(3)匿名登录,如Anonymous authentication等。

以上都是Spring Security提供的部分认证机制,如果在实际开发过程中这些认证机制无法满足需要时,开发者可以自定义认证逻辑。通常在对一些较老的系统进行集成时,开发者会自定义认证逻辑。

除了前面介绍的认证,接下来就是授权。SpringSecurity支持多种授权方式,如基于URL的请求授权,方法访问授权、对象访问授权等。

在学习Spring Security的过程中,开发者还能熟悉各种网络攻击,并据此加深对于Spring Security的理解和使用。

第一个demo

接下来通过一个demo来初识SpringSecurity安全框架。

基本使用

第一步,新建项目,并添加依赖。新建一个SpringBoot项目,名称为ssone,然后在pom.xml文件中添加如下依赖:

1
2
3
4
5
6
7
8
9
<!--添加springSecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

第二步,新建测试Controller类。新建一个controller测试类HelloController,里面的代码为:

1
2
3
4
5
6
7
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello,envy!";
}
}

第三步,启动项目进行测试。接下来启动项目,启动成功后访问http://localhost:8080/hello链接时,可以看到该链接会自动跳转到登录页面,这个登录页面是由SpringSecurity提供的,如下图所示:

请注意默认的用户名是user,默认的登录密码则在每次启动项目时随机生成,查看项目启动日志就能发现诸如如下图片所示的密码:

从项目启动的日志中获取的密码和user用户名就能进行登录,登录成功后用户就能访问http://localhost:8080/hello链接,并显示正确的信息。

你可能比较好奇,我只是单纯的在项目中添加了spring-boot-starter-security依赖,并没有做其他额外的事,怎么就多了一个登陆页面呢?那是因为这个登陆页面是SpringSecurity提供的。在Spring Security中,默认的登录页面和登录接口使用的API都是/login,区别在于登陆页面使用的是get请求,而登录接口使用的是post请求。

同时注意到SpringSecurity生成的临时密码是一个UUID字符串,可以查看UserDetailsServiceAutoConfiguration类,该类是SpringBoot中用于设置与用户相关的自动化配置:

查看一下该类的源码,可以发现其中有一个getOrDeducePassword方法:

1
2
3
4
5
6
7
private String getOrDeducePassword(User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
}

很显然在登陆时,控制台输出的随机密码就是这里打印出来的,当user.isPasswordGenerated()方法为true,就会打印输出随机密码。

查看一下这个isPasswordGenerated方法的源码,如下所示:

可以发现这个isPasswordGenerated方法存在于SecurityProperties类中,且在该类中有一个静态内部类User,且可以清楚的看到当不存在密码时就会通过UUID生成字符串,同时默认的用户是user:

也就是说默认情况下,passwordGenerated的值为true,SpringSecurity的用户名是user,密码则是UUID随机字符串。

自定义设置

配置文件方式

使用默认密码最大的问题就是每次重启项目时,都会产生一个随机的密码,很显然这种方式是非常麻烦的。还记得前面说的密码配置类SecurityProperties么,可以看到该类前面有如下注解信息:

1
2
3
@ConfigurationProperties(
prefix = "spring.security"
)

也就说明这个类是可以读取到application.yml配置文件中的信息,而用户的信息都是存在于这个SecurityProperties类的静态内部类User当中,因此就知道了application.yml配置文件的格式为spring.security.user,那么自定义的用户信息为:

1
2
3
4
5
spring:
security:
user:
name: envy
password: 1234

当用户自定义密码之后,可以看到会调用user对象的setPassword方法:

1
2
3
4
5
6
public void setPassword(String password) {
if (StringUtils.hasLength(password)) {
this.passwordGenerated = false;
this.password = password;
}
}

前面也说过,首先会判断是否自定义密码,如果有的话会将passwordGenerated设置为false,这样就不会在控制台随机生成UUID类型的密码字符串。之后重启项目,就可以使用自定义的用户名和密码进行登录。

配置类方式

除了上面介绍的配置文件方式外,开发者也可以通过配置类来实现对用户信息的自定义设置,不过在此之前需要先了解一些关于密码加密的知识。

密码加密

(1)为什么需要密码加密?如果开发者直接在数据库中存入明文密码,那么当数据库被不法分子攻破后密码就毫无保密可言,因此对密码进行加密是对密码进行二次保护。
(2)加密方案。密码加密一般会用到散列函数,又称散列算法,哈希函数,这是一种从任何数据中创建数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据进行打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据会使得数据库记录更难找到。常用的散列函数有MD5消息摘要算法、安全散列算法(Secure Hash Algorithm)。

但是仅仅使用散列函数还不够,为了增加密码的安全性,一般在密码加密过程中还需要加盐,所谓的盐可以是一个随机数,也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码,密文也是不相同的,这极大地提高了密码的安全性。但是传统的加盐方式需要在数据库中有专门的字段来记录盐值,这个字段可能使用用户名字段(因为用户名唯一),也可能是一个专门记录盐值的字段,这样的配置较为繁琐。SpringSecurity提供了多种密码加密方案,官方推荐使用BCryptPasswordEncoderBCryptPasswordEncoder使用BCrypt强哈希函数,开发者在使用时可以选择提供stength和SecureRandom实例。其中stength越大,密钥的迭代次数越多,密钥的迭代次数为2^strength。stength取值在4~31之间,默认为10,以上就是通常意义上的加密方案。

当你使用Shiro框架时,需要自己处理密码加盐,但是在Spring Security中,BCryptPasswordEncoder就自带了密码加盐功能,因此使用起来非常方便。PasswordEncoder接口是Spring Security框架中的密码加密的接口,接下来阅读该接口的源码:

1
2
3
4
5
6
7
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

可以看到该接口中定义了三个方法,其中encode方法用于对明文密码进行加密,之后返回加密后的密码;matches方法是一个密码校对方法,用于在用户登录的时候,将用户输入的明文密码和数据库中保存的密文密码进行对比,并根据返回的boolean值判断用户输入的密码是否正确;upgradeEncoding方法用于对密码再次进行加密,一般而言是不会再次进行了。

采用自定义类方式来自定义用户信息,需要开发者自定义类并继承WebSecurityConfigurerAdapter类。新建config包,并在其中新建MyWebSecurityConfig类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("1234").roles("ADMIN","USER")
.and()
.withUser("envy").password("1234").roles("USER");
}
}

简单解释一下上述代码的含义:

  • 首先自定义MyWebSecurityConfig类继承自WebSecurityConfigurerAdapter,且重写了configure(AuthenticationManagerBuilder auth)方法,在该方法中配置了两个用户,一个用户名是admin,密码是1234,具备两个角色ADMIN和USER;而另一个用户名是envy,密码是1234,仅仅具备一个角色USER。
  • inMemoryAuthentication方法表示基于内存来进行用户角色配置,其中withUser是用户名,password是用户密码,roles是用户角色。
  • and()方法相当于xml中的关闭之前的标签,并开始后续标签。
  • 本例子仅仅只是用于demo演示,使用的SpringSecurity版本是5.1.15,在SpringSecurity5.x中引入了多种密码加密方式,开发者必须制定一种,请注意本例子使用的NoOpPasswordEncoder已经过时,所以这里也只是demo,实际工作中不能使用该NoOpPasswordEncoder方式,且这种是不对密码进行加密的方式,非常不安全。
  • 基于内存的用户配置角色时,不用在前面添加ROLE_前缀,这一点需要和后续介绍的基于数据库的认证有较大差别。

配置完成后,注释掉前面在application.yml配置文件中新增代码逻辑,然后重启项目,接着就可以使用这两个配置用户进行登录了。

这样关于Spring Security框架的入门学习就到此为止,后续开始继续深入Spring Security框架。