Featured image of post Java工程师 用户中心项目 Java后端实战

Java工程师 用户中心项目 Java后端实战

🌏Java工程师 用户中心 🎯 这篇文章用于记录 用户中心项目实战过程 这个项目专注于实现企业核心的用户中心系统 是基于SpringBoot后端 + React前端的全栈项目 实现了用户注册、登录、查询等基础功能 这篇文章是第1部分开发记录 包括 [1] 前后端项目初始化 [2] 后端开发记录

🎄概述

这个项目专注于实现企业核心的用户中心系统 是基于SpringBoot后端 + React前端的全栈项目 实现了用户注册、登录、查询等基础功能 这篇文章是第1部分开发记录 包括 [1] 前后端项目初始化 [2] 后端开发记录

🎄初始化项目前端部分

❌尝试失败记录

🍭安装Node.js

🍭安装yarn包管理器

参考官网安装过程

Yarn是facebook发布的一款取代npm的包管理工具 Yarn和NPM都是JavaScript的包管理工具,用于在项目中安装、更新和管理依赖项。 但Yarn在性能和速度方面相对于NPM来说更快。Yarn使用了并行下载和缓存机制,可以更有效地处理依赖项的安装和更新,从而加快了项目的构建过程

🎯启用corepack -> operation not permitted 权限不足

win + x -> 管理员打开黑窗口执行 执行成功

🎯Updating the global Yarn version

当Node.js版本 ^16.17 or >=18.6 执行更新操作

🍭初始化项目

回到项目目录下

执行命令 yarn create umi myapp

根据提示执行 yarn install 执行后开始安装依赖包

✅使用npm重新初始化

参考Ant Design Pro的官网 https://pro.ant.design/zh-CN/docs/getting-started

npm i @ant-design/pro-cli -g

pro create myapp

解决办法 https://zhuanlan.zhihu.com/p/493496089

我们通过管理员权限运行power shell,然后输入命令 set-ExecutionPolicy RemoteSigned

重新执行pro create myapp

查看文件夹 项目myapp已经被创建

🎯使用Idea打开并启动

安装依赖

执行start 启动项目

解决方法: https://blog.csdn.net/m0_48300767/article/details/131450325

修改start命令 注意要把原来的cross-env UMI_ENV=dev umi dev放在后面

"start": "set NODE_OPTIONS=--openssl-legacy-provider && cross-env UMI_ENV=dev umi dev",

重新启动

使用admin模拟用户登录

🎄初始化项目后端部分

🎯创建项目·添加依赖·编写主启动类

🍭创建Maven项目

🍭添加需要的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>bbm.com</groupId>
	<artifactId>bigbigmeng-user-center-backend-20230923</artifactId>
	<version>1.0-SNAPSHOT</version>

	<!-- 20230923 引入SpringBoot父项目依赖 -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<!-- 20230923 依赖属性 -->
	<properties>
		<maven.compiler.source>8</maven.compiler.source>
		<maven.compiler.target>8</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<!-- 20230923 web开发需要的依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- 20230923 MyBatis依赖 -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.2.2</version>
		</dependency>
		<!-- 20230923 MyBatis-plus -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.5.1</version>
		</dependency>
		<!-- 20230923 工具类依赖 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.12.0</version>
		</dependency>
		<!-- 20230923 工具类依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<!-- 20230923 JDBC -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<!-- 20230923 SpringBoot配置处理 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<!-- 20230923 SpringBoot配置处理 -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<!-- 不给下层的子项目 -->
			<optional>true</optional>
		</dependency>
		<!-- 20230923 SpringBootTest -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<!-- 20230923 只在测试目录下起作用 -->
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<finalName>usercenter</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

🍭确定项目目录 编写主启动类UserCenterApplication并启动 报错没有设置数据源

🎯创建数据库并配置数据源·重新启动

🍭Idea连接MySQL

🍭线连接本地任意数据库

🍭创建数据库bbm_user_center

CREATE DATABASE IF NOT EXISTS bbm_user_center DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

🍭切换到数据库bbm_user_center

🍭创建表user

把所有字段设置为可null 在业务代码上进行约束即可

