博主在之前已经介绍了Spring Security的用户UserDetails与用户服务UserDetailsService,本篇博客介绍Spring Security的密码编码器PasswordEncoder,它们是相互联系的,博主会带大家一步步深入理解Spring Security的实现原理,也会带来Spring Security的实战分享。

  • Spring Security:用户UserDetails源码与Debug分析
  • Spring Security:用户服务UserDetailsService源码分析

为什么是介绍而不是源码分析?博主虽然在研一上过密码学的课,但毕竟没有细致研究过密码学领域,因此不敢管中窥豹。再者,这些加密算法的实现原理、是否能抵御攻击、明文与密文(明文经过加密得到)的匹配方法以及时间成本等因素都不是学习Spring Security框架的核心内容,因此本篇博客只会简单介绍Spring Security的密码编码器PasswordEncoder及其实现类,以及密码编码器在Spring Security中的使用时机。

PasswordEncoder

PasswordEncoder接口有很多实现类,也有被标记了@Deprecated注解的实现类,一般是该类表示的密码编码器(加密算法)不安全,比如可以在能接受的时间内被破解,比如彩虹表攻击。

这里不去分析每个密码编码器的实现原理,因为密码编码器的种类太多了,而且没有必要,密码编码器的主要作用无非就是对密码进行编码(加密),以及原始密码(客户端登录验证时输入的密码)与编码密码(正确原始密码通过密码编码器编码的结果)的正确匹配,因此密码编码器必定需要实现PasswordEncoder接口的两个方法,而其他方法的实现是服务于这两个方法。

package org.springframework.security.crypto.password;// 首选实现是BCryptPasswordEncoder
public interface PasswordEncoder {/*** 对原始密码进行编码*/String encode(CharSequence rawPassword);/*** 验证从存储(比如数据库或者内存等)中获取的编码密码是否与需要验证的密码匹配 * 如果密码匹配,则返回 true,否则返回 false* 存储的编码密码永远不会被解码* 因此会将需要验证的密码进行编码,然后与编码密码进行匹配*/boolean matches(CharSequence rawPassword, String encodedPassword);/*** 如果为了更好的安全性需要再次对编码的密码进行编码,则返回 true,否则返回 false* 默认实现始终返回 false*/default boolean upgradeEncoding(String encodedPassword) {return false;}
}

很显然密码编码器的主要作用是为了编码与匹配,而有些加密算法需要经过多次迭代加密,因此也需要实现upgradeEncoding方法,比如BCryptPasswordEncoder类的实现(strength属性越大,需要做更多的工作来加密密码,默认值为10):

 @Overridepublic boolean upgradeEncoding(String encodedPassword) {if (encodedPassword == null || encodedPassword.length() == 0) {logger.warn("Empty encoded password");return false;}Matcher matcher = BCRYPT_PATTERN.matcher(encodedPassword);if (!matcher.matches()) {throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);}else {int strength = Integer.parseInt(matcher.group(2));return strength < this.strength;}}

PasswordEncoderFactories

PasswordEncoderFactories类源码:

