第三期 - 下
第三期 - 下
用户中心笔记第三期 - 下
1、登录功能
1.1 接口设计
接收参数:用户名、密码
请求类型:POST
请求体:JSON 格式的数据
返回值:用户信息(脱敏)
注意
请求体很长时不建议用 get
1.2 登录逻辑
- 校验用户账户和密码是否合法
- 非空
- 账户长度不小于 4 位
- 密码就不小于 8 位
- 账户不包含特殊字符
- 校验密码是否输入正确,要和数据库中的密文密码(注册时加密后的)去对比
- 用户信息脱敏,隐藏敏感信息,防止数据库中的字段泄露
- 我们要记录用户的登录态(session),将其存到服务器上(用后端 SpringBoot 框架封装的服务器 tomcat 去记录 cookie)
- 返回脱敏后的用户信息
脱敏
信息脱敏(Data Masking)是一种隐私保护技术,通过对敏感数据进行修改或者替换的方
式,来保护数据的隐私和安全。信息脱敏通常应用于需要处理敏感数据的场景,例如测试、开发、分
析等环境。在信息脱敏技术中,被保护的敏感数据通常会被替换成某种规则定义的非敏感数据或者格
式,以避免敏感数据泄露和数据窃取的风险,主要就是防止信息泄露,隐藏敏感信息
1.3 如何知道是哪个用户登录了?
- 连接服务器端后,得到一个 session 状态(匿名会话),返回给前端(用户已经有了会话,但是这个会
在用户登录成功之后才会保存到 Session) - 登录成功后,得到了登录成功的 session,并且给该 session 设置一些值(比如用户信息),返回给前端
一个设置 cookie 的 命令
session => cookie - 前端接收到后端的命令后,设置 cookie,保存到浏览器内
- 前端再次请求后端的时候(相同的域名),在请求头中带上 cookie 去请求
- 后端拿到前端传来的 cookie,找到对应的 session
- 后端从 session 中可以取出基于该 session 存储的变量(用户的登录信息、登录名)
登录实现代码:
/**
* 用户登录实现
* @param userName 用户名
* @param password 用户密码
* @param request 请求对象
* @return 用户信息
*/
@Override
public User userLogin(String userName, String password, HttpServletRequest request) {
// 1.校验用户的用户名、密码、校验密码,是否符合要求
// 1.1 非空校验
if (StringUtils.isAnyBlank(userName, password)) {
return null;
}
// 1.2 用户名长度不小于4位
if (userName.length() < 4) {
return null;
}
// 1.3 密码不小于8位
if (password.length() < 8) {
return null;
}
// 1.4 用户名不包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
// 使用正则表达式进行校验
Matcher matcher = Pattern.compile(validPattern).matcher(userName);
if (matcher.find()) {
return null;
}
// 2.对密码进行md5盐值加密(密码千万不要直接明文存到数据库中)
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + password).getBytes());
// 查询用户是否存在
// TODO:此处有bug,会将逻辑删除的用户也查找出来,在 application.yml 中配置全局逻辑删除属性
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userName);
queryWrapper.eq("userPassword", encryptPassword);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
log.info("user login failed, userAccount cannot match userPassword");
return null;
}
// 3.用户信息脱敏
User safetyUser = getSafetyUser(user);
// 4.用户登录成功
// USER_LOGIN_STATE 用于记录用户的登录状态,后面用户状态优化会提到
request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);
return safetyUser;
}
/**
* 用户脱敏
* @param originUser 原始用户信息
* @return 脱敏后用户信息
*/
@Override
public User getSafetyUser(User originUser) {
if (originUser == null) {
return null;
}
User safetyUser = new User();
safetyUser.setUserId(originUser.getUserId());
safetyUser.setUserName(originUser.getUserName());
safetyUser.setNickName(originUser.getNickName());
safetyUser.setAvatar(originUser.getAvatar());
safetyUser.setGender(originUser.getGender());
safetyUser.setPhone(originUser.getPhone());
safetyUser.setEmail(originUser.getEmail());
safetyUser.setStatus(originUser.getStatus());
safetyUser.setLastTime(originUser.getLastTime());
safetyUser.setUserRole(originUser.getUserRole());
safetyUser.setRemark(originUser.getRemark());
return safetyUser;
}
逻辑删除
逻辑删除是指在数据库中不是真正删除记录,而是标记为已删除,使得这些记录在系统中看起来像已被删除。这比物理删除更加安全、可靠,也能够满足许多场景下的需求。
MyBatis-Plus 有一个逻辑删除,默认会帮助我们查询出来没有被删的用户,官方文档:https://baomidou.com/guides/logic-delete/
按照文档在 application.yml
中配置全局逻辑删除属性:
mybatis-plus:
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除字段名
logic-delete-value: 1 # 1 - 删除
logic-not-delete-value: 0 # 0 - 正常
并记得要在 User
实体类的 isDelete
字段上加上 @TableLogic
注解。
1.4 登录接口
控制层 Controller 的主要工作:
- 接收请求并解析参数
- 调用 Service 执行具体的业务代码(可能包含参数校验)
- 捕获业务逻辑异常做出反馈
- 业务逻辑执行成功做出响应
使用统一的 API 前缀方便管理:
# application.yml 指定接口全局路径前缀
server:
servlet:
context-path: /api
使用控制器注解:
// 适用于编写 restful 风格的 api,返回值默认为 json 类型
@RestController
如果使用 JSON 格式参数的话,最好封装一个对象来记录所有的请求参数,这里我们在 model.domain.request
包下新建两个对象,分别记录注册和登录的请求参数。
/**
* 用户注册请求体
* @author BraumAce
*/
@Data
public class UserRegisterRequest implements Serializable {
// 序列化
private static final long serialVersionUID = 3553317334228624372L;
// 用户名
private String userName;
// 用户密码
private String password;
// 校验密码
private String checkPassword;
}
/**
* 用户登录请求体
* @author BraumAce
*/
@Data
public class UserLoginRequest implements Serializable {
// 序列化
private static final long serialVersionUID = 3553317334228624372L;
// 用户名
private String userName;
// 用户密码
private String password;
}
实现序列化接口
在 implements Serializable
上右键选择 Generate -> serialVersionUID,生成序列化 ID。
不过有可能点击 Generate 后并没有出现 serialVersionUID,于是可以去 settings -> editor -> Inspections,搜索 UID,如下操作:
光标放在 UserRegisterRequest,按下快捷键 Alt+Enter,选择 add'serialVersionUlD'fileld 即可。
最后写一下校验,实现接口的调用,那么校验写在哪里?
controller
层倾向于对请求参数本身的校验,不涉及业务逻辑本身(越少越好)service
层是对业务逻辑的校验(有可能被 controller 之外的类调用)
前面在 service
层已经写好了注册和登录的业务逻辑,接下来在 controller
层实现业务请求。
使用 @RequestMapping
,定义请求的路径,这里设置为 /user
。
/**
* 用户控制器
* @author BraumAce
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
/**
* 用户注册
* @param userRegisterRequest 用户注册请求
* @return 用户ID
*/
@PostMapping("/register")
public Long userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRegisterRequest == null) {
return null;
}
String userName = userRegisterRequest.getUserName();
String password = userRegisterRequest.getPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if (StringUtils.isAnyBlank(userName, password, checkPassword)) {
return null;
}
return userService.userRegister(userName, password, checkPassword);
}
/**
* 用户登录
* @param userLoginRequest 用户登录请求
* @param request 请求对象
* @return 用户信息
*/
@PostMapping("/login")
public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
return null;
}
String userName = userLoginRequest.getUserName();
String password = userLoginRequest.getPassword();
if (StringUtils.isAnyBlank(userName, password)) {
return null;
}
return userService.userLogin(userName, password, request);
}
}
2、用户管理
设置 Session 过期时间为 24 小时
spring:
session:
timeout: 86400
接口设计关键:必须鉴权!!且只能管理员使用
2.1 查询用户
在 UserController
类下编写查询用户接口,不过要先进行管理员校验,只有管理员才能管理用户。如果这个接口不进行校验,那么人人都可以调用,这是非常不安全的。
先进行管理员校验:
private boolean isAdmin(HttpServletRequest request) {
// 管理员校验
User user = (User) request.getSession().getAttribute(USER_LOGIN_STATE);
if (user == null || user.getUserRole() != ADMIN_ROLE) {
return false;
}
return true;
}
实现查询用户接口:
/**
* 查询用户
* @param userName 用户名
* @param request 请求对象
* @return 用户列表
*/
@GetMapping("/search")
public List<User> searchUsers(String userName, HttpServletRequest request) {
// 管理员校验
if (isAdmin(request)) {
return new ArrayList<>();
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(userName)) {
queryWrapper.like("userName", userName);
}
List<User> userList = userService.list(queryWrapper);
return userList.stream()
.map(userService::getSafetyUser)
.collect(Collectors.toList());
}
2.2 删除用户
实现删除用户接口:
/**
* 删除用户
* @param userId 用户ID
* @param request 请求对象
* @return 结果
*/
@DeleteMapping("/delete")
public boolean deleteUser(@RequestBody long userId, HttpServletRequest request) {
if (!isAdmin(request)) {
return false;
}
if (userId < 0 ) {
return false;
}
return userService.removeById(userId);
}
2.3 用户状态优化
为方便前后端联调显示用户的登录状态,有必要在用户登录后记录用户状态。如果将状态直接写在登录逻辑里面,不利于后续维护和拓展,随着后期业务的拓展,其他地方也要调用用户状态,所以需要将用户的各种状态抽出来封装为常量。
我们新建一个 constant
包,用来存储各种常量,新建一个 UserConstant
接口,记录用户的状态,以及再加上两个权限常量,如下:
/**
* 用户常量
* @author BraumAce
*/
public interface UserConstant {
// 用户登录态键
String USER_LOGIN_STATE = "登录成功";
// 默认权限
int DEFAULT_ROLE = 0;
// 管理员权限
int ADMIN_ROLE = 1;
}