create table user (
    userId          bigint auto_increment   comment '用户唯一id' primary key,
    userAccount     varchar(256)    null    comment '用户账号',
    userNickname    varchar(256)    null    comment '用户昵称',
    userPwd         varchar(512)    null    comment '用户密码',
    userAvatar      varchar(1024)   null    comment '用户头像链接',
    userGender      tinyint         null    comment '用户性别',
    userPhone       varchar(128)    null    comment '用户手机号',
    userEmail       varchar(512)    null    comment '用户邮箱',
    userStatus      int default 0   null  comment '用户业务状态 | 0 可正常使用 |',
    userCreateTime  datetime default CURRENT_TIMESTAMP null comment '用户创建时间',
    userUpdateTime  datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP
        comment '这是一种数据库中表字段的设置它为"userUpdateTime"字段设置了两个默认值 首先设置了一个默认值为当前时间戳(CURRENT_TIMESTAMP)表示如果在插入数据时未指定"userUpdateTime"的值 则会自动使用当前的时间戳作为默认值其次设置了一个"On Update"规则即当更新该表中的任何时"userUpdateTime"字段将自动更新为当前时间戳 这样设置的好处是不需要在插入或更新记录时手动设置"userUpdateTime"字段的值而是通过数据库自动维护该字段的值保证了数据的准确性和一致性 该设置可用于跟踪记录的最后更新时间方便后续查询和分析数据的变更情况',
    userDeleted     tinyint default 0  null comment '用户是否被删除 1表示删除 0表示正常',
    userRole        int     default 0  null comment '0普通用户 1管理员',
    userCode        varchar(512)       null comment '用户编号 备用字段'
) comment '用户表';

🍭配置数据源application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bbm_user_center
    username: root
    password: root
server:
  port: 8080
  servlet:
    context-path: /

🍭启动项目测试

🎄MyBatis-X生成后端代码·初测试

🍭右键选择

🍭指定包路径和配置 base package是生成的多个包的共同文件夹 最好不指定已有的稳定的文件夹 而是指定一个其他的包名 自动生成后 把相应需要的类拷贝到自己原先的目录下即可

🍭生成的包结构如下

🍭整理包接口 编写UserService测试类 执行测试

❌问题:看不到SQL语句

数据已经插入进来

🍭添加application.yml配置 使得可以看见后台的SQL语句

mybatis-plus:
  #mapper-locations: classpath:mappers/*.xml
  #如果配置内容比较多 可以考虑单独的做一个mybatis-config.xml
  #config-location: classpath:mybatis-config.xml
  type-aliases-package: bbm.com
  configuration:
    useGeneratedKeys: true # 插入数据的时候自动生成id
    mapUnderscoreToCamelCase: false # 不开启数据库下划线与java entity对象的驼峰命名映射
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

🍭添加mybatis-config.xml的方式进行配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--配置MyBatis自带的日志输出 查看原生的sql-->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <!-- 20230924 该包下面的所有类名 可以直接使用 -->
    <typeAliases>
        <package name="bbm.com"/>
    </typeAliases>
</configuration>

查看效果:可以看到SQL语句打印出来了

🎄修改包结构·添加必要的组件

修改包结构·添加实体、工具类

添加UserLogin登录实体类 添加UserRegister注册实体类

/*********************** 登录实体类 ***********************/
package bbm.com.entity;
/**
@author Liu Xianmeng
@createTime 2023/9/24 15:16
@instruction 用户登录实体 登录只使用到账号和密码
*/
public class UserLogin implements Serializable {
    private static final long serialVersionUID = 3191241716373120793L;
    /**
     * 用户账号
     */
    private String userAccount;
    /**
     * 用户密码
     */
    private String userPwd;
}

/*********************** 注册实体类 ***********************/
package bbm.com.entity;
/**
@author Liu Xianmeng
@createTime 2023/9/24 15:16
@instruction 用户注册实体类
*/
public class UserRegister implements Serializable {
    private static final long serialVersionUID = 3191241716373120793L;
    /**
     * 用户账号
     */
    private String userAccount;
    /**
     * 用户密码
     */
    private String userPwd;
    /**
     * 用户确认密码
     */
    private String userRePwd;
    /**
     * 用户编号
     */
    private String userCode;
}

添加常数工具类ConstantUtil

package bbm.com.utils;
/**
@author Liu Xianmeng
@createTime 2023/9/24 15:26
@instruction 应用需要用到的常量都放在这里
*/
public interface ConstantUtil {
    /***************************** 用户常量 ****************************/
    // 用户登录态键
    String USER_LOGIN_STATE = "userLoginState";
    // 默认普通用户权限
    int DEFAULT_ROLE = 0;
    // 管理员权限
    int ADMIN_ROLE = 1;
}