package org.springframework.security.crypto.factory;import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;import java.util.HashMap;
import java.util.Map;/*** 用于创建PasswordEncoder实例*/
public class PasswordEncoderFactories {/*** 使用默认映射创建一个DelegatingPasswordEncoder* 可能会添加其他映射,并且将更新编码以符合最佳实践* 但是,由于DelegatingPasswordEncoder的性质,更新不应影响用户*/@SuppressWarnings("deprecation")public static PasswordEncoder createDelegatingPasswordEncoder() {// 默认bcryptString encodingId = "bcrypt";Map<String, PasswordEncoder> encoders = new HashMap<>();encoders.put(encodingId, new BCryptPasswordEncoder());encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());encoders.put("scrypt", new SCryptPasswordEncoder());encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());encoders.put("argon2", new Argon2PasswordEncoder());return new DelegatingPasswordEncoder(encodingId, encoders);}private PasswordEncoderFactories() {}
}

PasswordEncoderFactories类可以看作密码编码器工厂,它将已有的密码编码器存储在HashMap中,通过静态方法createDelegatingPasswordEncoder即可获取,该方法的返回值是一个DelegatingPasswordEncoder实例。

DelegatingPasswordEncoder

DelegatingPasswordEncoder类源码:

public class DelegatingPasswordEncoder implements PasswordEncoder {private static final String PREFIX = "{";private static final String SUFFIX = "}";private final String idForEncode;private final PasswordEncoder passwordEncoderForEncode;private final Map<String, PasswordEncoder> idToPasswordEncoder;private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();/*** 创建一个新实例* idForEncode:用于查找应使用哪个PasswordEncoder进行encode* idToPasswordEncoder:id到PasswordEncoder的映射,用于确定应使用哪个PasswordEncoder进行matches*/public DelegatingPasswordEncoder(String idForEncode,Map<String, PasswordEncoder> idToPasswordEncoder) {if (idForEncode == null) {throw new IllegalArgumentException("idForEncode cannot be null");}if (!idToPasswordEncoder.containsKey(idForEncode)) {throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);}for (String id : idToPasswordEncoder.keySet()) {if (id == null) {continue;}// 如果id有'{'或者'}'字符则会出现问题,比如:// 基于prefixEncodedPassword获取id,是根据'{'和'}'字符对第一次出现的位置来截取// 以及去除{id}得到encodedPassword,是根据'}'字符第一次出现的位置来截取if (id.contains(PREFIX)) {throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);}if (id.contains(SUFFIX)) {throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);}}// 初始化this.idForEncode = idForEncode;this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);}/*** 设置defaultPasswordEncoderForMatches,默认为UnmappedIdPasswordEncoder实例*/public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) {if (defaultPasswordEncoderForMatches == null) {throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");}this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;}// 编码,{id}前缀拼接委托的PasswordEncoder的编码结果@Overridepublic String encode(CharSequence rawPassword) {return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);}// 匹配@Overridepublic boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {if (rawPassword == null && prefixEncodedPassword == null) {return true;}// 根据prefixEncodedPassword提取idString id = extractId(prefixEncodedPassword);// 根据id获取PasswordEncoder PasswordEncoder delegate = this.idToPasswordEncoder.get(id);// 是否有对应的PasswordEncoder if (delegate == null) {// 没有对应的PasswordEncoder// 则使用defaultPasswordEncoderForMatches进行匹配return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);}// 有对应的PasswordEncoder// 提取encodedPassword,即去掉{id}前缀String encodedPassword = extractEncodedPassword(prefixEncodedPassword);// 返回匹配结果return delegate.matches(rawPassword, encodedPassword);}// 提取idprivate String extractId(String prefixEncodedPassword) {if (prefixEncodedPassword == null) {return null;}// 第一个'{'字符的位置int start = prefixEncodedPassword.indexOf(PREFIX);if (start != 0) {return null;}// 从start开始的第一个'}'字符的位置int end = prefixEncodedPassword.indexOf(SUFFIX, start);if (end < 0) {return null;}// 截取得到idreturn prefixEncodedPassword.substring(start + 1, end);}@Overridepublic boolean upgradeEncoding(String prefixEncodedPassword) {// 提取idString id = extractId(prefixEncodedPassword);// id与idForEncode属性不匹配,则返回trueif (!this.idForEncode.equalsIgnoreCase(id)) {return true;}else {// 提取encodedPassword String encodedPassword = extractEncodedPassword(prefixEncodedPassword);// 根据id获取PasswordEncoder// 返回该PasswordEncoder的upgradeEncoding方法基于encodedPassword的返回值return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);}}// 提取encodedPasswordprivate String extractEncodedPassword(String prefixEncodedPassword) {// 第一个'}'字符的位置int start = prefixEncodedPassword.indexOf(SUFFIX);// 截取得到encodedPasswordreturn prefixEncodedPassword.substring(start + 1);}/*** 引发异常的默认PasswordEncoder*/private class UnmappedIdPasswordEncoder implements PasswordEncoder {@Overridepublic String encode(CharSequence rawPassword) {throw new UnsupportedOperationException("encode is not supported");}@Overridepublic boolean matches(CharSequence rawPassword,String prefixEncodedPassword) {String id = extractId(prefixEncodedPassword);throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");}}
}

DelegatingPasswordEncoder是基于前缀标识符并委托给另一个PasswordEncoder的密码编码器,可以使用PasswordEncoderFactories类创建一个DelegatingPasswordEncoder实例,也可以创建自定义的DelegatingPasswordEncoder实例。

密码存储格式为 {id}encodedPasswordprefixEncodedPassword),id是用于查找应该使用哪个PasswordEncoder的标识符,encodedPassword是使用PasswordEncoder对原始密码进行编码的结果,id必须在密码的开头,以{开头,}结尾。 如果找不到id,则id将为空。 例如,以下可能是使用不同idPasswordEncoder)编码的密码列表:

  • {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
  • {noop}password
  • {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
  • {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
  • {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

第一个密码的idbcryptencodePassword$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG,匹配时,将委托给BCryptPasswordEncoder。第二个密码的idnoopencodePasswordpassword。匹配时,将委托给NoOpPasswordEncoder。以此类推。

传递给构造函数的idForEncode确定将使用哪个PasswordEncoder来编码原始密码。匹配是基于id和构造函数中提供的idToPasswordEncoder来完成的。matches方法可能使用带有未映射id(包括空id)的密码,调用matches方法将抛出IllegalArgumentException异常, 可以使用setDefaultPasswordEncoderForMatches方法自定义此行为,即设置defaultPasswordEncoderForMatches属性,当根据id获取不到PasswordEncoder时使用。

Debug分析

依赖:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.kaven</groupId><artifactId>security</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.1.RELEASE</version></parent><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies>
</project>

接口:

@RestController
public class MessageController {@GetMapping("/message")public String getMessage() {return "hello kaven, this is security";}
}

启动类:

@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class);}
}

Debug启动应用,访问接口,会被重定向到默认登录页。

使用Spring Security自动创建的用户(用户名为user,密码在启动日志中)进行登录验证。

用户验证时Spring Security会使用DelegatingPasswordEncoder类的matches方法进行密码匹配,提取的idnoopprefixEncodedPassword{noop}前缀,应用启动时,如果没有用户与用户源的相关配置,Spring Security会创建一个默认用户,即一个UserDetails实例,该实例的密码就是prefixEncodedPassword),因此委托给NoOpPasswordEncoder进行密码匹配。

