项目学习 鱼皮用户中心

前言

学习程序员鱼皮用户中心开源项目:https://github.com/liyupi/user-center-backend-public

以后端为主

后端技术选型

  • Java 编程语言
  • Spring + SpringMVC + SpringBoot 框架
  • MyBatis + MyBatis Plus 数据访问框架
  • MySQL 数据库
  • jUnit 单元测试库

项目分析

项目结构

  • common
    • BaseResponse:通用返回类,统一前端响应格式
      • code:错误码
      • data:数据
      • message:描述错误类别
      • description:具体错误描述
    • ErrorCode:枚举类,封装常用的错误信息
    • ResultUtils:将 return new BaseResponse(...) 语句封装成方法,增强代码易读性
      • success(T data):封装正常响应
      • error(ErrorCode errorCode):封装异常响应,设置响应数据为 null
    • WebMvcConfg:解决跨域问题
  • constant
    • UserConstant:接口,所有属性均为 public static,存放相关常量
  • controller
    • UserController:配置 RESTful API,同时进行简单的数据合法性检验
  • exception
    • BussinessException:继承 RuntimeException 类的自定义异常,用于描述业务异常,以实现业务异常与系统异常的区分
    • GlobalExceptionHandler:统一异常处理器,全局异常捕获后向前端返回统一的异常响应,用抛出业务异常的形式替代直接向前端返回异常处理的形式
  • mapper
    • UserMapper:继承 MyBatis Plus 提供的 BaseMapper,提供数据库访问服务
  • model
    • entity
      • User:数据库映射对象
    • request
      • UserLoginRequest:前端请求封装
      • UserRegisterRequest:前端请求封装
  • service
    • impl
      • UserServiceImpl
    • UserService:实现用户相关业务功能

数据库表设计

  • 业务属性:业务相关的数据属性
    • username(用户昵称): varchar(256)
    • id(用户ID): bigint(auto_increment, primary key)
    • userAccount(账号): varchar(256)
    • avatarUrl(用户头像): varchar(1024)
    • gender(性别): tinyint
    • userPassword(密码): varchar(512)
    • phone(电话): varchar(128)
    • email(邮箱): varchar(512)
    • userStatus(状态): int(default 0)
    • userRole(用户角色): int(default 0)
  • 通用属性:业务无关的数据属性,但凡关系表都应该有的基本属性
    • createTime(创建时间): datetime(default CURRENT_TIMESTAMP)
    • updateTime(更新时间): datetime(default CURRENT_TIMESTAMP, on update CURRENT_TIMESTAMP)
    • isDelete(是否删除): tinyint(default 0)
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)                       not null comment '密码',
    phone        varchar(128)                       null comment '电话',
    email        varchar(512)                       null comment '邮箱',
    userStatus   int      default 0                 not null comment '状态 0 - 正常',
    userRole     int      default 0                 not null comment '用户角色 0 - 普通用户 1 - 管理员',
 
  # 通用属性
    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 '用户';

知识补充:关系数据库的表可以称为关系表,因为它们是基于关系模型来组织数据的。而 NoSQL 数据库中的数据组织形式较为灵活,不一定遵循关系模型,所以通常称为集合(Collection)或者文档(Document)。

关键业务

登录注册属于常规业务,不做记录

用户密码加密
//盐值,混淆密码
private static final String SALT = "salt";

...
  
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
通过 Session 存储用户态
// 用户脱敏:创建对象副本,将敏感数据置空,其他数据复制
User safetyUser = getSafetyUser(user);
// 将用户的登录态存储到 Session 中
// Session 中的 Attribute 是一个键值对 <K, V> = <USER_LOGIN_STATE, safetyUser>
request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);
通过 Session 获取用户态

为什么不直接返回用户态?因为用户态的信息相当于缓存,但凡引入缓存,就可能出现数据一致性问题

@GetMapping("/current")
public BaseResponse<User> getCurrentUser(HttpServletRequest request) {
  	//1.从 Session 中获取用户的登录态(按键取值)
    Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
    User currentUser = (User) userObj;
    if (currentUser == null) {
        throw new BusinessException(ErrorCode.NOT_LOGIN);
    }
  	//2.获得用户 Id
    long userId = currentUser.getId();
  
    //TODO 校验用户是否合法
    // 如果引入多种用户状态(即数据库表中的 userStatus 不止 '0-正常' 这一种状态,还有被封号等其他状态)
  	// 则需要先校验用户是否合法,如果合法,再从数据库中获取用户信息;如果不合法,则进行异常处理
  
  	//3.通过用户 Id 从数据库中获取用户信息
    User user = userService.getById(userId);
  
  	//4.数据脱敏
    User safetyUser = userService.getSafetyUser(user);
  	
  	//5.返回用户信息
    return ResultUtils.success(safetyUser);
}
利用用户态实现接口鉴权
鉴权函数声明
private boolean isAdmin(HttpServletRequest request) {
    //从 Session 中获取用户信息,若用户信息非空且用户权限合法,则返回 true,否则返回 false
    Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
    User user = (User) userObj;
    return user != null && user.getUserRole() == ADMIN_ROLE;
}
鉴权函数使用
if (!isAdmin(request)) {
    throw new BusinessException(ErrorCode.NO_AUTH);
}