添加JSON工具类JSONUtil

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.80</version>
</dependency>
package bbm.com.utils;

import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
/**
@author Liu Xianmeng
@createTime 2023/9/24 15:35
@instruction 后端要返回给前端的数据 以JSON字符串的形式
             传给后端后 经过解析 就可以直接使用 完美✨
*/
public class JSONUtil {

    /**
     * 原函数 需要传入三个参数
     * @param code      状态码
     * @param message   简要信息
     * @param map       其他要携带的数据
     * @return          返回给前端的JSON字符串
     */
    public static String getJSONString(int code, String message, HashMap<String, Object> map) {
        JSONObject jo = new JSONObject();
        jo.put("node", code);
        if(message != null){
            jo.put("message", message);
        }
        if(map != null) {
            for(String key : map.keySet()) {
                jo.put(key, map.get(key));
            }
        }
        return jo.toJSONString();
    }

    /**
     * 重载函数
     * @param code      状态码
     * @param message   简要信息
     * @return          返回给前端的JSON字符串
     */
    public static String getJSONString(int code, String message) {
        return getJSONString(code, message, null);
    }

    /**
     * 重载函数
     * @param code      状态码
     * @param message   简要信息
     * @return          返回给前端的JSON字符串
     */
    public static String getJSONString(int code) {
        return getJSONString(code, null, null);
    }
}

添加消息返回实体类ResponseEntity

package bbm.com.entity;
/**
@author Liu Xianmeng
@createTime 2023/9/24 17:18
@instruction 返回消息实体类
*/
@Data
public class ResponseEntity {
    private int code;
    private String message;
    private HashMap<String, Object> map;
    public ResponseEntity() {
    }
    public ResponseEntity(int code, String message) {
        this.code = code;
        this.message = message;
    }
    public ResponseEntity(int code, String message, HashMap<String, Object> map) {
        this.code = code;
        this.message = message;
        this.map = map;
    }
}

添加字符串加密工具类MD5Util

package bbm.com.utils;

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.DigestUtils;
/**
@author Liu Xianmeng
@createTime 2023/9/24 15:51
@instruction 字符串加密工具类 用于对密码进行加密
*/
public class MD5Util {
    public static String getMD5Str(String str) {
        /**
         * 函数源代码中检查 只要出现一个非空字符 就说明原str不是空白字符串
         * 空字符的检测包含 "" 和 null 两种情况的判断
         */
        if(StringUtils.isBlank(str)) {
            return null;
        }
        // 对传入的str字符串进行加密后返回
        return DigestUtils.md5DigestAsHex(str.getBytes());
    }
}

添加获取随机字符串工具类UUIDUtil

package bbm.com.utils;
import java.util.UUID;
/**
@author Liu Xianmeng
@createTime 2023/9/24 15:49
@instruction 返回随机字符串
*/
public class UUIDUtil {
    public static String getUUIDStr() {
        // 把生成的字符串的间隔符 - 去掉
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
}

🎄进行后端注册登录功能开发

🍭编写注册方法

添加一个常量 用于对密码二次加密

/**
@author Liu Xianmeng
@createTime 2023/9/24 15:26
@instruction 应用需要用到的常量都放在这里
*/
public interface ConstantUtil {
    /***************************** 用户常量 ****************************/
    ...
    // 加盐加密字符串常量
    🎯String SALT = "BigBigMeng";
}

创建消息枚举类MessageEnum 用于管理JSON消息种类

package bbm.com.utils;

/**
@author Liu Xianmeng
@createTime 2023/9/24 16:23
@instruction
*/
public enum MessageEnum {
    
    NULL_BLANK_ERROR(401, "您的输入为空或不完整");
    
    private final int code;
    private final String message;
    MessageEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }
    public int getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }
}

编写Controller 添加注册方法

🎯如果希望Controller返回JSON数据(非字符串)则在方法上加上🎯@ResponseBody注解

另一种常用的返回模式是返回String 配合Thymeleaf + 前端JQuery使用

package bbm.com.controller;
/**
@author Liu Xianmeng
@createTime 2023/9/24 16:17
@instruction
*/
@Controller
public class UserController {

    @Autowired
    private UserServiceImpl usi;

