关于csmall-passport项目
此项目主要用于实现“管理员”账号的后台管理功能,主要实现:
- 管理员登录
- 添加管理员
- 删除管理员
- 显示管理员列表
- 启用 / 禁用管理员
关于RBAC
RBAC:Role-Based Access Control,基于角色的访问控制
在涉及权限管理的应用软件设计中,应该至少需要设计以下3张数据表:
- 用户表
- 角色表
- 权限表
并且,还至少需要2张关联表:
- 用户与角色的关联表
- 角色与权限的关联表
关于Spring Security框架
Spring Security主要解决了认证与授权相关的问题。
认证:判断某个账号是否允许访问某个系统,简单来说,就是验证登录
授权:判断是否允许已经通过认证的账号访问某个资源,简单来说,就是判断是否具有权限执行某项操作
添加依赖
在基于Spring Boot的项目中,使用Spring Security需要添加依赖项:
<!-- Spring Boot Security依赖项,用于处理认证与授权相关的问题 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
当在项目中添加以上依赖项后,你的项目会发生以下变化(Spring Boot中的Spring Security的默认行为):
-
所有的请求都是必须要登录才允许访问的,包括错误的URL
-
提供了默认的登录页面,当未登录时,会自动重定向到此登录页面
-
提供了临时的登录账号,用户名是
user
,密码是启动项目时在控制台中的UUID值(每次重启项目都会不同)
- 当登录成功后,将自动重定向到此前尝试访问的URL,如果此前没有尝试访问某个URL,则重定向到根路径
- 可以通过
/logout
路径访问到“退出登录”的页面,以实现登出 - 当登录成功后,
POST
请求都是不允许的,而GET
请求是允许的
关于Spring Security的配置类
在项目的根包下,创建config.SecurityConfiguration
类,继承自WebSecurityConfigurerAdapter
类,在类上添加@Configuration
注解:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}
然后,在类中重写void configure(HttpSecurity http)
方法:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
**注意:**在重写的方法中,不要使用super
调用父类的此方法!
由于没有调用父类此方法,再次重启项目后,与此前将有些不同:
- 所有请求都不再要求登录
- 登录、登出的URL不可访问
关于登录表单
在Spring Security配置类的configure(HttpSecurity http)
方法中,根据是否调用了参数对象的formLogin()
方法,决定是否启用登录表单页(/login
)和登出页(/logout
),例如:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 调用formLogin()表示启用登录表单页和登出页,如果未调用此方法,则没有登录表单页和登出页
http.formLogin();
}
关于URL的访问控制
在Spring Security配置类的configure(HttpSecurity http)
方法中,
// 白名单
// 使用1个星号,表示通配此层级的任意资源,例如:/admin/*,可以匹配 /admin/delete、/admin/add-new
// 但是,不可以匹配多个层级,例如:/admin/*,不可以匹配 /admin/9527/delete
// 使用2个连续的星号,表示通配任何层级的任意资源,例如:/admin/**,可以匹配 /admin/delete、/admin/9527/delete
String[] urls = {
"/doc.html",
"/**/*.js",
"/**/*.css",
"/swagger-resources",
"/v2/api-docs"
};
// 配置URL的访问控制
http.authorizeRequests() // 配置URL的访问控制
.mvcMatchers(urls) // 匹配某些URL
.permitAll() // 直接许可,即:不需要通过认证就可以直接访问
.anyRequest() // 任何请求
.authenticated(); // 以上配置的请求需要是通过认证的
使用临时的自定义账号实现登录
可以自定义类,实现UserDetailsService
接口,并保证此类是组件类,则Spring Security框架会基于此实现类来处理认证。
在项目的根包下创建security.UserDetailsServiceImpl
类,实现UserDetailsService
接口,并在类上添加@Service
注解,重写接口中定义的抽象方法:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s)
throws UsernameNotFoundException {
return null;
}
}
当项目中存在UserDetailsService
类型的组件对象时,尝试登录时,Spring Security会自动使用登录表单提交过来的用户名来调用以上loadUserByUsername()
方法,并得到UserDetails
类型的对象,此对象中应该包含用户的相关信息,例如密码、账号状态等,接下来,Spring Security会自动使用登录表单提交过来的密码与UserDetails
中的密码进行对比,且判断账号状态,以决定此账号是否能够通过认证。
所以,重写以上方法:
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 假设存在可用的账号信息:用户名(root),密码(123456)
if ("root".equals(s)) {
UserDetails userDetails = User.builder()
.username("root")
.password("123456")
.disabled(false)
.accountLocked(false)
.accountExpired(false)
.credentialsExpired(false)
.authorities("暂时给个山寨权限,暂时没有作用,只是避免报错而已")
.build();
return userDetails;
}
return null;
}
**提示:**当项目中存在UserDetailsService
类型的组件对象时,Spring Security框架不再提供临时的账号(用户名为user
密码为启动项目时的UUID值的账号)!
**注意:**Spring Security在处理认证时,要求密码必须经过加密码处理,即使你执意不加密,也必须明确的表示出来!
在SecurityConfiguration
中,通过@Bean
方法配置PasswordEncoder
,并返回NoOpPasswordEncoder
的对象,表示“不对密码进行加密处理”:
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
完成后,重启项目,通过/login
可以测试访问。
使用数据库中的账号数据实现登录
需要实现“根据用户名查询用户的登录信息”,需要执行的SQL语句大致是:
select id, username, password, enable from ams_admin where username=?
在项目的根包下创建pojo.vo.AdminLoginInfoVO
类:
@Data
public class AdminLoginInfoVO implements Serializable {
private Long id;
private String username;
private String password;
private Integer enable;
}
在AdminMapper.java
接口中添加抽象方法:
AdminLoginInfoVO getLoginInfoByUsername(String username);
在AdminMapper.xml
中配置以上抽象方法映射的SQL:
<select ...></select>
<sql></sql>
<resultMap></resultMap>
在AdminMapperTests
中编写并执行测试:
接下来,在UserDetailsServiceImpl
中,先自动装配AdminMapper
对象,然后,调整loadUserByUsername()
方法:
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 使用参数s作为参数,调用AdminMapper对象的getLoginInfoByUsername()方法执行查询
// 判断查询结果是否为null
// 是:无此用户名对应的账号信息,返回null
// 返回UserDetails对象
// username:来自查询结果
// password:暂时写死为123456,后续再改成来自查询结果
// disable:来自查询结果中的enable,判断enable是否为0
// accountExpired等:参考此前的Demo,将各值写死
}
完成后,可以使用数据库中的账号测试登录(暂时不方便测试密码)。
tLoginInfoByUsername()方法执行查询
// 判断查询结果是否为null
// 是:无此用户名对应的账号信息,返回null
// 返回UserDetails对象
// username:来自查询结果
// password:暂时写死为123456,后续再改成来自查询结果
// disable:来自查询结果中的enable,判断enable是否为0
// accountExpired等:参考此前的Demo,将各值写死
}
完成后,可以使用数据库中的账号测试登录(暂时不方便测试密码)。
#### 密码为什么需要加密
如果未加密,将密码的原文(原始密码)直接存入到数据库中,可以被轻松获取账户的关键信息!
以目前主流的网络结构和技术,通常,密码加密主要防范的是内部工作人员(能够接触到服务器的人员)!
需要注意:即使密码加密了,也要防范相关的内部工作人员,例如程序员!
#### 如何对密码进行加密
直接使用现有的某种算法,也就是说,不会自行设计某个算法!
#### 使用什么算法对密码进行加密
一定**不可以**使用**加密算法**!因为所有加密算法都是可以被逆向运算的,也就是说,可以根据加密得到的结果,进行反向运算,还原出原始密码!通常,加密算法仅用于保障数据在传输过程中的安全!
在对密码进行加密处理并存入到数据库中时,应该使用**不可逆**的算法!许多**哈希算法**,或基于哈希算法的**消息摘要算法**都是不可逆的!
#### 关于消息摘要算法
典型的消息摘要算法有:
- SHA(Secure Hash Algorithm)家族算法
- SHA-1(160位算法)
- SHA-256(256位算法)
- SHA-384(384位算法)
- SHA-512(512位算法)
- MD(Message Digest)系列算法
- MD2(128位算法)
- MD4(128位算法)
- MD5(128位算法)
消息摘要算法原本是用于验证接收方所接收的数据与发送方所发出的数据是否一致。
消息摘要算法有几个典型特征:
- 如果消息相同,则摘要一定相同
- 如果消息不同,则摘要极大概率会不同
- 必然存在n个不同的消息,摘要完全相同
- 使用同一种算法时,无论消息长度是多少,摘要的长度是固定的
#### 在项目中使用MD5算法
在Spring框架中,提供了`DigestUtils`,可以非常便利的使用MD5算法将消息处理为摘要:
```java
public class Md5Tests {
@Test
void encode() {
String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex(
rawPassword.getBytes());
System.out.println("原文:" + rawPassword);
System.out.println("密文:" + encodedPassword);
}
}
算法位数对安全性的影响
以MD5算法为例,它是128位的算法,即其运算结果是由128个二进制位组成的,所以,其运算结果的排列组件有2的128次方种,这个数字转换成十进制是:340282366920938463463374607431768211456。
理论上,使用MD5算法时,要想找到2个不同的消息运算出相同的摘要,概率应该是340282366920938463463374607431768211456分之1!或者,也可以认为,你至少需要运算340282366920938463463374607431768211456次,才可以找到2个不同的消息运算出相同的摘要。
相比之下,更高位数的算法,理论上,更难找出不同的消息运算出相同的摘要!
一般情况下,由于MD5的安全系数已经较高,所以,不一定需要使用位数更高的算法!
关于消息摘要算法的破解 – 学术
当2个不同的消息,运算出相同的摘要,从学术上,称之为“碰撞”。
理论上,128位的算法,其碰撞概率应该是2的128次方分之1。
关于消息算法的破解,主要是研究其碰撞概率,是否可以使用更少次数的运算实现碰撞!而不是尝试根据摘要进行逆向运算还原出消息!
目前,SHA-1算法已经被视为不安全的算法,它是160位算法,经过研究,只需要经过2的60几次方的运算就可以发生碰撞,即SHA-1的安全系数与60几位的算法几乎相当。
关于消息摘要算法的“破解” – 根据摘要得到消息
网上有许多平台可以做到“根据密文还原出原文”,这些平台都是记录大量的原文与密文的对应关系,当尝试“破解”时,本质上是在做查询操作,大概是:
select 原文 from 数据表 where 密文=?
例如,某平台明确的说明了:
本站针对md5、sha1、sha256等全球通用公开的加密算法进行反向查询,通过穷举字符组合的方式,创建了明文密文对应查询数据库,创建的记录约90万亿条,占用硬盘超过500TB,查询成功率95%以上,很多复杂密文只有本站才可查询。本站专注于各种公开算法,已稳定运行17年。
如果密码可以使用全部的可打印字符,7位长度的密码的排列组合有约70万亿种,8位长度的密码的排列组件在此基础上需要乘以95,则以上平台不可能记录8位长度的所有明文密文的对应关系!也就是说,只要原始密码的长度达到8位,这些平台就可能无法根据密文查询出原文,原始密码的长度越长,或原始密码的强度越高(由多种元素组成,例如大小写字母、数字、标点符号),被这些平台收录的可能性就越低!
如何进一步保障用户的密码安全 – 加盐
盐值的本质就只是一个外部人员很难预测到的字符串,它将作用于处理加密过程中,例如:
// 以下1行定义了盐值
String salt = "fsd4W87i78oiAsUu43IEF";
String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex(
(rawPassword + salt).getBytes());
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 将原始密码和盐值一起被处理
System.out.println("原文:" + rawPassword);
System.out.println("密文:" + encodedPassword);
当然,盐值应该如何使用,也没有明确的规定,你可以:
String encodedPassword = DigestUtils.md5DigestAsHex(
(rawPassword + salt).getBytes());
或者:
String encodedPassword = DigestUtils.md5DigestAsHex(
(salt + rawPassword).getBytes());
甚至:
String encodedPassword = DigestUtils.md5DigestAsHex(
(salt + rawPassword + salt + rawPassword + salt + salt).getBytes());
总而言之,使用盐的目的是”使得被MD5运算的原始数据变得更加复杂“。
你甚至可以使用随机的盐值,例如:
String salt = UUID.randomUUID().toString(); // 使用UUID作为盐值
String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex(
(rawPassword + salt).getBytes());
使用随机盐时必须注意:你需要将随机的盐值保存下来,否则,后续你将无法验证密码!
至于如何保存,方式有许多,例如在数据表中添加新的字段来保存盐值,或者,把盐值直接作为密码的一部分,例如:
String salt = UUID.randomUUID().toString();
String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex(
(rawPassword + salt).getBytes()) + salt;
System.out.println("盐值:" + salt);
System.out.println("原文:" + rawPassword);
System.out.println("密文:" + encodedPassword);
密码加密原则 – 小结
关于密码加密处理:
- 不可以使用加密算法,只能使用消息摘要算法或其它哈希算法
- 不建议使用SHA-1
- 应该要求用户使用更长的、强度更高的密码,避免容易被反查(根据密文查询得到原文)
- 应该进行加盐处理
- 你还可以使用多重加密(使用同一个算法,或不同算法,对数据进行反复运算)
- 可以考虑使用位数更长的算法(在MD5的基础上,改为使用SHA-256 / SHA-384 / SHA-512)
**注意:**无论你综合使用以上哪些做法,最终,可能都无法避免内部人员泄密(算法、加密参数、加密过程、密文都是破解时的已知条件)导致的穷举式的暴力破解,而BCrypt算法是被设计得运算效率极低的算法,可以非常有效的避免被暴力破解。