大家好呀,我是小黑。

我们在开发应用时,只要涉及到用户,登录注册功能则是必不可少的。
但是,并不是所有人都能做好登录注册功能。比如最基本的密码应该如何保存?应该用哪种加密方式对密码进行加密都不是很清楚。

一旦出现数据库泄漏,密码外泄等问题,会对用户造成极大的损失。

密码该如何保存?

如果我们要在服务器中对用户进行身份验证,我们需要完成以下的步骤:

  • 获取到要登录用户的用户名和密码;
  • 根据用户名在数据库中查找到用户;
  • 比较用户提供的密码和数据库中的密码是否一致。

那我们应该如何存储用户的密码呢?我们来看看都有哪些方式,以及存在的问题。

明文保存

将用户的密码以明文方式保存。

很显然,有点常识的人都应该知道,密码不能用明文保存的。但是话又说回来,系统都是由人开发的,开发系统的人可能并不专业。比如之前某个大型中文开发者社区,因为数据库泄露,导致大批用户的密码泄漏,而他们的密码就是明文保存的。

HASH保存

使用Hash函数计算出密码的hash值保存,可以解决密码直接暴露的问题。

Hash函数是一个单向函数,不能通过结果值反向得出原始值,Hash函数可以将一串密码转换成一个固定长度的字符串。

  • 在用户注册时,将用户的密码使用Hash函数计算出Hash值后保存到数据库;
  • 当用户登录时,对用户提交的密码使用相同的Hash函数计算出Hash值,和数据库中的Hash值进行比较。

这样可以避免让攻击者直接获取到用户的密码明文,攻击者想通过暴力攻击将字符串计算出hash值则需要花费巨大的精力,并且Hash值越长破解难度越大。

但是通过彩虹表攻击,攻击者仍然可以成功破解。 彩虹表是一个包含许多提前计算出Hash值的表,其中包含数百万个密码对应的hash值,对于一些简单密码可以非常快的破解。

所以,如果你不确定你注册的服务是采用哪种方式保存的密码,尽量将密码复杂度设置高一些。

加盐Hash

为了防止彩虹表攻击,可以使用Hash算法加盐处理。

是在进行Hash计算时,和原始密码拼接在一起进行计算的一个随机序列。

  • 用户注册时,将密码和盐值组合后进行Hash计算,得到密码结果保存在数据库中;
  • 当用户在登录验证时,将原始密码加盐后进行Hash计算,得到结果值和数据库中的密码进行比较。

因为彩虹表中的密码和加盐后的密码不一样,可以防止彩虹表攻击。如果盐值足够长并且随机,那么就可以保证在彩虹表中不能找到和密码相同的hash值。

但是,由于攻击者是有可能获取到盐值的,攻击者可以调整彩虹表生成的算法,用获取到的盐值计算出新的彩虹表,同样可以获取到密码。虽然计算一个新的彩虹表花费的时间巨大,但是随着硬件条件越来越好,要计算出一张彩虹表会变得越来越容易。

所以,使用Hash算法加盐处理,可以保证密码不被快速破解,但是还不够安全。

密码加密函数

Hash函数设计的初衷并不仅仅是对密码进行Hash计算,所以Hash函数的运算速度非常快,但是这样一来,攻击者也能快速计算hash值,进行暴力破解。

为了解决这个问题,我们可以让Hash加密函数变慢

我们只要让密码加密的时间在用户能接受的时间内,尽量的慢,这样攻击者蛮力破解将会花费无限的时间。

有以下一些专门用来加密密码的算法:

  • bcrypt
  • scrypt
  • PBKDF2
  • argon2

这些算法使用一些复杂的加密算法,并会故意让计算变慢。

工作因子

可以通过在算法中配置工作因子,来调整加密函数计算时间的缓慢程度。

每个密码加密算法都有自己的工作因子。工作因子影响密码编码的速度。例如,bcrypt有参数strength,该算法将使2的strength次方来计算哈希值。数字越大,编码越慢。

使用Spring Security加密密码

现在让我们看看 Spring Security 如何支持这些算法,以及我们如何使用它们加密密码。

PasswordEncoder

在Spring Security 中有一个PasswordEncoder接口。所有密码编码器都实现了该接口。

public interface PasswordEncoder {String encode(CharSequence rawPassword);boolean matches(CharSequence rawPassword, String encodedPassword);default boolean upgradeEncoding(String encodedPassword) {return false;}
}

该接口中有两个方法:

encode()方法用户将明文密码转换为密文形式;

matches()方法用户将明文密码与密文密码进行比较。