    /**
     * 所有数据的校验都放在业务层
     * @param ur 传入的user注册信息
     * @return 返回JSON字符串信息到前端
     */
    @RequestMapping("/user/register")
    🎯@ResponseBody // 返回json
    public ResponseEntity register(UserRegister ur) {
        // 检查注册数据是否为空
        if(ur == null) throw new RuntimeException("用户登录数据为空");
        // 直接在service返回JSON字符串
        return usi.register(ur);
    }
}

效果如下图:

用户编号修改为和用户id一样的属性 并且让其等于id

ALTER TABLE user MODIFY userCode bigint DEFAULT NULL COMMENT ‘用户编号’ AFTER userId;

❗❗❗后面发现这个操作是没有用的 我在后面注册用户后手动将userCode更新为userId字段值

编写ServiceImpl添加注册方法

package bbm.com.service.impl;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    @Autowired
    private UserMapper um;

    /**
     * 用户注册
     * @param ur    用户注册信息
     * @return      返回ResponseEntity消息 new ResponseEntity
     */
    public ResponseEntity register(UserRegister ur) {
        logger.info("C UserServiceImpl M register()..");
        // 1 检查注册数据是否存在空
        if(StringUtils.isAnyBlank(ur.getUserAccount(), ur.getUserPwd(), ur.getUserRePwd())) {
            return new ResponseEntity(MessageEnum.NULL_BLANK_ERROR.getCode(),
                MessageEnum.NULL_BLANK_ERROR.getMessage());
        }
        // 2 检查数据输入是否规范
        if(ur.getUserPwd().length() < 8 || ur.getUserRePwd().length() < 8) {
            return new ResponseEntity(MessageEnum.PWD_LENGTH_ERROR.getCode(),
                MessageEnum.PWD_LENGTH_ERROR.getMessage());
        }
        // 3 账户不能包含特殊字符
        String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
        Matcher matcher = Pattern.compile(validPattern).matcher(ur.getUserAccount());
        if (matcher.find()) {
            return new ResponseEntity(MessageEnum.ACCOUNT_CHARACTERS_ERROR.getCode(),
                MessageEnum.ACCOUNT_CHARACTERS_ERROR.getMessage());
        }
        // 4 密码和校验密码相同
        if (!ur.getUserPwd().equals(ur.getUserRePwd())) {
            return new ResponseEntity(MessageEnum.PWD_REPWD_ERROR.getCode(),
                MessageEnum.PWD_REPWD_ERROR.getMessage());
        }
        // 5 验证账户是否已存在
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", ur.getUserAccount());
        if (um.selectCount(queryWrapper) > 0) {
            return new ResponseEntity(MessageEnum.ACCOUNT_DUPLICATION_ERROR.getCode(),
                MessageEnum.ACCOUNT_DUPLICATION_ERROR.getMessage());
        }
        // 6 对密码加密并注册该用户
        String encryptedPwd = MD5Util.getMD5Str(ur.getUserPwd());
        User newUser = new User();
        newUser.setUserAccount(ur.getUserAccount());
        newUser.setUserPwd(encryptedPwd);
        boolean rst = this.save(newUser);
        // 6.2 更新用户编号
        User user = this.getOne(queryWrapper);
        user.setUserCode(user.getUserId());
        boolean rstUpdate = this.updateById(user);
        // 6.3 验证注册结果并返回结果
        if(rst && rstUpdate) {
            logger.info("C UserServiceImpl M register().. 注册成功~");
            return new ResponseEntity(200, "注册成功~");
        }else {
            return new ResponseEntity(500, "服务端注册失败");
        }
    }
}

执行测试

可以使用Idea自带的测试工具:

🍭编写登录方法

⚡避免查询到已经逻辑删除的数据

有两种方式:

(1)第一种方式是手动在SQL上完成筛选WHETE userDeleted != 1 1表示已经被逻辑删除

(2)第二种方式是在yml文件中进行配置 并且给entity字段记上@TableLogic字段

在yml文件中进行配置🎯:

mybatis-plus:
  global-config:
    db-config:
      🎯logic-delete-field: userDeleted # 全局逻辑删除的实体字段名
      🎯logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      🎯logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

⚡编写登录方法

引入Redis的依赖

<!-- 20230924 Redis依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.6</version>
</dependency>

配置Redis

