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

工作流程
WebSecurityConfigurerAdapter 初始化密码解析器时,如果没有自定义 Bean 的话,会默认初始化 DelegatingPasswordEncoder



DaoAuthenticationProvider 在 additionalAuthenticationChecks 方法中会获取 Spring 容器中的 PasswordEncoder 来对用户输入的密码进行比较
BCryptPasswordEncoder 密码匹配流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public class BCryptPasswordEncoder implements PasswordEncoder { private Pattern BCRYPT_PATTERN = Pattern .compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}"); private final Log logger = LogFactory.getLog(getClass());
private final int strength; private final BCryptVersion version;
private final SecureRandom random;
public String encode(CharSequence rawPassword) { if (rawPassword == null) { throw new IllegalArgumentException("rawPassword cannot be null"); }
String salt; if (random != null) { salt = BCrypt.gensalt(version.getVersion(), strength, random); } else { salt = BCrypt.gensalt(version.getVersion(), strength); } return BCrypt.hashpw(rawPassword.toString(), salt); }
public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.length() == 0) { logger.warn("Empty encoded password"); return false; }
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) { logger.warn("Encoded password does not look like BCrypt"); return false; }
return BCrypt.checkpw(rawPassword.toString(), encodedPassword); } }
|
从数据库得到的“密码”(参数: salt )进行一系列校验(长度校验等)并截取“密码”中相应的密码盐,利用这个密码盐进行同样的一系列计算 Hash 操作和 Base64 编码拼接一些标识符生成所谓的“密码”,最后 equalsNoEarlyReturn 方法对同一个密码盐生成的两个“密码”进行匹配
- 每次使用 BCryptPasswordEncoder 编码同一个密码都是不一样的,因为用到的随机密码盐每次都是不一样的,同一个密码和不同的密码盐组合计算出来的 Hash 值不一样
- BCryptPasswordEncoder 编码同一个密码后结果都不一样,怎么进行匹配?因为密码盐是随机生成的,但是可以根据数据库查询出来的“密码”拿到密码盐,同一个密码盐+原密码计算 Hash 结果值是能匹配的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| public class BCrypt { public static String hashpw(String password, String salt) { byte passwordb[]; passwordb = password.getBytes(StandardCharsets.UTF_8); return hashpw(passwordb, salt); } public static String hashpw(byte passwordb[], String salt) { BCrypt B; String real_salt; byte saltb[], hashed[]; char minor = (char) 0; int rounds, off; StringBuilder rs = new StringBuilder(); if (salt == null) { throw new IllegalArgumentException("salt cannot be null"); } int saltLength = salt.length(); if (saltLength < 28) { throw new IllegalArgumentException("Invalid salt"); } if (salt.charAt(0) != '$' || salt.charAt(1) != '2') throw new IllegalArgumentException ("Invalid salt version"); if (salt.charAt(2) == '$') off = 3; else { minor = salt.charAt(2); if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') throw new IllegalArgumentException ("Invalid salt revision"); off = 4; } if (salt.charAt(off + 2) > '$') throw new IllegalArgumentException ("Missing salt rounds");
if (off == 4 && saltLength < 29) { throw new IllegalArgumentException("Invalid salt"); } rounds = Integer.parseInt(salt.substring(off, off + 2));
real_salt = salt.substring(off + 3, off + 25); saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
if (minor >= 'a') passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
B = new BCrypt(); hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0); rs.append("$2"); if (minor >= 'a') rs.append(minor); rs.append("$"); if (rounds < 10) rs.append("0"); rs.append(rounds); rs.append("$"); encode_base64(saltb, saltb.length, rs); encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs); return rs.toString(); }
public static boolean checkpw(String plaintext, String hashed) { return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed)); }
public static boolean equalsNoEarlyReturn(String a, String b) { return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8)); } }
|
自定义密码解析器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class MyMD5PasswordEncoder implements PasswordEncoder {
@Override public String encode(CharSequence charSequence) { try { MessageDigest digest = MessageDigest.getInstance("MD5"); return toHexString(digest.digest(charSequence.toString().getBytes())); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return ""; } }
@Override public boolean matches(CharSequence charSequence, String s) { return s.equals(encode(charSequence)); }
private String toHexString(byte [] tmp){ StringBuilder builder = new StringBuilder(); for (byte b :tmp){ String s = Integer.toHexString(b & 0xFF); if (s.length()==1){ builder.append("0"); } builder.append(s); } return builder.toString(); } }
|
1 2 3 4 5 6 7 8 9
| @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean public PasswordEncoder passwordEncoder(){ return new MyMD5PasswordEncoder(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Slf4j @Component public class UserDetailServiceImpl implements UserDetailsService {
@Autowired private PasswordEncoder passwordEncoder;
@Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { String password = passwordEncoder.encode("123"); log.info("登录用户:{},密码:{}", s, password); return new User(s, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
|