快速入门
在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 headers
、HTTP Digest authentication headers
和HTTP 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 | <!--添加springSecurity依赖--> |
第二步,新建测试Controller类。新建一个controller测试类HelloController,里面的代码为:
1 | @RestController |
第三步,启动项目进行测试。接下来启动项目,启动成功后访问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 | private String getOrDeducePassword(User user, PasswordEncoder encoder) { |
很显然在登陆时,控制台输出的随机密码就是这里打印出来的,当user.isPasswordGenerated()
方法为true,就会打印输出随机密码。
查看一下这个isPasswordGenerated方法的源码,如下所示:
可以发现这个isPasswordGenerated方法存在于SecurityProperties类中,且在该类中有一个静态内部类User,且可以清楚的看到当不存在密码时就会通过UUID生成字符串,同时默认的用户是user:
也就是说默认情况下,passwordGenerated的值为true,SpringSecurity的用户名是user,密码则是UUID随机字符串。
自定义设置
配置文件方式
使用默认密码最大的问题就是每次重启项目时,都会产生一个随机的密码,很显然这种方式是非常麻烦的。还记得前面说的密码配置类SecurityProperties么,可以看到该类前面有如下注解信息:
1 | @ConfigurationProperties( |
也就说明这个类是可以读取到application.yml配置文件中的信息,而用户的信息都是存在于这个SecurityProperties类的静态内部类User当中,因此就知道了application.yml配置文件的格式为spring.security.user
,那么自定义的用户信息为:
1 | spring: |
当用户自定义密码之后,可以看到会调用user对象的setPassword方法:
1 | public void setPassword(String password) { |
前面也说过,首先会判断是否自定义密码,如果有的话会将passwordGenerated设置为false,这样就不会在控制台随机生成UUID类型的密码字符串。之后重启项目,就可以使用自定义的用户名和密码进行登录。
配置类方式
除了上面介绍的配置文件方式外,开发者也可以通过配置类来实现对用户信息的自定义设置,不过在此之前需要先了解一些关于密码加密的知识。
密码加密
(1)为什么需要密码加密?如果开发者直接在数据库中存入明文密码,那么当数据库被不法分子攻破后密码就毫无保密可言,因此对密码进行加密是对密码进行二次保护。
(2)加密方案。密码加密一般会用到散列函数,又称散列算法,哈希函数,这是一种从任何数据中创建数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据进行打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据会使得数据库记录更难找到。常用的散列函数有MD5消息摘要算法、安全散列算法(Secure Hash Algorithm)。
但是仅仅使用散列函数还不够,为了增加密码的安全性,一般在密码加密过程中还需要加盐,所谓的盐可以是一个随机数,也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码,密文也是不相同的,这极大地提高了密码的安全性。但是传统的加盐方式需要在数据库中有专门的字段来记录盐值,这个字段可能使用用户名字段(因为用户名唯一),也可能是一个专门记录盐值的字段,这样的配置较为繁琐。SpringSecurity提供了多种密码加密方案,官方推荐使用BCryptPasswordEncoder
,BCryptPasswordEncoder
使用BCrypt强哈希函数,开发者在使用时可以选择提供stength和SecureRandom实例。其中stength越大,密钥的迭代次数越多,密钥的迭代次数为2^strength。stength取值在4~31之间,默认为10,以上就是通常意义上的加密方案。
当你使用Shiro框架时,需要自己处理密码加盐,但是在Spring Security中,BCryptPasswordEncoder
就自带了密码加盐功能,因此使用起来非常方便。PasswordEncoder接口是Spring Security框架中的密码加密的接口,接下来阅读该接口的源码:
1 | public interface PasswordEncoder { |
可以看到该接口中定义了三个方法,其中encode方法用于对明文密码进行加密,之后返回加密后的密码;matches方法是一个密码校对方法,用于在用户登录的时候,将用户输入的明文密码和数据库中保存的密文密码进行对比,并根据返回的boolean值判断用户输入的密码是否正确;upgradeEncoding方法用于对密码再次进行加密,一般而言是不会再次进行了。
采用自定义类方式来自定义用户信息,需要开发者自定义类并继承WebSecurityConfigurerAdapter
类。新建config包,并在其中新建MyWebSecurityConfig类,其中的代码为:
1 | @Configuration |
简单解释一下上述代码的含义:
- 首先自定义
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框架。