spring:
  redis:
    host: 111.42.34.90 # 远程地址
    port: 6379
    database: 0 #默认数据库
    timeout: 10000ms
    lettuce:
      pool:
        max-active: 8 #最大连接数:默认是 8
        max-wait: 10000ms #最大连接阻塞等待时间,默认是-1
        max-idle: 200 #最大空闲连接,默认是 8
        min-idle: 5 #最小空闲连接,默认是 0

注入后即可使用

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
    ...
    @Autowired
    private RedisTemplate redisTemplate;
    ...
}

写登录方法

编写Controller方法

package bbm.com.controller;
...
/**
@author Liu Xianmeng
@createTime 2023/9/24 16:17
@instruction (1) 用户注册和登录
*/
@Controller
public class UserController {
    @Autowired
    private UserServiceImpl usi;
    ...
    /**
     * 用户登录 流程如下
     *
     * 1 检查注册数据是否为空
     * 2 判断这个用户是否存在
     * 3 查询Redis是否存储有该用户的登陆信息
     * 5 如果(1)查询存在该用户的登陆信息 则直接返回该用户的信息给前端
     * 4 如果(1)查询不存在该用户的登陆信息 则将用户的登录信息放入Redis并返回用户信息给前端
     *   在将用户的信息放入Redis的过程中会生成一个随机字符串作为ticket传送给前端客户浏览器
     *
     * @param ur        必要的登录信息 账号和密码
     * @param response  response.addCookie(cookie) -> 将登录凭证ticket放到客户端cookie
     * @param ticket    浏览器上的Cookie是针对具体的服务器而言的 当用户访问一个网站时
     *                  服务器会发送带有Cookie的响应头给浏览器存储 浏览器会将这些Cookie保存在用户的计算机上
     *                  以便在用户再次访问相同的网站时 可以将Cookie通过请求头发送给服务器进行身份验证或其他操作
     * @CookieValue(required = false) String ticket 设置为false 以便第一次访问的时候能通过方法头验证
     * @return
     */
    @RequestMapping("/user/login")
    @ResponseBody
    public ResponseEntity login(UserLogin ul,
                                HttpServletResponse response,
                                @CookieValue(required = false) String ticket) {
        // 检查注册数据是否为空
        if(ul == null) {
            return new ResponseEntity(MessageEnum.NULL_BLANK_ERROR.getCode(),
                MessageEnum.NULL_BLANK_ERROR.getMessage());
        }
        return usi.login(ul, response, ticket);
    }
}

编写ServiceImpl方法

