前后端分离后台api接口框架探索
前言
很久没写文章了,今天有时间,把自己一直以来想说的,写出来,算是一种总结吧! 这篇文章主要说前后端分离模式下(也包括app开发),自己对后台框架和与前端交互的一些理解和看法。
前后端分离,一般传递json数据,对于出参,现在通用的做法是,包装一个响应类,里面包含code,msg,data三个属性,code代表状态码,msg是状态码对应的消息,data是返回的数据。
如 {"code":"10008","message":"手机号不存在","totalRows":null,"data":null}
对于入参,如果没有规范,可是各式各样的,比如:
UserController的getById方法,可能是这样的:

如果是把变量放在url,是这样的:

比如 addUser方法,如果是用user类直接接收参数,是这样的:

这样在前后端不分离的情况下,自己前后端代码都写,是没有啥问题,但是前后端分离情况下,如果这样用user类接收参数,如果你用了swagger来生成接口文档,那么,User类里面的一些对于前段来说没用的字段(createTime、isDel、updateTime。。。),也都会给前端展示出来,这时候前端得来问你,哪些参数是有用的,哪些是没用的。其实每个接口,对前端没用的参数,最好是不要给他展示,所以,你定义了一个AddUserRequest类,去掉了那些没用的字段,来接收addUser方法的参数:

如果入参用json格式,你的方法是这样的:

如果多个人开发一个项目,很可能代码风格不统一,你传递 json ,他是 form提交,你用rest在url传递变量,他用?id=100 来传参,,,,
分页查询,不同的人不同的写法:

慢慢你的项目出现了一大堆的自定义请求和响应对象:(请求响应对象和DTO还是很有必要的,无可厚非)

而且随着项目代码的增多,service、Controller方法越来越多,自己写的代码,自己还得找一会才能找到某个方法。出了问题,定位问题不方便,团队技术水平参差不齐(都这样的),无法约束每个人的代码按照同一个套路去写的规范些。
等等等。。。
正文
鉴于此,个人总结了工作中遇到的好的设计,开发了这个前后端分离的api接口框架(逐渐完善中):

技术选型:springboot,mybatis
框架大概是这个结构:前后端以 http json传递消息,所有请求经过 统一的入口,所以项目只有一个Controller入口 ,相当于一个轻量级api网关吧,不同的就是多了一层business层,也可以叫他manager层,一个business只处理一个接口请求。