BCryptPasswordEncoder

String plainPassword = "123456";
// 工作因子
int strength = 10;
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength, new SecureRandom());
String encodedPassword = bCryptPasswordEncoder.encode(plainPassword);
System.out.println(encodedPassword);

BCryptPasswordEncoder中的参数strength是密码加密算法的工作因子,Spring Security中的默认值为10

在创建时指定SecureRandom作为随机加盐生成器。

$2a$10$pYxXvggEgN7znYKofHIr/uRTw.dsYeW9mbxzNMSNOoGIYZU8twXNG

Pbkdf2PasswordEncoder

PBKDF2 算法不是为密码编码专门设计的,而是为了从密码中派生出密钥而设计的。当我们想用密码对某些数据进行加密时,通常需要密钥,但密码的强度不足以用作加密密钥。

String plainPassword = "123456";
//加密秘钥
String pepper = "小黑说JAVA";
// 哈希次数
int iterations = 200000;
// 哈希长度
int hashWidth = 256;Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder(pepper, iterations, hashWidth);
pbkdf2PasswordEncoder.setEncodeHashAsBase64(true);
String encodedPassword = pbkdf2PasswordEncoder.encode(plainPassword);
System.out.println(encodedPassword);

Pbkdf2PasswordEncoder会多次在普通密码上运行哈希算法。我们可以定义输出的hash长度,并额外使用pepper让密码编码更安全。

WnCG4wMZFHPAD9DGg+SChNceQqbeAZRQyf2OHCK5WKdYBRzbeAGsQg==

Pbkdf2PasswordEncoder默认会执行185000哈希计算,默认的哈希长度为256。

SCryptPasswordEncoder

SCryptPasswordEncoder算法可以配置CPU和内存成本,通过这两项配置可以让攻击者破解密码的难度更大。

String plainPassword = "123456";
// cpu消耗
int cpuCost = (int) Math.pow(2, 14);
// 内存消耗
int memoryCost = 8;
// currently not supported by Spring Security
int parallelization = 1;
// 秘钥长度
int keyLength = 32;
// 盐值长度
int saltLength = 64;SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder(cpuCost,memoryCost,parallelization,keyLength,saltLength);
String encodedPassword = sCryptPasswordEncoder.encode(plainPassword);
System.out.println(encodedPassword);

输出结果如下:

$e0801$PgZZvXdDjbxMZJi4eidFCHblUdvwOT/n0FZFyCWIHloqL6Wkbk7bAJ2nwVIWsW9PJTodncEtok1qcaWR+u+pZg==$lcqK7ACDTv8gG3ZwGoz0X7rn4EnZvnEcZ7rS0Qq31Ng=

Argon2PasswordEncoder

Argon2算法是2015 年密码哈希竞赛的获胜者。该算法也允许我们调整 CPU 和内存成本。该算法将所有参数保存在结果字符串中。

int saltLength = 16;
int hashLength = 32;
int parallelism = 1;
int memory = 4096;
int iterations = 3;Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder(saltLength,hashLength,parallelism,memory,iterations);
String encodePassword = argon2PasswordEncoder.encode(plainPassword);

输出结果如下:

$argon2id$v=19$m=4096,t=3,p=1$uft4b+crs6tiwOhDnuFsIg$d/GXjYZnEw+/ubVnPqNeQDFX32GRYe+yTwuwydXLjos

在Spring Boot中设置PasswordEncoder

接下来,为了能更好的了解PasswordEncoder在Spring Boot中如何应用,我们先来开发一个Rest Api接口,并且配置Spring Security支持基于密码验证。

配置PasswordEncoder

首先,我们创建一个需要Spring Security保护的Rest API:

@RestController
public class BlogRest {@GetMapping(path = "/blogs")public List<Blog> blogs() {return Lists.newArrayList(new Blog("hello world", "小黑说Java"));}
}

我们需要/blogs接口的访问需要经过用户身份的验证。因此,我们使用 Spring Security 配置:

/*** @author 小黑说Java* @ClassName SecurityConfiguration* @Description* @date 2022/2/3**/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {httpSecurity.csrf().disable().authorizeRequests().antMatchers("/registration").permitAll().anyRequest().authenticated().and().httpBasic();}// other codes.
}
  • 该配置表示除了/registration 外,其他的请求路径都需要进行身份验证;
  • 每当向应用程序发送 HTTP 请求时,Spring Security 都会检查Header是否包含Authorization: Basic <credentials>.
  • 如果未设置Header,则服务器会返回 401
  • 如果 Spring Security 找到对应Header,它将进行身份验证。