package bbm.com.service.impl;
/**
@author Liu Xianmeng
@createTime 2023/9/24 16:25
@instruction 
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    @Autowired
    private UserMapper um;
    @Autowired
    private RedisTemplate redisTemplate;
    ...
        
    /**
     * 用户登录 流程如下
     *
     * 1 判断Redis是否存在此用户的登录信息 如果存在则直接返回
     * 2 检查登录数据是否存在空
     * 3 检查密码长度是否过短
     * 4 账户不能包含特殊字符
     * 5 判断这个用户是否存在
     * 6 核验密码是否正确
     * 7 ticket == null 将用户的登录信息放入Redis并返回用户信息给前端
     * 8 返回最终的成功登录结果 并返回用户信息给前端
     *
     * @param ur        必要的登录信息 账号和密码
     * @param response  response.addCookie(cookie) -> 将登录凭证ticket放到客户端cookie
     * @param ticket    浏览器上的Cookie是针对具体的服务器而言的 当用户访问一个网站时
     *                  服务器会发送带有Cookie的响应头给浏览器存储 浏览器会将这些Cookie保存在用户的计算机上
     *                  以便在用户再次访问相同的网站时 可以将Cookie通过请求头发送给服务器进行身份验证或其他操作
     * @return
     */
    public ResponseEntity login(UserLogin ul, HttpServletResponse response, String ticket) {
        logger.info("C UserServiceImpl M login()..");

        // 1 判断Redis是否存在此用户的登录信息 如果存在则直接返回
        if(!StringUtils.isBlank(ticket) && !StringUtils.isBlank(ul.getUserAccount())) {
            Object o = redisTemplate.opsForValue().get(RedisKeyUtil.getTicketKey(ul.getUserAccount(), ticket));
            if(o != null) {
                HashMap<String, Object> map = new HashMap<>();
                map.put("userInfo", (User)o);
                return new ResponseEntity(200, "登录成功!", map);
            }
        }

        // 2 检查登录数据是否存在空
        if(StringUtils.isAnyBlank(ul.getUserAccount(), ul.getUserPwd())) {
            logger.error("C UserServiceImpl M login() 数据存在空");
            return new ResponseEntity(MessageEnum.NULL_BLANK_ERROR.getCode(),
                MessageEnum.NULL_BLANK_ERROR.getMessage());
        }
        // 3 检查密码长度是否过短
        if(ul.getUserAccount().length() < 8 || ul.getUserPwd().length() < 8) {
            logger.error("C UserServiceImpl M login() 数据输入不规范");
            return new ResponseEntity(MessageEnum.PWD_LENGTH_ERROR.getCode(),
                MessageEnum.PWD_LENGTH_ERROR.getMessage());
        }
        // 4 账户不能包含特殊字符
        String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
        Matcher matcher = Pattern.compile(validPattern).matcher(ul.getUserAccount());
        if (matcher.find()) {
            logger.error("C UserServiceImpl M login() 账户包含特殊字符");
            return new ResponseEntity(MessageEnum.ACCOUNT_CHARACTERS_ERROR.getCode(),
                MessageEnum.ACCOUNT_CHARACTERS_ERROR.getMessage());
        }

        // 5 判断这个用户是否存在
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", ul.getUserAccount());
        User user = this.getOne(queryWrapper);
        if(user == null) {
            logger.error("C UserServiceImpl M login() 用户不存在");
            return new ResponseEntity(MessageEnum.ACCOUNT_NOTEXSIT_ERROR.getCode(),
                MessageEnum.ACCOUNT_NOTEXSIT_ERROR.getMessage());
        }

        // 6 核验密码是否正确
        if(! user.getUserPwd().equals(MD5Util.getMD5Str(ConstantUtil.SALT + ul.getUserPwd()))){
            logger.error("C UserServiceImpl M login() 校验密码");
            return new ResponseEntity(MessageEnum.PWD_ERROR.getCode(),
                MessageEnum.PWD_ERROR.getMessage());
        }

        // 7 将用户的登录信息放入Redis并返回用户信息给前端
        String newTicket = UUIDUtil.getUUIDStr(); // 🎯随机字符串作为ticket
        user.setUserPwd(""); // 隐藏用户的敏感信息 密码
        Cookie cookie = new Cookie("ticket", newTicket);
        cookie.setPath("/"); // 设置cookie的有效路径
        cookie.setMaxAge(ConstantUtil.DEFAULT_EXPIRED_SECONDS); // 登录凭证的有效时间
        response.addCookie(cookie); // 将登录凭证放到客户端cookie
        // 将登录用户信息存入Redis
        redisTemplate.opsForValue().set(RedisKeyUtil.getTicketKey(ul.getUserAccount(), newTicket), user);

        // 8 返回最终的成功登录结果 并返回用户信息给前端
        HashMap<String, Object> map = new HashMap<>();
        map.put("userInfo", user);
        return new ResponseEntity(200, "登录成功!", map);
    }
}

⚡执行测试

查看Redis

🎄编写用户管理接口

🍭编写查询和删除用户接口

模糊查询所有的用户信息

/**
 * 根据用户昵称 模糊查询所有的用户信息
 * @param userNickName 用户昵称
 * @return
 */
@RequestMapping("/searchUsers")
@ResponseBody
public List<User> searchUsers(String userNickname) {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    if(StringUtils.isNotBlank(userNickname)) {
        queryWrapper.like("userNickname", userNickname);
    }
    return usi.list(queryWrapper);
}

逻辑删除用户信息

 /**
 * 逻辑删除用户
 * @param id
 * @return
 */
@RequestMapping("/user/delete")
public boolean deleteById(long id) {
    if(id <= 0) return false;
    // 自动支持逻辑删除
    return usi.removeById(id);
}

上面的写法有没有问题?存在安全问题 只允许管理员调用上面的接口(用户中心就是让管理员来使用的)修改上面的两个接口得到如下的结果

/**
 * 根据用户昵称 模糊查询所有的用户信息
 * @param userNickName 用户昵称
 * @param request 从request中获取用户的ticket 从而在Redis获取用户的完整信息
 *                再判断其userRole属性是不是管理员
 * @return
 */