先简单介绍下框架,先从接口设计说起,前后端以http 传递json的方式进行交互,消息的结构如下:
消息分 Head、body级:
{
"message":{
"head":{
"transactionType":"10130103",
"resCode":"",
"message":"",
"token":"9007c19e-da96-4ddd-84d0-93c6eba22e68",
"timestamp":"1565500145022",
"sign":"97d17628e4ab888fe2bb72c0220c28e3"
},
"body":{"userId":"10","hospitalId":"5"}
}
}
参数说明:
head:token、时间戳timestamp、md5签名sign、响应状态码resCode,响应消息message。transtransactionType:每个接口的编号,这个编号是有规则的。
body:具体的业务参数
项目是统一入口,如 http://localhost:8888/protocol ,所有接口都请求这个入口,传递的json格式,所以对前端来说,感觉是很方便了,每次请求,只要照着接口文档,换transtransactionType 和body里的具体业务参数即可。
响应参数:
{
"message": {
"head": {
"transactionType": "10130103",
"resCode": "101309",
"message": "时间戳超时",
"token": "9007c19e-da96-4ddd-84d0-93c6eba22e68",
"timestamp": "1565500145022",
"sign": "97d17628e4ab888fe2bb72c0220c28e3"
},
"body": {
"resCode": "101309",
"message": "时间戳超时"
}
}
}
贴出来统一入口的代码:
@RestController
public class ProtocolController extends BaseController{ private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolController.class); @PostMapping("/protocol")
public ProtocolParamDto dispatchCenter(@RequestParam("transMessage") String transMessage){
long start = System.currentTimeMillis();
//请求协议参数
LOGGER.info("transMessage---" + transMessage);
//响应对象
ProtocolParamDto result = new ProtocolParamDto();
Message message = new Message();
//协议号
String transactionType = ""; //请求header
HeadBean head = null;
//响应参数body map
Map<String, Object> body = null; try {
//1-请求消息为空
if (Strings.isNullOrEmpty(transMessage)) {
LOGGER.info("[" + ProtocolCodeMsg.REQUEST_TRANS_MESSAGE_NULL.getMsg() + "]:transMessage---" + transMessage);
return buildErrMsg(result,ProtocolCodeMsg.REQUEST_TRANS_MESSAGE_NULL.getCode(),
ProtocolCodeMsg.REQUEST_TRANS_MESSAGE_NULL.getMsg(),new HeadBean());
}
// 请求参数json转换为对象
ProtocolParamDto paramDto = JsonUtils.jsonToPojo(transMessage,ProtocolParamDto.class);
//2-json解析错误
if(paramDto == null){
return buildErrMsg(result,ProtocolCodeMsg.JSON_PARS_ERROR.getCode(),
ProtocolCodeMsg.JSON_PARS_ERROR.getMsg(),new HeadBean());
} // 校验数据
ProtocolParamDto validParamResult = validParam(paramDto, result);
if (null != validParamResult) {
return validParamResult;
} head = paramDto.getMessage().getHead();
//消息业务参数
Map reqBody = paramDto.getMessage().getBody(); //判断是否需要登录
//协议号
transactionType = head.getTransactionType(); //从spring容器获取bean
BaseBiz baseBiz = SpringUtil.getBean(transactionType);
if (null == baseBiz) {
LOGGER.error("[" + ProtocolCodeMsg.TT_NOT_ILLEGAL.getMsg() + "]:协议号---" + transactionType);
return buildErrMsg(result, ProtocolCodeMsg.TT_NOT_ILLEGAL.getCode(), ProtocolCodeMsg.TT_NOT_ILLEGAL.getMsg(), head);
}
//获取是否需要登录注解
Authentication authentication = baseBiz.getClass().getAnnotation(Authentication.class);
boolean needLogin = authentication.value();
System.err.println("获取Authentication注解,是否需要登录:"+needLogin);
if(authentication != null && needLogin){
ProtocolParamDto validSignResult = validSign(head, reqBody, result);
if(validSignResult != null){
return validSignResult;
}
}
// 参数校验
final Map<String, Object> validateParams = baseBiz.validateParam(reqBody);
if(validateParams != null){
// 请求参数(body)校验失败
body = validateParams;
}else {
//请求参数body校验成功,执行业务逻辑
body = baseBiz.processLogic(head, reqBody);
if (null == body) {
body = new HashMap<>();
body.put("resCode", ProtocolCodeMsg.SUCCESS.getCode());
body.put("message", ProtocolCodeMsg.SUCCESS.getMsg());
}
body.put("message", "成功");
}
// 将请求头更新到返回对象中 更新时间戳
head.setTimestamp(String.valueOf(System.currentTimeMillis()));
//
head.setResCode(ProtocolCodeMsg.SUCCESS.getCode());
head.setMessage(ProtocolCodeMsg.SUCCESS.getMsg());
message.setHead(head);
message.setBody(body);
result.setMessage(message); }catch (Exception e){
LOGGER.error("[" + ProtocolCodeMsg.SERVER_BUSY.getMsg() + "]:协议号---" + transactionType, e);
return buildErrMsg(result, ProtocolCodeMsg.SERVER_BUSY.getCode(), ProtocolCodeMsg.SERVER_BUSY.getMsg(), head);
}finally {
LOGGER.error("[" + transactionType + "] 调用结束返回消息体:" + JsonUtils.objectToJson(result));
long currMs = System.currentTimeMillis();
long interval = currMs - start;
LOGGER.error("[" + transactionType + "] 协议耗时: " + interval + "ms-------------------------protocol time consuming----------------------");
}
return result;
} }
在BaseController进行token鉴权:
/**
* 登录校验
* @param head
* @return
*/
protected ProtocolParamDto validSign(HeadBean head,Map reqBody,ProtocolParamDto result){
//校验签名
System.err.println("这里校验签名: ");
//方法是黑名单,需要登录,校验签名
String accessToken = head.getToken();
//token为空
if(StringUtils.isBlank(accessToken)){
LOGGER.warn("[{}]:token ---{}",ProtocolCodeMsg.TOKEN_IS_NULL.getMsg(),accessToken);
return buildErrMsg(result,ProtocolCodeMsg.TOKEN_IS_NULL.getCode(),ProtocolCodeMsg.TOKEN_IS_NULL.getMsg(),head);
}
//黑名单接口,校验token和签名 // 2.使用MD5进行加密,在转化成大写
Token token = tokenService.findByAccessToken(accessToken);
if(token == null){
LOGGER.warn("[{}]:token ---{}",ProtocolCodeMsg.SIGN_ERROR.getMsg(),accessToken);
return buildErrMsg(result,ProtocolCodeMsg.SIGN_ERROR.getCode(),ProtocolCodeMsg.SIGN_ERROR.getMsg(),head);
}
//token已过期
if(new Date().after(token.getExpireTime())){
//token已经过期
System.err.println("token已过期");
LOGGER.warn("[{}]:token ---{}",ProtocolCodeMsg.TOKEN_EXPIRED.getMsg(),accessToken);
return buildErrMsg(result,ProtocolCodeMsg.TOKEN_EXPIRED.getCode(),ProtocolCodeMsg.TOKEN_EXPIRED.getMsg(),head);
}
//签名规则: 1.已指定顺序拼接字符串 secret+method+param+token+timestamp+secret
String signStr = token.getAppSecret()+head.getTransactionType()+JsonUtils.objectToJson(reqBody)+token.getAccessToken()+head.getTimestamp()+token.getAppSecret();
System.err.println("待签名字符串:"+signStr);
String sign = Md5Util.md5(signStr);
System.err.println("md5签名:"+sign);
if(!StringUtils.equals(sign,head.getSign())){
LOGGER.warn("[{}]:token ---{}",ProtocolCodeMsg.SIGN_ERROR.getMsg(),sign);
return buildErrMsg(result,ProtocolCodeMsg.SIGN_ERROR.getCode(),ProtocolCodeMsg.SIGN_ERROR.getMsg(),head);
}
return null;
}
business代码分两部分