NoOpPasswordEncoder类的matches方法只是简单的字符串匹配,上图的rawPasswordencodedPassword很显然是匹配的。

 public boolean matches(CharSequence rawPassword, String encodedPassword) {return rawPassword.toString().equals(encodedPassword);}

验证成功。

配置PasswordEncoder

增加配置:

package com.kaven.security.config;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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 重写验证处理的配置@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());}// 自定义的用户服务public static class UserDetailsServiceImpl implements UserDetailsService {// 使用PasswordEncoderFactories工厂创建DelegatingPasswordEncoder实例作为该用户服务的密码编码器private static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// TODO 查找数据库// 使用密码编码器对原始密码进行编码String encodedPassword = PASSWORD_ENCODER.encode("itkaven");// 默认存在该用户名的用户,并且原始密码都为itkaven,角色都为USER和ADMINreturn User.withUsername(username).password(encodedPassword).roles("USER", "ADMIN").build();}}
}

Debug启动应用,访问接口,然后进行验证登录,由于自定义的用户服务默认任意用户名的用户都存在,并且原始密码都为itkaven,角色都为USERADMIN,因此登录时用户名可任意,但密码必须为itkaven才能通过验证。

客户端进行验证登录时,Spring Security通过用户服务加载匹配用户名的UserDetails实例,而博主自定义的用户服务直接默认该UserDetails实例存在,并且设置默认的密码(编码后的密码,使用UserDetailsServiceImpl类中的PASSWORD_ENCODER进行编码)与角色(权限)。密码匹配使用在重写验证处理的配置时指定的密码编码器来完成(passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()))。

idbcrypt,使用BCryptPasswordEncoder进行密码匹配,因为通过自定义的用户服务加载的UserDetails实例的密码就是DelegatingPasswordEncoder{bcrypt}前缀与BCryptPasswordEncoder对原始密码(itkaven)的编码的拼接,最后会返回true

所以,密码编码器在用户验证时用于密码的匹配,以及创建UserDetails实例时对密码进行编码(可选,如果是基于用户服务加载的UserDetails实例创建的新实例,新实例一般不更改该UserDetails实例的密码,因此,通过用户服务加载的UserDetails实例的密码应该是编码后的密码),因此密码的编码与匹配过程需要使用相同的密码编码器,不然一样的原始密码也有可能匹配不成功。

