Spring Security 的注册登录流程
Spring Security 的注册登录流程
数据库字段设计
主要数据库字段要有:
用户的 ID
用户名称
联系电话
登录密码(非明文)
UserDTO对象
需要一个数据传输对象来将所有注册信息发送到我们的 Spring Boot 后端,该DTO对象应该要拥有所有我们以后创建User对象的所有字段内容:
public class UserDto {
private String userName;
private String password;
private String phone;
// standard getters and setters
}
用户注册控制器
登录页面上的“注册”链接会将用户带到注册页面。该页面的后端位于注册控制器中,并映射到 “/user/registration”,或者你可以使用 PostMan 来发送注册请求到后端,方便测试后端内容。
@PostMapping("/user/registration")
public String register(@RequestBody UserDTO userDTO) {
if (userService.saveUserInfo(userDTO)) {
logger.info("用户注册成功");
return "注册成功";
} else {
logger.error("用户注册失败");
return "注册失败";
}
}
application/json
这个 Content-Type
作为响应头大家肯定不陌生。实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON 字符串。
当控制器收到请求 “/user/registration” 时,它将创建新的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();
}
还可以定义其他的验证,比如说用户名、密码格式之类的。
注册前检查账号是否存在
验证数据库中不存在该电子邮件帐户, 这是在验证表单之后执行的,也是在UserService
的实现的帮助下完成。
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,以检查从持久性层登录的凭据。
@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
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;
}
}
loadUserByUsername
这个函数返回的是一个完全填充的用户记录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
,可以将其视为某些内置功能使用的策略。
启用新的身份验证提供程序
为了能够在 Spring Security 配置新的用户服务,我们只需要添加一个引用到一个UserDetailsService内部认证管理元素,并添加了一个UserDetailsService的bean:
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService);
}
添加对用户认证的自定义AuthenticationProvider
@Autowired
private AuthenticationProvider provider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(provider).userDetailsService(vbUserDetailService);
}
使用BCrypt加密算法
注册过程的关键部分- 密码编码 -基本上不以明文形式存储密码。
在配置中将简单的BCryptPasswordEncoder
定义为bean开始。
@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
BCrypt算法会生成长度为60的字符串,因此我们需要确保密码将存储在可以容纳该密码的列中。一个常见的错误是创建不同长度的列,然后在身份验证时收到“ 无效的用户名或密码”错误。
注册时进行编码:
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
BCrypt算法会生成长度为60的字符串,因此我们需要确保密码将存储在可以容纳该密码的列中。一个常见的错误是创建不同长度的列,然后在身份验证时收到“ 无效的用户名或密码”错误。
把密码编码器加入身份验证配置中
@Override
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
(推测以上hash密码加粗部分为盐,盐的位置是有规律的)
Spring Security 的注册登录流程的更多相关文章
- spring security使用自定义登录界面后,不能返回到之前的请求界面的问题
昨天因为集成spring security oauth2,所以对之前spring security的配置进行了一些修改,然后就导致登录后不能正确跳转回被拦截的页面,而是返回到localhost根目录. ...
- Spring Security OAuth2 实现登录互踢
背景说明 一个账号只能一处登录,类似的业务需求在现有后管类系统是非常常见的. 但在原有的 spring security oauth2 令牌方法流程(所谓的登录)无法满足类似的需求. 我们先来看 To ...
- Spring Security OAuth2 单点登录
1. OAuth 2.0 OAuth(Open Authorization)为用户资源的授权提供了一个安全的.开放而又简易的标准.最简单的理解,我们可以看一下微信OAuth2.0授权登录流程: 通过O ...
- 如何设计一个 App 的注册登录流程?
移 动设备发力之前的登录方式很简单:用户名/邮箱+密码+确认密码,所有的用户登录注册都是围绕着邮箱来做.随着移动设备和社交网络的普及,邮箱不再是唯 一,渐渐的出现了微博,QQ,微信等第三方登录方式,手 ...
- Spring Security 概念基础 验证流程
Spring Security 概念基础 验证流程 认证&授权 认证:确定是否为合法用户 授权:分配角色权限(分配角色,分配资源) 认证管理器(Authentication Manager) ...
- spring security之 默认登录页源码跟踪
spring security之 默认登录页源码跟踪 2021年的最后2个月,立个flag,要把Spring Security和Spring Security OAuth2的应用及主流程源码研究透 ...
- Spring Security Oauth2 单点登录案例实现和执行流程剖析
Spring Security Oauth2 OAuth是一个关于授权的开放网络标准,在全世界得到的广泛的应用,目前是2.0的版本.OAuth2在“客户端”与“服务提供商”之间,设置了一个授权层(au ...
- 认证与授权】Spring Security系列之认证流程解析
上面我们一起开始了Spring Security的初体验,并通过简单的配置甚至零配置就可以完成一个简单的认证流程.可能我们都有很大的疑惑,这中间到底发生了什么,为什么简单的配置就可以完成一个认证流程啊 ...
- spring security整合QQ登录
最近在了解第三方登录的内容,尝试对接了一下QQ登录,此次记录一下如何实现QQ登录的过程,在这个例子中是和spring secuirty整合的,不整合spring secuirty也是一样的. 需求: ...
随机推荐
- go 1.14上怎么下载第三方包
终端 go env -w GO111MODULE=on GOPATH-->src/pkg/bin in src源码包中 某个包中 go mod init XXX(表示当前报的第三方依赖) 然 ...
- 第25篇-虚拟机对象操作指令之putstatic
之前已经介绍了getstatic与getfield指令的汇编代码执行逻辑,这一篇介绍putstatic指令的执行逻辑,putfield将不再介绍,大家可以自己去研究,相信大家有这个实力. putsta ...
- Apache Dolphin Scheduler - Dockerfile 详解
Apache DolphinScheduler 是一个分布式去中心化,易扩展的可视化 DAG 工作流任务调度系统.简称 DS,包括 Web 及若干服务,它依赖 PostgreSQL 和 Zookeep ...
- 学生信息管理系统.cpp(大二上)
#include<iostream> #include<fstream> #include<string> #include<iomanip> #i ...
- PHP中的数据库连接持久化
数据库的优化是我们做web开发的重中之重,甚至很多情况下其实我们是在面向数据库编程.当然,用户的一切操作.行为都是以数据的形式保存下来的.在这其中,数据库的连接创建过程有没有什么可以优化的内容呢?答案 ...
- 一起搞懂PHP的错误和异常(二)
上回文章中我们讲到了错误是编译和语法运行时会出现的,它们与逻辑无关,是程序员在码代码时不应该出现的,也就是说,这些错误应该是尽量避免带到线上环境的,他们不能通过try...catch捕获到.而异常则正 ...
- learn git(远程仓库github)
|由于本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置: 第1步:创建SSH Key.在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsa ...
- Linux系列(22) - 用户登录查看命令
需求 查看当前在线用户情况:历史用户登录情况 W 格式 [root@localhost ~]# w:查看所有登录用户信息 [root@localhost ~]# w [用户名]:查看指定登录用户信息 ...
- LeetCode2-链表两数和
目录 LeetCode2-链表两数和 题目描述 示例提示 经验教训 参考正解 题目描述 示例提示 经验教训 链表题的判空条件不是万能的,有时候示例会极其复杂,根本难以通过判空来区分不同情况. /** ...
- CI框架 core
https://blog.csdn.net/admin_admin/article/details/51769805 1.扩展控制器 1.在application/core新建一个自己的控制器(MY_ ...