@RequestMapping("/searchUsers")
@ResponseBody
public ResponseEntity searchUsers(String userNickname, String userAccount, HttpServletRequest request) {
    // 如果是普通用户 直返返回 权限不足
    if(!isAdmin(userAccount, request)) {
        return new ResponseEntity(MessageEnum.AUTHORITY_ERROR.getCode(), MessageEnum.AUTHORITY_ERROR.getMessage());
    }
    // 如果是管理员 继续往下走
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    if(StringUtils.isNotBlank(userNickname)) {
        queryWrapper.like("userNickname", userNickname);
    }
    ResponseEntity responseEntity = new ResponseEntity();
    // 使用流式计算 对用户的信息进行脱敏
    responseEntity.getMap().put("usersInfo", usi.list(queryWrapper).stream()
            .map(user -> usi.getSafetyUser(user)).collect(Collectors.toList()));
    return responseEntity;
}

/**
 * 逻辑删除用户
 * @param id
 * @return
 */
@RequestMapping("/user/delete")
@ResponseBody
public ResponseEntity deleteById(long id, String userAccount, HttpServletRequest request) {
    // 如果是普通用户 直返返回 权限不足
    if(!isAdmin(userAccount, request)) {
        return new ResponseEntity(MessageEnum.AUTHORITY_ERROR.getCode(), MessageEnum.AUTHORITY_ERROR.getMessage());
    }
    // 如果是管理员 继续往下走
    boolean rst = usi.removeById(id); // 自动支持逻辑删除
    if(rst) return new ResponseEntity(200, "删除成功");
    else    return new ResponseEntity(500, "删除失败");
}


public boolean isAdmin(String userAccount, HttpServletRequest request) {
    User curUser = null;
    Cookie[] cookies = request.getCookies();
    for(Cookie cookie : cookies) {
        if(cookie.getName().equals("ticket")) {
            curUser = (User) redisTemplate.opsForValue().get(RedisKeyUtil.getTicketKey(userAccount, cookie.getValue()));
            break;
        }
    }
    // 如果是普通用户 直返返回 权限不足
    if(curUser.getUserRole() == 0)  return false;
    else                            return true;
}

测试获取用户数据

测试删除用户数据

🍭编写查询所有用户的接口

引入PageHelper的依赖

<!-- 20230930 引入分页插件 PageHelper -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.2</version>
</dependency>

编写Controller方法

/**
 * 多条件查询所有用户信息
 * @param userQueryCondition        查询条件
 * @param pageRule                  分页要求
 * @return                          返回所有用户的信息(分页)
 */
@RequestMapping("/user/search")
@ResponseBody
public ResponseEntity search(@RequestBody(required = false) User userQueryCondition,
                             @RequestBody(required = false) PageRule pageRule) {
    logger.info("C UserController M search()..");
    if(pageRule == null) {
        // 默认显示第一页的前十条数据
        pageRule = new PageRule(1, 10);
    }
    // 构造查询条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    // 当查询条件不为空 暂定根据这6个条件来执行查询 或查询
    if(userQueryCondition != null) {
        queryWrapper.or(wrapper -> wrapper.like("userCode", userQueryCondition.getUserCode()));
        queryWrapper.or(wrapper -> wrapper.like("userCreateTime", userQueryCondition.getUserCreateTime()));
        queryWrapper.or(wrapper -> wrapper.like("userNickname", userQueryCondition.getUserNickname()));
        queryWrapper.or(wrapper -> wrapper.eq("userGender", userQueryCondition.getUserGender()));
        queryWrapper.or(wrapper -> wrapper.eq("userRole", userQueryCondition.getUserRole()));
        queryWrapper.or(wrapper -> wrapper.eq("userStatus", userQueryCondition.getUserStatus()));
    }
    // 从service返回最终的查询结果
    return usi.search(queryWrapper, pageRule);
}

编写Service方法

@Override
public ResponseEntity search(QueryWrapper<User> queryWrapper, PageRule pageRule) {
    ResponseEntity responseEntity = new ResponseEntity(200, "获取获取到所有用户信息");
    // 使用流式计算 对用户的信息进行🎯脱敏
    List<User> users =  list(queryWrapper).stream().map(user -> 🎯getSafetyUser(user)).collect(Collectors.toList());
    // 构建分页
    PageHelper.startPage(pageRule.getPageNum(), pageRule.getPageSize());
    responseEntity.getMap().put("usersPageInfo", new PageInfo<User>(users));
    return responseEntity;
}
Licensed under CC BY-NC-SA 4.0
最后更新于 2023年9月30日