Spring Security的密码编码器PasswordEncoder的介绍与Debug分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

Spring Security:密码编码器PasswordEncoder介绍与Debug分析相关推荐

  1. Spring Security:身份验证令牌Authentication介绍与Debug分析

    在Spring Security中,通过Authentication来封装用户的验证请求信息,Authentication可以是需要验证和已验证的用户请求信息封装.接下来,博主介绍Authentica ...

  2. Spring Security:身份验证入口AuthenticationEntryPoint介绍与Debug分析

    ExceptionTranslationFilter ExceptionTranslationFilter(Security Filter)允许将AccessDeniedException和Authe ...

  3. 【Spring Security】基本功能介绍

    文章目录 1.spring security 简介 spring security 基本原理 2 入门项目 2.1 web工程配置 2.1 加入Spring Security 3. 参数详解 3.1. ...

  4. Spring Security 密码验证动态加盐的验证处理

    本文个人博客地址:https://www.leafage.top/posts/detail/21697I2R 最近几天在改造项目,需要将gateway整合security在一起进行认证和鉴权,之前ga ...

  5. Spring Security 详解与实操第一节 认证体系与密码安全

    开篇词 Spring Security,为你的应用安全与职业之路保驾护航 你好,我是鉴湘,拉勾教育专栏<Spring Cloud 原理与实战><Spring Boot 实战开发> ...

  6. 【Spring Security】解答Spring Boot 中密码加密的正确方式?

    Spring Boot 项目中密码如何加密 先说一句:密码是采用非对称加密是无法解密的.密码无法解密,还是为了确保系统安全.今天就来和大家聊一聊,密码要如何处理,才能在最大程度上确保我们的系统安全.密 ...

  7. Spring Security中的密码安全

    Spring Security中的密码安全 PasswordEncoder 接口 在 Spring Security 中,PasswordEncoder 接口代表的是一种密码编码器,其核心作用在于指定 ...

  8. Spring Security 与 OAuth2 介绍

    个人 OAuth2 全部文章 Spring Security 与 OAuth2(介绍):https://www.jianshu.com/p/68f22f9a00ee Spring Security 与 ...

  9. Spring Boot 密码加密的 2 种姿势!

    先说一句:密码是无法解密的. 密码无法解密,还是为了确保系统安全.今天松就来和大家聊一聊,密码要如何处理,才能在最大程度上确保我们的系统安全. 1.为什么要加密 2011 年 12 月 21 日,有人 ...

最新文章

  1. 一款好用 mongodb 可视化工具
  2. pthread_mutex_lock的thread特性
  3. Java基础——Servlet(六)分页相关
  4. 学习笔记(十四)——MySQL(CRUD)
  5. 新浪微博搜索php待遇,新浪微博面试
  6. 大于2T硬盘通过UEFI启动+GPT分区表安装Server 2008 R2
  7. 当兵的目标和计划_“士兵计划”与我的那些事儿——【初试篇】
  8. 《我也能做CTO之程序员职业规划》之七:大学生职业规划技巧
  9. spring加载bean的流程
  10. 【D】分布式系统的CAP理论
  11. JavaScript : 基本的处理事件
  12. 使用Docker 安装jdk8
  13. 流媒体协议(二):RTMP协议
  14. 数学模型——Logistic回归模型(含Matlab代码)
  15. sqlserver修改主键id自增
  16. Pixelmator for Mac(全能图像编辑软件)
  17. 三、常规Dos命令附图
  18. XML学习-方立勋视频学习
  19. JavaScript:将输入的一串数字转换成中文大写,最高可写12位(千亿)
  20. 如何提高固态硬盘的读取速度

热门文章

  1. 《隐私计算应用研究报告(2022年)》:规模将达到145.1亿元
  2. Socket编程之地址之间转换、字节序转换
  3. 实名认证挂号订单就诊人管理管理员对就诊人管理
  4. PostgreSQL vs MySQL——哪种关系数据库更好?
  5. 智能汽车时代,“BATH”的跨界姿势与逻辑
  6. 计算机毕业设计Java智能旅游电子票务系统演示录像2020(源码+系统+mysql数据库+lw文档)
  7. nginx启动成功,但是访问不了页面解决办法
  8. SQL注入上传一句话木马
  9. 查看端口被占用的情况以及如何解除端口占用
  10. mint-ui使用手册