數據庫字段設計
主要數據庫字段要有:
-
用戶的 ID
-
用戶名稱
-
聯系電話
-
登錄密碼(非明文)
public class UserDto {
private String userName;
private String password;
private String phone;
// standard getters and setters
}
"/user/registration") (
public String register( UserDTO userDTO) {
if (userService.saveUserInfo(userDTO)) {
logger.info("用戶注冊成功");
return "注冊成功";
} else {
logger.error("用戶注冊失敗");
return "注冊失敗";
}
}
這個 Content-Type
作為響應頭大家肯定不陌生。實際上,現在越來越多的人把它作為請求頭,用來告訴服務端消息主體是序列化后的 JSON 字符串。
UserDTO
對象,該對象將獲取請求頭Content-Type: application/json
的輸入流內容,在json_decode
成對象。
private static final String MOBILE_CM_AREA_REX = "^(13[0-9]{9}$|14[0-9]{9}|15[0-9]{9}$|17[0-9]{9}$|18[0-9]{9})$";
正則表達式的編譯
private static final Pattern MOBILE_CM_AREA_REX_PATTERN = Pattern
.compile(MOBILE_CM_AREA_REX);
調用 Pattern 對象的 matcher 方法來獲得一個 Matcher 對象,對輸入字符串進行解釋和匹配操作
public static boolean isMobileCM(String mobile) {
return MOBILE_CM_AREA_REX_PATTERN.matcher(mobile).matches();
}
還可以定義其他的驗證,比如說用戶名、密碼格式之類的。
public boolean checkAccountByPhone(UserDTO userDTO) {
boolean flags;
... // check account from database or other ways
if (檢查出賬戶已存在存在) {
throw new UserAlreadyExistException(
"There is an account with that email address: "
+ userDTO.getPhone());
}
return flags;
}
保留注冊數據並完成表單處理
在控制器層中實現注冊邏輯,成功后通知前端或者Postman注冊結果。
加載安全性登錄的用戶詳細信息
之前討論的登錄驗證時使用的是硬編碼憑據。讓我們進行更改,並使用新注冊的用戶信息和憑據。我們將實現一個自定義UserDetailsService,以檢查從持久性層登錄的憑據。
public class MyUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDTO userDTO = userRepository.selectOneByUsername(username);
if (userDTO == null) {
logger.warn("用戶" + username + "不存在");
throw new UsernameNotFoundException("用戶" + username + "不存在");
}
userDTO.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(userDTO.getRoles()));
return userDTO;
}
}
這個函數返回的是一個完全填充的用戶記錄UserDetail
對象,為用戶加載信息的最常見方法。UserDetails
將用於構建Authentication
存儲在中的對象SecurityContextHolder
。那這個函數什么時候被調用呢?(參考1,參考2,參考3)
-
AuthenticationProvider
實例調用,以認證用戶。例如,提交用戶名和密碼后,將UserdetailsService
被調用來查找該用戶的密碼以查看其是否正確。通常,它還將提供有關用戶的其他信息,例如權限和你可能希望為已登錄用戶(例如電子郵件)訪問的任何自定義字段。那是主要的使用模式。關於UserDetailsService
經常會有一些困惑。它純粹是用於用戶數據的DAO層,除了將數據提供給框架內的其他組件外,不執行其他功能。特別是,它不對用戶進行身份驗證,這由AuthenticationManager完成。在許多情況下,如果您需要自定義身份驗證過程,則直接實現AuthenticationProvider更有意義。 -
用戶通過身份驗證后,會將
SecurityContext
實例存儲在會話中。根據應用程序的類型,可能需要制定一種策略來存儲用戶操作之間的SecurityContext
。在典型的Web應用程序中,用戶登錄一次,然后通過其會話ID進行標識。服務器緩存持續時間會話的主體信息。在Spring Security中,請求之間存儲SecurityContext
的責任落在SecurityContextPersistenceFilter
,默認情況下,HTTP請求之間將上下文存儲為HttpSession
的屬性。 -
如果你需要實現自定義UserDetailsService,則將取決於您的要求及其存儲方式。通常,你將在與其他用戶信息同時加載它們。你可能不會在過濾器中執行此操作。如上述參考手冊中的引用所述,如果你·實際上要實現其他身份驗證機制,則應直接實現
AuthenticationProvider
。你的應用程序中沒有是強制性的要有UserDetailsService
,可以將其視為某些內置功能使用的策略。
private MyUserDetailsService userDetailsService;
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService);
}
private AuthenticationProvider provider;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(provider).userDetailsService(vbUserDetailService);
}
BCrypt加密算法
注冊過程的關鍵部分- 密碼編碼 -基本上不以明文形式存儲密碼。
在配置中將簡單的BCryptPasswordEncoder
定義為bean開始。
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
以下PasswordEncoderFactories
會默認使用BCryptPasswordEncoder
編碼
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());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
BCrypt 會在內部生成隨機鹽。因為這意味着每個調用都會有不同的結果,因此我們只需要對密碼進行一次編碼。注意,即使是相同密碼明文,兩次調用編碼后得到的結果也不是一樣的。
BCrypt 把密碼編碼后通常長這樣子:
{bcrypt}$2a$10$BQ2AivawsVvTmnkzETQ6s.OAcHuafwsCJ9e6x0ScHybWlY7Xh1QlC
算法會生成長度為60的字符串,因此我們需要確保密碼將存儲在可以容納該密碼的列中。一個常見的錯誤是創建不同長度的列,然后在身份驗證時收到“ 無效的用戶名或密碼”錯誤。
注冊時進行編碼:
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
算法會生成長度為60的字符串,因此我們需要確保密碼將存儲在可以容納該密碼的列中。一個常見的錯誤是創建不同長度的列,然后在身份驗證時收到“ 無效的用戶名或密碼”錯誤。
把密碼編碼器加入身份驗證配置中
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(provider).userDetailsService(vbUserDetailService)
.passwordEncoder(passwordEncoder());
}
那么自定義用戶身份驗證中,如何對前端返回的密碼與數據庫中的密碼進行核驗。把明文密碼進行編碼來.equals()
?不是這樣,前面說過,每個調用編碼器都會有不同的結果,即使是對相同的明文密碼。可以使用passwordEncoder
里面的matches
方法來判斷, 畢竟每次加密相同密碼存進數據庫的都不一樣的。
String encodePwd = passwordEncoder.encode(password); // 這里僅僅是為了調試的時候驗證每次BCrypt編碼器用的是隨機鹽
String dbPwd = userInfo.getPassword();
if (!passwordEncoder.matches(password, dbPwd)) {
logger.warn("密碼不正確");
throw new BadCredentialsException("密碼不正確");
}
這個matches
方法會先對前端傳來的進行相同方式加密的密碼進行判空,然后檢查是不是對應的編碼格式。然后才對前端傳來密碼串和數據庫中的密碼串進行核對,檢查明文密碼是否與數據哈希密碼匹配。具體一點就是說,matches
的工作是先檢查dbPwd
的格式,使用的編碼器類型,然后再由DelegatingPasswordEncoder
轉發給對用類型的BCryptPasswordEncoder
來處理,它提取了數據庫中的先前密碼hash過的值中,取出當時hash所用的鹽,然后再把password
和這個鹽進行編碼,返回通過一樣的鹽hash出的字符串,最后在進行簡單數組對比。matches
方法返回true
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);
}
上面代碼中相關變量的舉例說明:
前端傳來的密碼password
:
123
前端編碼后matches
方法外的encodePwd
:
{bcrypt}$2a$10$0wPZ/Gth9qcB6ALJ6XYMs.TffeGBkn/a7EJz0C9IGIVQRzfcek81i
數據庫中先前編碼好的密碼hash串dbPwd
:
{bcrypt}$2a$10$BQ2AivawsVvTmnkzETQ6s.OAcHuafwsCJ9e6x0ScHybWlY7Xh1QlC