业务逻辑

表的设计

开辟原则 尽量减少修改 只做拓展 初始化项目的时候没必要设计的太复杂 过度设计之后发现用不着其实更麻烦

设计表要思考什么

    有哪些表(模型)?

    有哪些字段?

    字段有哪些类型

 (数据库字段添加索引? 表与表之间的关联?)

对于用户表来说

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 '用户';

后端设计

规整目录层级

image-48b81d72

mybatisX——生成pojo、mapper 甚至service

按照表中字段自己写实体类的话 有些麻烦

可以用MybatisX帮助自动生成

(根据数据库生成domain实体对象 mapper操作数据库对象 mapper.xml 定义mapper对象与数据库的关联 可以在里面自己写sql service包含常用增删改查 serviceimpl具体实现服务)

image-42054876 image-e4122d1b image-2dc5b11d image-06784654

将生成的代码重构进对应的目录即可

不建议直接生成在对应路径里 可能会覆盖原有代码导致出问题

image-3c1f1871

test

image-e0fb3382 image-ea61a64b image-96e5075a

在测试时 添加一个数据需要new一个对象 得对每个字段都写一个user.setxxx() 很繁琐 也有个插件可以生成对象的所有set方法

GenerateAllSetter

image-378afbab

使用方法:在对象上按alt+enter 生成该对象的所有set方法 并提供默认值

image-607df373 image-3937831b
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 实现方法

image-c62a2a48

发现在写校验逻辑的时候也是一直干重复性的事情 没意义

有个工具

添加依赖 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);

时 就可以过滤掉 被标记为逻辑删除的数据了

逻辑删除mybatis-plus配置原理

中端交互

后端的实现写完了 现在要封装起来与前端进行交互

也就是写controller层

@RestController 适用于编写restful风格的api 返回值默认为json类型

image-44da916f

非常戏剧性的一幕 “写这些参数好麻烦啊 给你们推荐一个插件吧”

得,一行代码不用写呗

Auto Filling Java Call Arguments

image-cee0db1b image-9eaa25f3

注册接口

给请求封装一个请求体

image-80d1946d
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调试工具

image-edfb6d74 image-455dafa3
POST http://localhost:8080/user/login
Content-Type: application/json

{
  "userAccount": "test",
  "userpassword": 12345678
}
image-0b0a63d2

返回值会保存在一个json文件中

image-69dc987f image-9a42e9cb

或者直接从controll点击左边工具进入调试工具

image-0a0ec73c

用户管理接口

简单功能直接在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表示管理员

image-b2197fbb

对应修改pojo

image-6292ea0e

以及封装的脱敏用户类

image-54ad9aaf

把一些公用的东西提出来 放在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表示一天

image-a5588134

test

将test用户的role改为1 授予管理员权限

先执行login刷新session 再以当前管理员用户执行search

image-073b64a2 image-8b73f289 image-7ca86256 image-9c85452c

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;
    }

}

这样一来查到的就是脱敏后的信息了

image-e3db81f9

前端设计

前端接口

proxy.ts中:

export default {

  dev: {
    '/api/': {

      target: 'http://localhost:8080/',

      changeOrigin: true,
    },
  },
image-97141146

api.ts中

修改接口地址

image-3b99132c

后端在配置中添加 默认所有接口加上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)
image-be799598 image-3e720e9e
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;
image-9f196c09

项目分区导航:⬅️ 13-javadoc注释 | 14-业务逻辑 | ➡️ 15-逻辑删除mybatis-plus配置原理