BaseBiz:所有的business实现该接口,这个接口只做两件事,1-参数校验,2-处理业务,感觉这一步可以规范各个开发人员的行为,所以每个人写出来的代码,都是一样的套路,看起来会很整洁
/**
* 所有的biz类实现此接口
*/
public interface BaseBiz { /**
* 参数校验
* @param paramMap
* @return
*/
Map<String, Object> validateParam(Map<String,String> paramMap) throws BusinessException; /**
* 处理业务逻辑
* @param head
* @param body
* @return
* @throws BusinessException
*/
Map<String, Object> processLogic(HeadBean head,Map<String,String> body) throws BusinessException;
}
一个business实现类:business只干两件事,参数校验、执行业务逻辑,所以项目里business类会多些,但是那些请求request类,都省了。
@Authentication(value = true) 是我定义的一个注解,标识该接口是否需要登录,暂时只能这样搞了,看着一个business上有两个注解很不爽,以后考虑自定义一个注解,兼顾把business成为spring的bean的功能,就能省去@Component注解了。
/**
* 获取会员信息,需要登录
*/
@Authentication(value = true)
@Component("10130102")
public class MemberInfoBizImpl implements BaseBiz { @Autowired
private IMemberService memberService; @Autowired
private ITokenService tokenService; @Override
public Map<String, Object> validateParam(Map<String, String> paramMap) throws BusinessException {
Map<String, Object> resultMap = new HashMap<>(); // 校验会员id
String memberId = paramMap.get("memberId");
if(Strings.isNullOrEmpty(memberId)){
resultMap.put("resCode", ProtocolCodeMsg.REQUEST_USER_MESSAGE_ERROR.getCode());
resultMap.put("message", ProtocolCodeMsg.REQUEST_USER_MESSAGE_ERROR.getMsg());
return resultMap;
}
return null;
} @Override
public Map<String, Object> processLogic(HeadBean head, Map<String, String> body) throws BusinessException {
Map<String, Object> map = new HashMap<>();
String memberId = body.get("memberId");
Member member = memberService.selectById(memberId);
if(member == null){
map.put("resCode", ProtocolCodeMsg.USER_NOT_EXIST.getCode());
map.put("message", ProtocolCodeMsg.USER_NOT_EXIST.getMsg());
return map;
}
map.put("memberId",member.getId());//会员id
map.put("username",member.getUsername());//用户名
return map;
}
}
关于接口安全:
1、基于Token安全机制认证
a. 登陆鉴权
b. 防止业务参数篡改
c. 保护用户敏感信息
d. 防签名伪造
2、Token 认证机制整体架构
整体架构分为Token生成与认证两部分:
1. Token生成指在登陆成功之后生成 Token 和密钥,并其与用户隐私信息、客户端信息一起存储至Token
表,同时返回Token 与Secret 至客户端。
2. Token认证指客户端请求黑名单接口时,认证中心基于Token生成签名

Token表结构说明:

