数据库字段设计
主要数据库字段要有:
-
用户的 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