可复用机制

统一封装机制

前言
为什么要进行统一封装?

根本目标是返回前端友好的响应

为什么要为前端提供友好的响应?

从前后端分离的角度看,前后端分离的划分就相当于让前端放弃了业务逻辑的实现职责,专心实现用户交互,这种职责的让出导致后端系统在前端看来是一个黑箱,前端是无法判断是黑箱本身出问题还是自己的使用方式不对的

如何实现返回前端友好的响应?

从格式一致性的角度看,为了实现前端友好的响应,响应形式应该要统一,此处给出一种 BaseResponse(code、data、message、description)的习惯性标准响应格式,无论返回的是正确结果还是错误信息,都用一套格式

从开发分工的角度看,为了实现前端友好的响应,要让前端开发者明确是前端请求的问题,还是后端系统的问题,统一封装通过引入自定义运行时异常(BusinessException)来区分前后端的异常,方便前端定位错误

从提供信息的角度看,为了实现前端友好的响应,应该提供详细的错误信息,此处给出一种 ErrorCode(code、message、description)的习惯性错误信息组织形式

  • ErrorCode
    • code 可以参考 HTTP 中的状态码进行扩充
      • 200:ok
      • 4XX:客户端错误
      • 5XX:服务器错误
    • message 用于描述错误的大类
      定性简单描述,通常必填
    • description 用于进一步描述错误信息
      详细描述,通常选填
补充:关于 AOP 的感受

从实现手段来看,无论是否引入 AOP,正常的返回结果都是在 Controller 中进行封装的,区别在于错误信息的返回是否引入了 AOP 进行全局异常拦截,可以分为

  • 不引入 AOP 直接返回错误信息:在出错的地方直接返回带错误信息的统一形式响应

    graph LR 请求参数为空 --> 返回请求参数为空的响应 请求参数错误 --> 返回请求参数错误的响应 ... --> 返回...的响应
  • 引入 AOP 统一返回错误信息:先抛出异常,再通过全局异常拦截器拦截,在全局异常拦截器进行统一返回统一形式响应(本项目采用的实现手段)
    这样做一方面使得响应处理显得没那么零散,另一方面也使得统一记录错误日志成为可能

    graph LR 请求参数为空 --> 抛出请求参数为空的自定义异常 --> 全局异常拦截器 请求参数错误 --> 抛出请求参数错误的自定义异常 --> 全局异常拦截器 ... --> 抛出...的自定义异常 --> 全局异常拦截器 系统出错 --> 抛出运行时异常 --> 全局异常拦截器 全局异常拦截器 --> 统一记录错误日志并返回响应

先前对 AOP 的认知过于狭隘,将其定位为一种针对某项流程(小粒度)的增强技术(外置装甲、ガンダム),在接触了统一封装之后,也感受到了 AOP 在整体流程(大粒度)中的整合作用(统一管理)

统一封装机制实现流程

统一封装

    • 相当于提供自定义错误码模板

    • 比如说,成功时返回 200 OK,失败时返回 对应的错误码 + message

      如果只用 BaseResponse 则不太容易看出来是成功还是失败,所以再做一层封装,正确的封装成函数 success,出错的封装成函数 error

      从增强代码可读性的角度看,其作用与 Request 的封装的作用是一样的,Request 也是将前端的表单数据组织成对象方便使用,避免引入难看的 Map

统一封装机制运行流程

异常情况

graph LR n["出错了:抛出异常"] --> 异常统一处理 --> 返回统一响应

正常情况

graph LR n["处理完成:返回统一响应"]
统一封装机制复用
  1. 复制 common 和 exception 文件夹到新项目
  2. 使用统一封装机制改写或编写业务代码
    • 业务异常定义
    • 业务异常抛出
    • 返回统一响应

跨域后端解决方案

前言
什么是跨域?

浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域。

为什么会出现跨域问题?

出于浏览器的同源策略限制。

所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)

同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互。

解决过程
原理

在请求头中加入 Access-Control-Allow-Origin 相关配置信息

实践

重写 WebMvcConfigurer 的方式以解决跨域问题

package edu.hitwh.werunassignment.model.request;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfg implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置允许跨域的路径
        registry.addMapping("/**")
                //设置允许跨域请求的域名
                //当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
                .allowedOrigins("*")
                //是否允许证书 不再默认开启
                .allowCredentials(false)
                //设置允许的方法
                .allowedMethods("*")
                //跨域允许时间
                .maxAge(3600);
    }
}
跨域解决方案复用
  1. 复制 common 中的 WebMvcConfg.java 到新项目
  2. 根据实际需求配置 addCorsMappings(CorsRegistry registry) 方法中的代码

项目总结

  • 一种更规范的数据库表设计方案
    • 业务属性
    • 通用属性
      • createTime:创建时间
      • updateTime:更新时间
      • isDelete:逻辑删除
  • 两大可复用机制
    • 统一封装机制
    • 跨域解决方案(后端)
  • 三种关键业务
    • 用户密码加密:引入盐值和加密函数
    • 基于 Session 的用户态存取
    • 利用用户态实现接口鉴权
posted @ 2023-08-21 10:50  Ba11ooner  阅读(474)  评论(0编辑  收藏  举报