具体代码看 github:感觉给你带来了一点用处的话,给个小星星吧谢谢
https://github.com/lhy1234/NB-api
前后端分离后台api接口框架探索的更多相关文章
- swagger -- 前后端分离的API接口
文章目录 一.背景 二.swagger介绍 三.在maven+springboot项目中使用swagger 四.swagger在项目中的好处 五.美化界面 参考链接:5分钟学会swagger配置 参考 ...
- 前后端分离后API交互如何保证数据安全性
前后端分离后API交互如何保证数据安全性? 一.前言 前后端分离的开发方式,我们以接口为标准来进行推动,定义好接口,各自开发自己的功能,最后进行联调整合.无论是开发原生的APP还是webapp还是PC ...
- 前后端分离后API交互如何保证数据安全性?
一.前言 前后端分离的开发方式,我们以接口为标准来进行推动,定义好接口,各自开发自己的功能,最后进行联调整合.无论是开发原生的APP还是webapp还是PC端的软件,只要是前后端分离的模式,就避免不了 ...
- 如何处理好前后端分离的 API 问题(转载自知乎)
9 个月前 API 都搞不好,还怎么当程序员?如果 API 设计只是后台的活,为什么还需要前端工程师. 作为一个程序员,我讨厌那些没有文档的库.我们就好像在操纵一个黑盒一样,预期不了它的正常行为是什么 ...
- 前后端分离后台管理系统 Gfast v3.0 全新发布
GFast V3.0 平台简介 基于全新Go Frame 2.0+Vue3+Element Plus开发的全栈前后端分离的管理系统 前端采用vue-next-admin .Vue.Element UI ...
- 前后端分离之【接口文档管理及数据模拟工具docdoc与dochelper】
前后端分离的常见开发方式是: 后端:接收http请求->根据请求url及params处理对应业务逻辑->将处理结果序列化为json返回 前端:发起http请求并传递相关参数->获取返 ...
- SpringBoot实现JWT保护前后端分离RESTful API
通常情况下, 将api直接暴露出来是非常危险的. 每一个api呼叫, 用户都应该附上额外的信息, 以供我们认证和授权. 而JWT是一种既能满足这样需求, 而又简单安全便捷的方法. 前端login获取J ...
- 《Spring Boot 入门及前后端分离项目实践》系列介绍
课程计划 课程地址点这里 本课程是一个 Spring Boot 技术栈的实战类课程,课程共分为 3 个部分,前面两个部分为基础环境准备和相关概念介绍,第三个部分是 Spring Boot 项目实践开发 ...
- 基于shiro+jwt的真正rest url权限管理,前后端分离
代码地址如下:http://www.demodashi.com/demo/13277.html bootshiro & usthe bootshiro是基于springboot+shiro+j ...
随机推荐
- Windows使用Python统一设置解析器路径
碰到的问题: .py文件放在cgi-bin文件夹下面,这个.py文件都要设置"#!python.exe路径"来告诉CGI如何找解析器解析这个.py的文件,我是想知道这个路径可否统一 ...
- python 中的__name__ == "__main__"(转)
有句话经典的概括了这段代码的意义: “Make a script both importable and executable” 意思就是说让你写的脚本模块既可以导入到别的模块中用,另外该模块自己也可 ...
- echo-nginx-module的安装、配置、使用
一.下载压缩包 [root@www nginx-1.16.0]# wget https://github.com/openresty/echo-nginx-module/archive/v0.61.t ...
- 《转载黑马教程》HTML&&CSS讲义0,,包含教程_仅供参考
今日内容 1. web概念概述 2. HTML web概念概述 * JavaWeb: * 使用Java语言开发基于互联网的项目 * 软件架构: 1. C/S: Client/Server 客户端/服务 ...
- 手动启动log4j|nginx实现http https共存
手动加载log4j.xml文件 DOMConfigurator.configure("src/main/resources/log4j.xml"); log4j.propertie ...
- vue+element项目中使用el-dialog弹出Tree控件报错问题
1. 按正常的点击按钮,显示dialog弹出的Tree控件,然后把该条数据下的已经选中的checkbox , 用setCheckedNodes或者setCheckedKeys方法选择上 , 报下面这个 ...
- Python入门基础(9)__面向对象编程_2
__str__方法 如果在开发中,希望使用print输出对象变量时,能够打印自定义的内容,就可以利用__str__这个内置方法了 注意:__str__方法必须返回一个字符串 class Cat(): ...
- React躬行记(3)——组件
组件(Component)由若干个React元素组成,包含属性.状态和生命周期等部分,满足独立.可复用.高内聚和低耦合等设计原则,每个React应用程序都是由一个个的组件搭建而成,即组成React应用 ...
- 个人永久性免费-Excel催化剂功能第44波-可见区域复制粘贴不覆盖隐藏内容
Excel的复制粘贴操作,每天都在进行,若其中稍能提升一点效率,长久来说,实在是很可观的效率提升. Excel自带的复制粘贴功能,若复制的数据源或粘贴的目标位置中有隐藏的行列内容,简单一个复制粘贴充满 ...
- I/O的简介
文本我们能读懂的都可以认为是字符流,文章 java文件都是字符流数据 流的分类 输入流 输出流 1.输出流 Writer:关于字符流的父类,抽象类.与之相对的输入流 Reader类 一.字符流 字符流 ...