业务逻辑
表的设计
开辟原则 尽量减少修改 只做拓展 初始化项目的时候没必要设计的太复杂 过度设计之后发现用不着其实更麻烦
设计表要思考什么
有哪些表(模型)?
有哪些字段?
字段有哪些类型
(数据库字段添加索引? 表与表之间的关联?)
对于用户表来说
id主键、userName、userAccount、avatarUrl、gender、password、phone、email 是比较基础的
另外 一些经验性的 几乎每个表都要必备的字段 isVaild(userStatus)、createTime、updateTime、isDelete
isDelete与业务没有任何关系 但每个表都得有它 它意味着逻辑删除 因为删除数据是个非常危险的操作 用这种逻辑删除的方法 哪怕是删除了也能够找回
是否与isVaild功能重复呢 并不 isVaild与业务相关 表示该用户的账号是否有效是否被封
id(主键)bigint
username 昵称 varchar
userAccount 登录账号
avatarUrl 头像 varchar
gender 性别 tinyint
userPassword 密码 varchar
phone 电话 varchar
email 邮箱 varchar
userStatus 用户状态 int 0 - 正常
createTime 创建时间(数据插入时间)datetime
updateTime 更新时间(数据更新时间)datetime
isDelete 是否删除 0 1(逻辑删除)tinyint
userRole 用户角色 0 - 普通用户 1 - 管理员
create table user
(
username varchar(256) null comment '用户昵称',
id bigint auto_increment comment 'id' primary key ,
userAccount varchar(256) null comment '账号',
avatarUrl varchar(1024) null comment '用户头像',
gender tinyint null comment '性别',
userPassword varchar(512) null comment '密码',
phone varchar(128) null comment '电话',
email varchar(512) null comment '邮箱',
userStatus int default 0 not null comment '状态 0 - 正常',
createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
isDelete tinyint default 0 not null comment '是否删除'
)
comment '用户';
后端设计
规整目录层级
mybatisX——生成pojo、mapper 甚至service
按照表中字段自己写实体类的话 有些麻烦
可以用MybatisX帮助自动生成
(根据数据库生成domain实体对象 mapper操作数据库对象 mapper.xml 定义mapper对象与数据库的关联 可以在里面自己写sql service包含常用增删改查 serviceimpl具体实现服务)
将生成的代码重构进对应的目录即可
不建议直接生成在对应路径里 可能会覆盖原有代码导致出问题
test
在测试时 添加一个数据需要new一个对象 得对每个字段都写一个user.setxxx() 很繁琐 也有个插件可以生成对象的所有set方法
GenerateAllSetter
使用方法:在对象上按alt+enter 生成该对象的所有set方法 并提供默认值
package com.zwnsyw.bankend.service;
import java.util.Date;
import com.zwnsyw.bankend.pojo.User;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.Assert.*;
/**
* 用户服务测试
*
* @author Zwww
*/
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testAddUser(){
User user = new User();
user.setUsername("Zwww");
user.setUserAccount("202267021038");
user.setAvatarUrl("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSC28lvhB3X_P4cDQ17N2RQvttJRUYagluoPw&s");
user.setGender(0);
user.setUserPassword("zw200495");
user.setPhone("19807940898");
user.setEmail("zv041118@163.com");
boolean result = userService.save(user);
System.out.println(user.getId());
Assertions.assertTrue(result);
}
}
插曲:实体类属性与表字段的映射
报错java.sql.SQLSyntaxErrorException: (conn=39) Unknown column 'user_account' in 'field list'
SQL 语句中尝试插入 user_account 列,但数据库表 user 中并没有这个列
是因为MyBatis 默认情况下使用的是 驼峰命名法
Java 类字段 userAccount 会映射为数据库表的 user_account 字段
而数据库表中也是userAccount 所以导致无法找到
几个解决方法
1、在pojo层中 相关字段上加上类似@TableField("userAccount")的注解 表示映射为userAccount
2、直接在配置文件中 将默认映射关掉
spring:
application:
name: bankend
datasource:
url: jdbc:mariadb://localhost:3306/usermanage?useSSL=false&serverTimezone=UTC
username: root
password: zw200495
driver-class-name: org.mariadb.jdbc.Driver
server:
port: 8080
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
3、自己养成习惯 数据库里用下划线命名 实体类中用驼峰命名
注册逻辑
用户在前端输入账户密码 以及校验码(todo)
校验用户的账户、密码、校验密码 是否符合要求
账户不小于4位
密码不小于8位(正则表达式实现强校验 todo)
账户不能重复
账户不含特殊字符
密码和校验密码相同
对密码进行加密(数据库存密文)
向数据库插入用户数据
UserService
package com.zwnsyw.bankend.service;
import com.zwnsyw.bankend.pojo.User;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @author Zwww
* @description 针对表【user(用户)】的数据库操作Service
* @createDate 2024-11-12 00:08:31
*/
public interface UserService extends IService<User> {
/**
* 用户注册
* @param userAccount 用户账户
* @param userPassword 用户密码
* @param checkPassword 校验密码
* @return 新用户id
*/
Long UserRegister(String userAccount,String userPassword,String checkPassword);
}
alt enter 实现方法
发现在写校验逻辑的时候也是一直干重复性的事情 没意义
有个工具
添加依赖 commons-lang3
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
StringUtils.isAnyBlank方法即可校验字段是否为空是否长度大于1
if(StringUtils.isAnyBlank(userAccount,userPassword,checkPassword)){
return (long) -1;
}
过滤特殊字符的正则表达式
//账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if(matcher.find()){
return (long)-1;
}
另外 在校验的时候有个小细节 有的校验只需要判断传入的数据是否正确 而有些校验要涉及到查表的操作 比如判断账户是否重复 那么这类校验就应该放在最后面 能不占用资源去查询就别查询
关于加密
有个MD5的加密方法 DigestUtils.md5DigestAsHex
final String SALT = "Zwww";
String newPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
test
UserServiceImpl
package com.zwnsyw.bankend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import com.zwnsyw.bankend.pojo.User;
import com.zwnsyw.bankend.service.UserService;
import com.zwnsyw.bankend.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static net.sf.jsqlparser.util.validation.metadata.NamedObject.user;
/**
* 用户实现类方法
* @author Zwww
* @description 针对表【user(用户)】的数据库操作Service实现
* @createDate 2024-11-12 00:08:31
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public Long UserRegister(String userAccount, String userPassword, String checkPassword) {
//校验
if(StringUtils.isAnyBlank(userAccount,userPassword,checkPassword)){
return (long) -1;
}
if(userAccount.length()<4){
return (long)-1;
}
if(userPassword.length()<8 || checkPassword.length()<8){
return (long)-1;
}
//账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if(matcher.find()){
return (long)-1;
}
if(!userPassword.equals(checkPassword)){
return (long)-1;
}
//账户不能重复 放校验的最后面 能不占用资源查询就不查询
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount",userAccount);
long count = userMapper.selectCount(queryWrapper);
if(count>0){
return (long)-1;
}
//加密
final String SALT = "Zwww";
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
boolean saveResult = this.save(user);
if (!saveResult) {
return (long)-1;
}
return user.getId();
}
}
UserServiceTest
package com.zwnsyw.bankend.service;
import java.util.Date;
import com.zwnsyw.bankend.pojo.User;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.Assert.*;
/**
* 用户服务测试
*
* @author Zwww
*/
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testAddUser(){
User user = new User();
user.setUsername("Zwww");
user.setUserAccount("202267021038");
user.setAvatarUrl("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSC28lvhB3X_P4cDQ17N2RQvttJRUYagluoPw&s");
user.setGender(0);
user.setUserPassword("zw200495");
user.setPhone("19807940898");
user.setEmail("zv041118@163.com");
boolean result = userService.save(user);
System.out.println(user.getId());
Assertions.assertTrue(result);
}
@Test
public void userRegister() {
String userAccount = "test";
String userPassword = "";
String checkPassword = "123456";
long result = userService.UserRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);
userAccount = "zw";
result = userService.UserRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);
userAccount = "test";
userPassword = "123456";
result = userService.UserRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);
userAccount = "Zw ww";
userPassword = "12345678";
result = userService.UserRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);
userAccount = "test";
checkPassword = "123456789";
result = userService.UserRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);
userAccount="202267021038";
checkPassword = "12345678";
result = userService.UserRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);
userAccount = "test2";
result = userService.UserRegister(userAccount, userPassword, checkPassword);
Assertions.assertTrue(result>0);
}
}
登录逻辑
接受参数: 用户账户、密码
请求类型: post (涉及密码不能get明文传 另外一个原因 请求参数很长的话也不建议用get)
请求体: JSON格式
返回值:用户信息(脱敏)
逻辑:
校验输出是否合法
账户非空且不小于4位
密码不小于8位
账户不能重复
账户不含特殊字符
校验密码是否正确 先加密再对比
记录用户登录态(session) 存在服务器上(用后端springboot封装的服务器tomcat记录)
返回用户信息(脱敏 隐藏敏感信息)
如何知道哪个用户登录了?
client连上服务器后 生成一个session状态(匿名会话) 返回给前端
登录成功后 得到了登录成功的session 并给该session设置一些值(比如用户信息) 返回给前端一个设置cookie的命令
前端接受到该命令 设置cookie保存在浏览器内
前端再次请求后端的时候 (相同的域名 在请求头中带上cookie去请求)
后端拿到前端传来的cookie 找到对应的session
后端从session中可以取出基于该session存储的变量(用户的登录信息 登录名 token等等)
package com.zwnsyw.bankend.service.impl;
import java.util.Date;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import com.zwnsyw.bankend.pojo.User;
import com.zwnsyw.bankend.service.UserService;
import com.zwnsyw.bankend.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 用户实现类方法
*
* @author Zwww
* @description 针对表【user(用户)】的数据库操作Service实现
* @createDate 2024-11-12 00:08:31
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 盐值
*/
private final String SALT = "Zwww";
/**
* 用户登录态键
*/
private static final String USER_LOGIN_STATE = "userLoginState";
@Override
public Long UserRegister(String userAccount, String userPassword, String checkPassword) {
//校验
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return (long) -1;
}
if (userAccount.length() < 4) {
return (long) -1;
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
return (long) -1;
}
//账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return (long) -1;
}
if (!userPassword.equals(checkPassword)) {
return (long) -1;
}
//账户不能重复 放校验的最后面 能不占用资源查询就不查询
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
return (long) -1;
}
//加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
boolean saveResult = this.save(user);
if (!saveResult) {
return (long) -1;
}
return user.getId();
}
@Override
public User doLogin(String userAccount, String userPassword, HttpServletRequest request) {
//校验
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
// todo 修改为自定义异常
return null;
}
if (userAccount.length() < 4) {
return null;
}
if (userPassword.length() < 8) {
return null;
}
//账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return null;
}
//加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//查询是否存在
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
queryWrapper.eq("userPassword", encryptPassword);
User user = userMapper.selectOne(queryWrapper);
//用户不存在
if (user == null) {
log.info("user login failed, userAccount cannot match userPassword");
return null;
}
//用户脱敏
User safetyUser = new User();
safetyUser.setId(user.getId());
safetyUser.setUsername(user.getUsername());
safetyUser.setUserAccount(user.getUserAccount());
safetyUser.setAvatarUrl(user.getAvatarUrl());
safetyUser.setGender(user.getGender());
// safetyUser.setUserPassword(""); 密码不给返回
safetyUser.setPhone(user.getPhone());
safetyUser.setEmail(user.getEmail());
safetyUser.setUserStatus(user.getUserStatus());
safetyUser.setCreateTime(user.getCreateTime());
safetyUser.setUpdateTime(user.getUpdateTime());
// safetyUser.setIsDelete(0); 与业务无关 不返回
//记录用户登录态
request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);
return safetyUser;
}
}
用户脱敏
在返回给用户的信息中 不应该包含敏感信息 不能直接把数据库中查出的数据直接返回给客户端 所以还要再加一层封装
逻辑删除
实际上我们在设计表时 是有逻辑删除的存在的 但在刚才的实现中 并没有完成这个功能 仅是查询看是否存在 这样会导致逻辑删除的用户也被查出来
在配置文件中添加对mybatis-plus的全局配置
spring:
application:
name: bankend
datasource:
url: jdbc:mariadb://localhost:3306/usermanage?useSSL=false&serverTimezone=UTC
username: root
password: zw200495
driver-class-name: org.mariadb.jdbc.Driver
server:
port: 8080
mybatis-plus:
configuration:
# 关闭驼峰与下划线映射
map-underscore-to-camel-case: false
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
这样一来 在执行
//查询是否存在
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
queryWrapper.eq("userPassword", encryptPassword);
User user = userMapper.selectOne(queryWrapper);
时 就可以过滤掉 被标记为逻辑删除的数据了
中端交互
后端的实现写完了 现在要封装起来与前端进行交互
也就是写controller层
@RestController 适用于编写restful风格的api 返回值默认为json类型
非常戏剧性的一幕 “写这些参数好麻烦啊 给你们推荐一个插件吧”
得,一行代码不用写呗
Auto Filling Java Call Arguments
注册接口
给请求封装一个请求体
package com.zwnsyw.bankend.pojo.request;
import lombok.Data;
import java.io.Serializable;
/**
* 用户注册请求体
*
* @author Zwww
*/
@Data
public class UserRegisterRequest implements Serializable {
private static final long serialVersionUID = 1L;
private String userAccount;
private String userPassword;
private String checkPassword;
}
UserController
package com.zwnsyw.bankend.controller;
import com.zwnsyw.bankend.pojo.request.UserRegisterRequest;
import com.zwnsyw.bankend.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户接口
*
* @author Zwww
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Long userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if(userRegisterRequest == null){
return null;
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if(StringUtils.isAnyBlank(userAccount,userPassword,checkPassword)){
return null;
}
return userService.UserRegister(userAccount, userPassword, checkPassword);
}
}
这里进行了一些校验 其实也可以 它是对于传入数据本身的一个没有感情的校验 并不涉及到什么业务逻辑 真正的逻辑校验在service 这里加一层其实对性能没什么影响 加点保险
登录接口
同理 封装一个请求体
package com.zwnsyw.bankend.pojo.request;
import java.io.Serializable;
/**
* 用户登录请求体
*
* @author Zwww
*/
@Data
public class UserLoginRequest implements Serializable {
private static final long serialVersionUID = 1L;
private String userAccount;
private String userpassword;
}
UserController
package com.zwnsyw.bankend.controller;
import com.zwnsyw.bankend.pojo.User;
import com.zwnsyw.bankend.pojo.request.UserLoginRequest;
import com.zwnsyw.bankend.pojo.request.UserRegisterRequest;
import com.zwnsyw.bankend.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* 用户接口
*
* @author Zwww
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Long userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if(userRegisterRequest == null){
return null;
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if(StringUtils.isAnyBlank(userAccount,userPassword,checkPassword)){
return null;
}
return userService.UserRegister(userAccount, userPassword, checkPassword);
}
@PostMapping("/login")
public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if(userLoginRequest == null){
return null;
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserpassword();
if(StringUtils.isAnyBlank(userAccount,userPassword)){
return null;
}
return userService.UserLogin(userAccount, userPassword,request);
}
}
test
idea自带http调试工具
POST http://localhost:8080/user/login
Content-Type: application/json
{
"userAccount": "test",
"userpassword": 12345678
}
返回值会保存在一个json文件中
或者直接从controll点击左边工具进入调试工具
用户管理接口
简单功能直接在controller里实现了 后续优化的时候要把具体实现放到serviceImpl里面
查询用户
允许根据用户名查询
@GetMapping("/search")
public List<User> searchUser(@RequestParam String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if(StringUtils.isNotBlank(username)){
queryWrapper.like("username", username);
}
return userService.list(queryWrapper);
}
删除用户
@PostMapping("/delete")
public boolean deleteUser(@RequestParam long id) {
if(id<=0){
return false;
}
return userService.removeById(id);
}
因为在前面配置里设了 这里正常用removeById就行了 执行的就是逻辑删除(实际做的事不是删除而是更新)
这个地方其实是有很多问题的 结构先不说 逻辑上
现在的查询和删除接口是没有把控的 谁都可以调用 显然不安全
需要加上一层鉴权
鉴权设计
给user表添加一个字段 role 0表示普通用户 1表示管理员
对应修改pojo
以及封装的脱敏用户类
把一些公用的东西提出来 放在bankend/contant/UserConstant.java里
package com.zwnsyw.bankend.contant;
/**
* 用户常量
*
* @author Zwww
*/
public interface UserConstant {
/**
* 用户登录态键
*/
String USER_LOGIN_STATE = "userLoginState";
//--------------权限-------------
/**
* 默认权限
*/
int DEFAULT_USER = 0;
/**
* 管理员权限
*/
int ADMIN_USER = 1;
}
在进行查询和删除前 先进行一次鉴权
获取用户登录态 检查其user.getRole是0还是1
package com.zwnsyw.bankend.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zwnsyw.bankend.pojo.User;
import com.zwnsyw.bankend.pojo.request.UserLoginRequest;
import com.zwnsyw.bankend.pojo.request.UserRegisterRequest;
import com.zwnsyw.bankend.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static com.zwnsyw.bankend.contant.UserConstant.ADMIN_USER;
import static com.zwnsyw.bankend.contant.UserConstant.USER_LOGIN_STATE;
/**
* 用户接口
*
* @author Zwww
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Long userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRegisterRequest == null) {
return null;
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return null;
}
return userService.UserRegister(userAccount, userPassword, checkPassword);
}
@PostMapping("/login")
public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
return null;
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserpassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return null;
}
return userService.UserLogin(userAccount, userPassword, request);
}
@GetMapping("/search")
public List<User> searchUser(@RequestParam String username, HttpServletRequest request) {
//鉴权 仅管理员可查询
if(!isAdmin(request)) {
return new ArrayList<>();
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like("username", username);
}
return userService.list(queryWrapper);
}
@PostMapping("/delete")
public boolean deleteUser(@RequestParam long id, HttpServletRequest request) {
//鉴权 仅管理员可删除
if(!isAdmin(request)) {
return false;
}
if (id <= 0) {
return false;
}
return userService.removeById(id);
}
/**
* 是否为管理员
*
* @param request
* @return
*/
private boolean isAdmin(HttpServletRequest request) {
//鉴权
Object userobj = request.getSession().getAttribute(USER_LOGIN_STATE);
User user = (User) userobj;
return user != null && user.getRole() == ADMIN_USER;
}
}
设计session过期
可以给会话添加一个有效时间 直接在配置里面改
86400表示一天
test
将test用户的role改为1 授予管理员权限
先执行login刷新session 再以当前管理员用户执行search
debug 封装脱敏
发现存在问题 一些敏感信息也被查到了 所以这个地方也得脱敏
把脱敏逻辑封装出来
UserService
package com.zwnsyw.bankend.service;
import com.zwnsyw.bankend.pojo.User;
import com.baomidou.mybatisplus.extension.service.IService;
import javax.servlet.http.HttpServletRequest;
/**
* @author Zwww
* @description 针对表【user(用户)】的数据库操作Service
* @createDate 2024-11-12 00:08:31
*/
public interface UserService extends IService<User> {
/**
* 用户注册
*
* @param userAccount 用户账户
* @param userPassword 用户密码
* @param checkPassword 校验密码
* @return 新用户id
*/
Long UserRegister(String userAccount, String userPassword, String checkPassword);
/**
* 用户登录
*
* @param userAccount 用户账户
* @param userPassword 用户密码
* @param request 请求
* @return 脱敏后的用户信息
*/
User UserLogin(String userAccount, String userPassword, HttpServletRequest request);
/**
* 用户脱敏
*
* @param originUser 原user数据
* @return
*/
User getSafetyUser(User originUser);
}
UserServiceImpl
package com.zwnsyw.bankend.service.impl;
import java.util.Date;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import com.zwnsyw.bankend.pojo.User;
import com.zwnsyw.bankend.service.UserService;
import com.zwnsyw.bankend.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.zwnsyw.bankend.contant.UserConstant.USER_LOGIN_STATE;
/**
* 用户实现类方法
*
* @author Zwww
* @description 针对表【user(用户)】的数据库操作Service实现
* @createDate 2024-11-12 00:08:31
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 盐值
*/
private final String SALT = "Zwww";
@Override
public Long UserRegister(String userAccount, String userPassword, String checkPassword) {
//校验
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return (long) -1;
}
if (userAccount.length() < 4) {
return (long) -1;
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
return (long) -1;
}
//账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return (long) -1;
}
if (!userPassword.equals(checkPassword)) {
return (long) -1;
}
//账户不能重复 放校验的最后面 能不占用资源查询就不查询
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
return (long) -1;
}
//加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
boolean saveResult = this.save(user);
if (!saveResult) {
return (long) -1;
}
return user.getId();
}
@Override
public User UserLogin(String userAccount, String userPassword, HttpServletRequest request) {
//校验
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
// todo 修改为自定义异常
return null;
}
if (userAccount.length() < 4) {
return null;
}
if (userPassword.length() < 8) {
return null;
}
//账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return null;
}
//加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//查询是否存在
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
queryWrapper.eq("userPassword", encryptPassword);
User user = userMapper.selectOne(queryWrapper);
//用户不存在
if (user == null) {
log.info("user login failed, userAccount cannot match userPassword");
return null;
}
//用户脱敏
User safetyUser = getSafetyUser(user);
//记录用户登录态
request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);
return safetyUser;
}
/**
* 用户脱敏
*
* @param originUser
* @return
*/
@Override
public User getSafetyUser(User originUser){
//用户脱敏
User safetyUser = new User();
safetyUser.setId(originUser.getId());
safetyUser.setUsername(originUser.getUsername());
safetyUser.setUserAccount(originUser.getUserAccount());
safetyUser.setAvatarUrl(originUser.getAvatarUrl());
safetyUser.setGender(originUser.getGender());
// safetyUser.setUserPassword(""); 密码不给返回
safetyUser.setPhone(originUser.getPhone());
safetyUser.setEmail(originUser.getEmail());
safetyUser.setUserStatus(originUser.getUserStatus());
safetyUser.setCreateTime(originUser.getCreateTime());
safetyUser.setUpdateTime(originUser.getUpdateTime());
safetyUser.setUserRole(originUser.getUserRole());
// safetyUser.setIsDelete(0); 与业务无关 不返回
return safetyUser;
}
}
UserController
package com.zwnsyw.bankend.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zwnsyw.bankend.pojo.User;
import com.zwnsyw.bankend.pojo.request.UserLoginRequest;
import com.zwnsyw.bankend.pojo.request.UserRegisterRequest;
import com.zwnsyw.bankend.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.zwnsyw.bankend.contant.UserConstant.ADMIN_USER;
import static com.zwnsyw.bankend.contant.UserConstant.USER_LOGIN_STATE;
/**
* 用户接口
*
* @author Zwww
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Long userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRegisterRequest == null) {
return null;
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return null;
}
return userService.UserRegister(userAccount, userPassword, checkPassword);
}
@PostMapping("/login")
public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
return null;
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserpassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return null;
}
return userService.UserLogin(userAccount, userPassword, request);
}
@GetMapping("/search")
public List<User> searchUser(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(user -> userService.getSafetyUser(user)).collect(Collectors.toList());
}
@PostMapping("/delete")
public boolean deleteUser(@RequestParam long id, HttpServletRequest request) {
//鉴权 仅管理员可删除
if (!isAdmin(request)) {
return false;
}
if (id <= 0) {
return false;
}
return userService.removeById(id);
}
/**
* 是否为管理员
*
* @param request
* @return
*/
private boolean isAdmin(HttpServletRequest request) {
//鉴权
Object userobj = request.getSession().getAttribute(USER_LOGIN_STATE);
User user = (User) userobj;
return user != null && user.getUserRole() == ADMIN_USER;
}
}
这样一来查到的就是脱敏后的信息了
前端设计
前端接口
proxy.ts中:
export default {
dev: {
'/api/': {
target: 'http://localhost:8080/',
changeOrigin: true,
},
},
api.ts中
修改接口地址
后端在配置中添加 默认所有接口加上api前缀
并在MariaDB 连接 URL 中添加allowPublicKeyRetrieval=true:允许客户端从服务器检索公钥,用于加密和认证,适合用于本地开发和测试环境。
spring:
application:
name: bankend
datasource:
url: jdbc:mariadb://localhost:3306/usermanage?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: zw200495
driver-class-name: org.mariadb.jdbc.Driver
# session 失效时间
session:
timeout: 86400
server:
port: 8080
servlet:
context-path: /api
mybatis-plus:
configuration:
# 关闭驼峰与下划线映射
map-underscore-to-camel-case: false
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
import { Footer } from '@/components';
import { login } from '@/services/ant-design-pro/api';
import {
LockOutlined,
UserOutlined,
} from '@ant-design/icons';
import {
LoginForm,
ProFormCheckbox,
ProFormText,
} from '@ant-design/pro-components';
import { Helmet, history, useModel } from '@umijs/max';
import { Alert, message, Tabs } from 'antd';
import { createStyles } from 'antd-style';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
import Settings from '../../../../config/defaultSettings';
const useStyles = createStyles(({ token }) => {
return {
action: {
marginLeft: '8px',
color: 'rgba(0, 0, 0, 0.2)',
fontSize: '24px',
verticalAlign: 'middle',
cursor: 'pointer',
transition: 'color 0.3s',
'&:hover': {
color: token.colorPrimaryActive,
},
},
lang: {
width: 42,
height: 42,
lineHeight: '42px',
position: 'fixed',
right: 16,
borderRadius: token.borderRadius,
':hover': {
backgroundColor: token.colorBgTextHover,
},
},
container: {
display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
backgroundImage:
"url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
backgroundSize: '100% 100%',
},
};
});
const LoginMessage: React.FC<{
content: string;
}> = ({ content }) => {
return (
<Alert
style={{
marginBottom: 24,
}}
message={content}
type="error"
showIcon
/>
);
};
const Login: React.FC = () => {
const [userLoginState] = useState<API.LoginResult>({});
const [type, setType] = useState<string>('account');
const { initialState, setInitialState } = useModel('@@initialState');
const { styles } = useStyles();
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
flushSync(() => {
setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
});
}
};
const handleSubmit = async (values: API.LoginParams) => {
try {
// 登录
const user = await login({
...values,
type,
});
if (user) {
const defaultLoginSuccessMessage = '登录成功!';
message.success(defaultLoginSuccessMessage);
await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
}
// console.log(msg);
// // 如果失败去设置用户错误信息
// setUserLoginState(msg);
} catch (error) {
const defaultLoginFailureMessage = '登录失败,请重试!';
console.log(error);
message.error(defaultLoginFailureMessage);
}
};
const { status, type: loginType } = userLoginState;
return (
<div className={styles.container}>
<Helmet>
<title>
{'登录'}- {Settings.title}
</title>
</Helmet>
<div
style={{
flex: '1',
padding: '32px 0',
}}
>
<LoginForm
contentStyle={{
minWidth: 280,
maxWidth: '75vw',
}}
logo={<img alt="logo" src="api/images/MeBMWob6cJql/logo.svg" />}
title="Zwww Design"
subTitle={`日拱一卒 功不唐捐`}
initialValues={{
autoLogin: true,
}} (values) => {
await handleSubmit(values as API.LoginParams);
}}
>
<Tabs
activeKey={type}
centered
items={[
{
key: 'account',
label: '账户密码登录',
},
]}
/>
{status === 'error' && loginType === 'account' && (
<LoginMessage content={'错误的用户名和密码'} />
)}
{type === 'account' && (
<>
<ProFormText
name="userAccount"
fieldProps={{
size: 'large',
prefix: <UserOutlined />,
}}
placeholder={'请输入用户名'}
rules={[
{
required: true,
message: '用户名是必填项!',
},
]}
/>
<ProFormText.Password
name="userPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined />,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '密码是必填项!',
},
]}
/>
</>
)}
<div
style={{
marginBottom: 24,
}}
>
<ProFormCheckbox noStyle name="autoLogin">
自动登录
</ProFormCheckbox>
<a
style={{
float: 'right',
}}
>
忘记密码 ?
</a>
</div>
</LoginForm>
</div>
<Footer />
</div>
);
};
export default Login;
注册页面
import {LockOutlined, UserOutlined,} from '@ant-design/icons';
import {message, Tabs} from 'antd';
import React, {useState} from 'react';
import {history} from 'umi';
import {PLANET_LINK} from '@/constants';
import Footer from '@/components/Footer';
import {register} from '@/services/ant-design-pro/api';
import styles from './index.less';
import {LoginForm, ProFormText} from '@ant-design/pro-form';
const Register: React.FC = () => {
const [type, setType] = useState<string>('account');
// 表单提交
const handleSubmit = async (values: API.RegisterParams) => {
const {userPassword, checkPassword} = values;
// 校验
if (userPassword !== checkPassword) {
message.error('两次输入的密码不一致');
return;
}
try {
// 注册
const id = await register(values);
if (id) {
const defaultLoginSuccessMessage = '注册成功!';
message.success(defaultLoginSuccessMessage);
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const {query} = history.location;
history.push({
pathname: '/user/login',
query,
});
return;
}
} catch (error: any) {
const defaultLoginFailureMessage = '注册失败,请重试!';
message.error(defaultLoginFailureMessage);
}
};
return (
<div className={styles.container}>
<div className={styles.content}>
<LoginForm
submitter={{
searchConfig: {
submitText: '注册'
}
}}
logo={<img alt="logo" src="api/images/MeBMWob6cJql/logo.svg" />}
title="Zwww Design"
subTitle={<a href={PLANET_LINK} target="_blank" rel="noreferrer">日拱一卒 功不唐捐</a>}
initialValues={{
autoLogin: true,
}} (values) => {
await handleSubmit(values as API.RegisterParams);
}}
>
<Tabs activeKey={type}>
<Tabs.TabPane key="account" tab={'账号密码注册'}/>
</Tabs>
{type === 'account' && (
<>
<ProFormText
name="userAccount"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon}/>,
}}
placeholder="请输入账号"
rules={[
{
required: true,
message: '账号是必填项!',
},
]}
/>
<ProFormText.Password
name="userPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon}/>,
}}
placeholder="请输入密码"
rules={[
{
required: true,
message: '密码是必填项!',
},
{
min: 8,
type: 'string',
message: '长度不能小于 8',
},
]}
/>
<ProFormText.Password
name="checkPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon}/>,
}}
placeholder="请再次输入密码"
rules={[
{
required: true,
message: '确认密码是必填项!',
},
{
min: 8,
type: 'string',
message: '长度不能小于 8',
},
]}
/>
</>
)}
</LoginForm>
</div>
<Footer/>
</div>
);
};
export default Register;
项目分区导航:⬅️ 13-javadoc注释 | 14-业务逻辑 | ➡️ 15-逻辑删除mybatis-plus配置原理
💬 评论