Spring Security 進階-加密篇
在 Spring Security 中加密是一個很簡單卻又不能忽略的模組,資料只有加密起來才更安全,這樣就散算據庫密碼洩漏也都是密文。本文分析對應的版本是 5.14。
概念
Spring Security 為我們提供了一套加密規則和密碼比對規則,org.springframework.security.crypto.password.PasswordEncoder 介面,該接口裡面定義了三個方法。
public interface PasswordEncoder { //加密(外面呼叫一般在註冊的時候加密前端傳過來的密碼儲存進資料庫) String encode(CharSequence rawPassword); //加密前後對比(一般用來比對前端提交過來的密碼和資料庫儲存密碼, 也就是明文和密文的對比) boolean matches(CharSequence rawPassword, String encodedPassword); //是否需要再次進行編碼, 預設不需要 default boolean upgradeEncoding(String encodedPassword) { return false; } }
該介面實現類有下面這幾個
其中常用到的分別有下面這麼幾個
- BCryptPasswordEncoder:Spring Security 推薦使用的,使用BCrypt強雜湊方法來加密。
- MessageDigestPasswordEncoder:用作傳統的加密方式加密(支援 MD5、SHA-1、SHA-256...)
- DelegatingPasswordEncoder:最常用的,根據加密型別id進行不同方式的加密,相容性強
- NoOpPasswordEncoder:明文, 不做加密
- 其他
使用示例
MessageDigestPasswordEncoder
構造的時候需要傳入演算法字串,例如 "MD5"、"SHA-1"、"SHA-256"...
String password = "123"; MessageDigestPasswordEncoder encoder = new MessageDigestPasswordEncoder("MD5"); String encode = encoder.encode(password); System.out.println(encode); System.out.println(encoder.matches(password,encode) == true ? "相等" : "不相等");
輸出
{EUjIxnT/OVlk5J54s3LaJRuQgwTchm1gduFHTqI0qjo=}4b40375c57c285cc56c7048bb114db23 相等
呼叫 encode(..)
加密方法每次都會隨機生成鹽值,所以對相同的明文進行多次加密,每次結果也是不一樣的。
從上面輸出部分結合原始碼可以的出:加密的最終結果分為兩部分, 鹽值 + MD5(password+鹽值) ,呼叫 matches(..)
方法的時候先從密文中得到鹽值,用該鹽值加密明文和最終密文作對比。
BCryptPasswordEncoder
構造的時候可以傳入雜湊強度( strength
),強度越大計算量就越大,也就意味著越安全, strength
取值區間[4-31],系統預設是10。
String password = "123"; BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encode = encoder.encode(password); System.out.println(encode); System.out.println(encoder.matches(password, encode) == true ? "相等" : "不相等");
輸出
$2a$10$lxPfE.Zvat6tejB8Q1QGYu3M9lXUUpiWFYzboeyK64kbfgN9v7iBq 相等
呼叫 encode(..)
方法加密跟上面一樣,每次都會隨機生成鹽值,密文也分為兩部分,鹽值和最終加密的結果,最終對比的時候從密文裡面拿出鹽值對明文進行加密,比較最終加密後的結果。
DelegatingPasswordEncoder
這是 Spring Security 推出的一套相容方案,根據加密型別id字串( idForEncode
)去自身快取的所有加密方式中( idToPasswordEncoder
)取出對應的加密方案物件對明文進行加密和對應密文的對比,只是其密文前面都加上了加密方案id的字串,具體的咱們看下面程式碼演示。
其初始化 Spring Security 提供了一個工廠構造方法
public class PasswordEncoderFactories { @SuppressWarnings("deprecation") public static PasswordEncoder createDelegatingPasswordEncoder() { String 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()); return new DelegatingPasswordEncoder(encodingId, encoders); } }
這個工廠的靜態構造方法把常用的幾種方案都注入到快取中,但是注入的 idForEncode
對應的卻是 BCryptPasswordEncoder
,這樣系統就可以達到在新儲存密碼可以使用 BCryptPasswordEncoder
加密方案進行加密,但是對於資料庫裡面以前用其他方式加密的密碼也支援比對。
String password = "123"; PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); String encode = encoder.encode(password); System.out.println(encode); System.out.println(encoder.matches(password, encode) == true ? "相等" : "不相等");
輸出
{bcrypt}$2a$10$Bh23zGZ2YPOsORNexoowb.fX4QH18GEh13eVtZUZvbe2Blx0jIVna 相等
從結果中可以看出,相比原始的 BCryptPasswordEncoder
密文前面多了加密方式的id。
當然也可以自定義構造方法,來制定 DelegatingPasswordEncoder
用其他的方案進行加密。
接下來我們將其指定使用 MD5 方式來加密密碼看看結果
Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); DelegatingPasswordEncoder encoder = new DelegatingPasswordEncoder("MD5", encoders); String encode = encoder.encode(password); System.out.println(encode); System.out.println(encoder.matches(password, encode) == true ? "相等" : "不相等");
輸出
{MD5}{XYwuzP8/lL/a3ASzA9UVM4rFs8lbsLvEoa5ydKER844=}d7f919bfd94554150f8ab3a809209ee3 相等
相比原始的 MessageDigestPasswordEncoder
也是密文前面多了加密方式的id。
應用
先示範下使用系統的 UserDetailsManager
來演示下簡單的注入
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("liuchao") .password("{bcrypt}$2a$10$S.hMD3oV60YRIj38lHRhP.e3DAu3OwmssE/u/p2GLqqZ3SVsZA77W") .roles("admin","user") .and() .passwordEncoder(passwordEncoder); } @Bean(value = "passwordEncoder") public PasswordEncoder delegatingPasswordEncoder() { //構造 DelegatingPasswordEncoder 加密方案 return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Autowired private PasswordEncoder passwordEncoder; }