Spring Security 在进行身份验证时,需要从数据库中查询用户名、密码信息,需要提供一个UserDetailsService接口的实现类,实现该接口中的loadUserByUsername方法。所以我们定义如下接口DatabaseUserDetailsService:

/*** @author 小黑说Java* @ClassName DataBaseUserDetailService* @Description* @date 2022/2/3**/
@Service
@Transactional
public class DataBaseUserDetailService implements UserDetailsService {private final UserDAO userDAO;private final UserMapper userMapper;public DataBaseUserDetailService(UserDAO userDAO, UserMapper userMapper) {this.userDAO = userDAO;this.userMapper = userMapper;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userDAO.selectByUserName(username);return userMapper.toUserDetails(user);}
}

Spring Security中的AuthenticationProvider接口的实现在身份验证时将使用UserDetailsService来执行身份验证逻辑。

AuthenticationProvider接口的实现有很多,因为我们的用户信息存在数据库中,所以我们使用DaoAuthenticationProvider

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {private final DatabaseUserDetailsService databaseUserDetailsService;// constructor ...@Beanpublic AuthenticationProvider daoAuthenticationProvider() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();// 设置密码加密器provider.setPasswordEncoder(passwordEncoder2());// 设置用户信息查询服务provider.setUserDetailsService(this.databaseUserDetailsService);return provider;}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// ...}

到这里,我们已经完成了Spring Security的配置,如果客户端发送带有基本身份验证的Header的HTTP请求,Spring Security将读取该Header信息,根据username获取数据库中的用户信息,并使用BCryptPasswordEncoder 进行密码验证,如果一致则验证通过。如果不一致,服务器将响应 401。

用户注册服务

在验证用户身份之前,我们需要先在数据库中保存用户,也就是用户需要先注册账号。那么我们来实现一个用户注册的接口:

@RestController
public class UserRest {private final UserRegistrationService userRegistrationService;public UserRest(UserRegistrationService userRegistrationService) {this.userRegistrationService = userRegistrationService;}@PostMapping("/registration")@ResponseStatus(code = HttpStatus.CREATED)public void register(@RequestBody UserDTO user) {// 注册用户userRegistrationService.register(user);}
}

按照我们对Spring Security规则的定义,/registration路径的访问不需要进行身份验证。

我们在register方法中调用userRegistrationService.register(user)进行用户注册。

@Service
@Transactional
public class UserRegistrationService {private final UserDAO userDAO;private final PasswordEncoder passwordEncoder;public UserRegistrationService(UserDAO userDAO, PasswordEncoder passwordEncoder) {this.userDAO = userDAO;this.passwordEncoder = passwordEncoder;}public void register(UserDTO userDTO) {User user = new User();user.setUserStatus(UserStatusEnum.INFORCE.getStatus());user.setUsername(userDTO.getUsername());user.setPassword(passwordEncoder.encode(userDTO.getPassword()));userDAO.insert(user);}
}

在进行用户注册时,我们通过PasswordEncoder将用户提供的明文密码进行加密后,保存到数据库中。

小结

以上就是本期的主要内容,我们讲了密码应该如何在系统中保存,最没有常识和安全意识的就是明文保存;使用Hash算法保存会被彩虹表攻击,同样也不可取;而使用加盐Hash加密虽然能一定程度地降低彩虹表攻击的可能性,但是随着硬件性能的发展,同样可能被彩虹表攻击,所以我们应该选择一些特定的密码加密算法。比如
Bcrypt,Pbkdf2,Scrypt,Argon2等。

最后我们通过SpringBoot+Spring Security完成了一个用户登录和注册的功能。

希望本文能对你有所帮助,写文不易,需要一点正反馈,可以的话点个赞吧。

我是小黑,一名在互联网“苟且”的程序员

流水不争先,贵在滔滔不绝

2022年了,密码该如何保存都不会?相关推荐

  1. 2022年商用密码行业市场规模前景调研分析预测及投资建议可行性研究预测

    2022年商用密码行业市场规模前景调研分析预测及投资建议可行性研究预测 (1)中国商用密码行业发展情况:中国商用密码发展起步较晚,历程始于近现代密码时代.整体来看,我国商用密码经历了起步形成.快速发展 ...

  2. 虚拟机中Windows server 2022找回开机密码

    虚拟机中Windows server 2022找回开机密码(2种方式) 第一种:通过系统PE盘进入cmd找回密码 1.在网上下载WE-PE工具箱无网络版.iso(必须是.iso结尾的文件) 2.编辑虚 ...

  3. 如何将网页保存为图片_网页账号密码该如何保存?

    我们在使用浏览器浏览一些网页的时候,需要输入我们的账号密码才能登陆,以保证安全.但是有时候浏览网页,不小心关掉了,重新打开时又要重新输入密码,这样会显得很繁琐.那么有什么办法能让 网页记住我们的账号密 ...

  4. 搜狗浏览器怎么保存账号密码 搜狗浏览器保存账号密码教程

    搜狗浏览器是一款非常安全的电脑浏览器.该软件能够对用户密码有安全保障,下面小编就为您带来搜狗浏览器保存账号密码教程. 搜狗浏览器怎么保存账号密码 搜狗浏览器保存账号密码教程 搜狗浏览器怎么保存账号密码 ...

  5. Aruba7010 默认密码_工信部提醒:手机要及时设置SIM卡密码!很多人都不知道!...

    原标题:工信部提醒:手机要及时设置SIM卡密码!很多人都不知道! 要说现在最必不可少的东西 依小编之见 估计就是手上那部手机啦 看时事新闻.娱乐资讯用它 联系家人.同事和朋友用它 吃喝玩乐也少不了它 ...

  6. Chrome 浏览器有些网站无法保存密码,手动保存

    在 Chrome 地址栏输入「chrome://flags/#enable-password-force-saving」打开实验功能 选择强制保存密码为「已启用」 重启浏览器后打开无法保存密码的网站, ...

  7. Chrome禁用浏览器的密码框自动保存密码提示输入('autocomplete=off'在Chrome中不起作用解决方案),以及密码和用户名回填无问题

    1.Chrome禁用浏览器的密码框自动保存密码提示输入 最近在做项目的时候,要求项目的密码框不显示所记住密码的提示框(就是密码输入框点击不显示该网站所保存的密码列表),然后试了很多方法.autocom ...

  8. windows 11远程桌面连接无法使用已保存的凭据密码,每次连接都要求输入的解决方案

    记录windows 11远程桌面连接无法使用已保存的凭据密码每次都要求输入的问题 主要表现为:每次连接都提示:Windows Defender Credential Guard不允许使用已保存的凭据. ...

  9. 远程桌面如何保存密码--解决每次登录都提示输入密码的问题

    FROM:http://www.veryhuo.com/a/view/80444.html 原标题:解决Windows远程桌面连接每次都提示输入密码的问题 Windows 远程桌面连接几乎每天都用,所 ...

最新文章

  1. java学习笔记13--反射机制与动态代理
  2. 并联串联混合的电压和电流_电流互感器一次绕组串联、并联,二次绕组串联、并联的相关问题...
  3. 通信线路工程验收规范 gb51171-2016_老杨一建通信学堂通信线路工程施工技术
  4. 除夕快乐 | 2月11日 星期四 | B站发文回应热搜风波;美团上线“团好货”独立App;国内首家自动驾驶企业获网约车运营许可...
  5. MySQL学习-group by和having
  6. .sln vcxproj vcxproj.filter文件作用(转载)
  7. linux jboss 多实例,在单个JBoss实例上设置多个端口?
  8. 捷径app 保存视频_Android N App捷径
  9. Java二叉树的层序遍历
  10. 计算机网络二进制转化为十进制,二进制如何转十进制?二进制转换十进制公式...
  11. R 计算变量之间的相关性
  12. zabbix 参数 脚本_zabbix 自定义脚本短信报警
  13. 清华大学百年校庆给清华大学的一封信
  14. 【web系列十一】使用django创建数据库表
  15. 微信公众平台接口测试账号网址
  16. 使用 Python 的铅笔素描图像
  17. 〖文字素材】关于恶魔
  18. kali 安装步骤失败,选择并安装软件包,失败解决方法 “换源”
  19. 来,开局先创建一个 app
  20. 理解了DirectShow播放原理

热门文章

  1. 第一章 概率论的基本概念 1.6 独立性
  2. Fabric v2.3 下载二进制文件和镜像bootstrap.sh脚本解析
  3. 如何替换空格、回车符
  4. vue openlayers 加载高德地图等 gcj02 的图层偏移问题
  5. 为地球减碳1亿吨,阿里云低碳科技加速器发布
  6. js面向对象怎么理解
  7. python3中flask下载文件:图像.jpg
  8. 转发与重定向的区别详解
  9. 《Large scale GAN training for high fidelity natural image synthesis》论文阅读记录
  10. 中文版sublime text3的下载网址和注意方法