前后端分离后台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

  

原文地址:https://www.cnblogs.com/lihaoyang/p/11334925.html

时间: 2024-07-30 12:42:48

前后端分离后台api接口框架探索的相关文章

前后端分离后API交互如何保证数据安全性

前后端分离后API交互如何保证数据安全性? 一.前言 前后端分离的开发方式,我们以接口为标准来进行推动,定义好接口,各自开发自己的功能,最后进行联调整合.无论是开发原生的APP还是webapp还是PC端的软件,只要是前后端分离的模式,就避免不了调用后端提供的接口来进行业务交互. 网页或者app,只要抓下包就可以清楚的知道这个请求获取到的数据,这样的接口对爬虫工程师来说是一种福音,要抓你的数据简直轻而易举. 数据的安全性非常重要,特别是用户相关的信息,稍有不慎就会被不法分子盗用,所以我们对这块要非

前后端开发过程中API接口管理有哪些痛点?附几种解决方案

一.API管理的痛点 API接口在设计时往往需要编写大量的文档,而且编写完成之后还会经常改动,文档编写维护工作量大. 接口文档编写好后,实际的代码可能会与文档有出入,这个时候文档是不准确的,文档与代码保持修改同步也是一个很大的工作量.随着接口版本的迭代,接口文档需要同步更新.有些时候接口会成为对接双方的开发进度瓶颈,因为接口调用会有依赖,类似app的项目,前端会需要调用后端接口,接口功能不实现会影响前端开发进度.接口开发完以后,做接口测试不方便,特别是接口数量多,参数复杂的情况,测试工作量大.接

前后端分离之【接口文档管理及数据模拟工具docdoc与dochelper】

前后端分离的常见开发方式是: 后端:接收http请求->根据请求url及params处理对应业务逻辑->将处理结果序列化为json返回 前端:发起http请求并传递相关参数->获取返回结果处理相关逻辑 分离的主要目的是让前后端可以并行的进行工作,彼此之间只需要依赖一份接口文档 接口文档可能会使用一些文本工具进行记录,例如word,excel等 其中记录的内容可能为请求路径,请求类型,请求参数,响应参数,请求示例,响应示例,变更记录等 不过以上方式还存在那么一点不完美,那就是前端需要等待后

微信自定义分享 laraverl 框架,前后端分离 报错 签名失效 63002

<?phpclass JSSDK {private $appId;private $appSecret;private $url;//如果是前后端分离,是接口请求,必须自定义当前页面地址 送过来 否则签名错误20190905public function __construct($appId, $appSecret,$url) {$this->appId = $appId;$this->appSecret = $appSecret;$this->url = $url;} publi

《Spring Boot 入门及前后端分离项目实践》系列介绍

课程计划 课程地址点这里 本课程是一个 Spring Boot 技术栈的实战类课程,课程共分为 3 个部分,前面两个部分为基础环境准备和相关概念介绍,第三个部分是 Spring Boot 项目实践开发.Spring Boot 介绍.前后端分离.API 规范等内容旨在让读者更加熟悉 SpringBoot 及企业开发中需要注意的事项并具有使用 SpringBoot 技术进行基本功能开发的能力:这最后的项目实战为课程的主要部分,我会带着大家实际的开发一个前后端分离的 Spring Boot 实践项目,

从壹开始前后端分离[.netCore 不定期 ] 36 ║解决JWT权限验证过期问题

缘起 哈喽,老张的不定期更新的日常又开始了,在咱们的前后端分离的.net core 框架中,虽然已经实现了权限验证<框架之五 || Swagger的使用 3.3 JWT权限验证[修改]>,只不过还是有一些遗留问题,最近有不少的小伙伴发现了这样的一些问题,本来想着直接就在原文修改,但是发现可能怕有的小伙伴看不到,就单发一条推送吧,所以我还是单写出一篇文章来说明解决这些问题,希望对无论是正在开发权限管理系统,还是平时需要数据库动态绑定权限分配的你有一些启发和思考.今天咱们注意解决这三个问题: 1.

Docker环境下的前后端分离项目部署与运维(一)项目简介及环境要求

项目简介 本教程将从零开始部署一个前后端分离的开源项目,利用docker虚拟机的容器技术,采用分布式集群部署,将项目转换成为高性能.高负载.高可用的部署方案.包括了MySQL集群.Redis集群.负载均衡.双机热备等等. 部署图 所用到的主流技术 Docker容器.前后端集群.MySQL集群.Redis集群.Haproxy负载均衡.Nginx负载均衡.Keepalived实现双机热备 前后端分离项目部署图 前后端分离项目开源框架介绍 本次教程所采用的前后端分离的项目开源框架是人人网的renren

从壹开始前后端分离【 .NET Core2.0 Api + Vue 3.0 + AOP + 分布式】框架之九 || 依赖注入IoC学习 + AOP界面编程初探

代码已上传Github,文末有地址 说接上文,上回说到了<从壹开始前后端分离[ .NET Core2.0 Api + Vue 2.0 + AOP + 分布式]框架之八 || API项目整体搭建 6.3 异步泛型+依赖注入初探>,后来的标题中,我把仓储两个字给去掉了,因为好像大家对这个模式很有不同的看法,嗯~可能还是我学艺不精,没有说到其中的好处,现在在学DDD领域驱动设计相关资料,有了好的灵感再给大家分享吧. 到目前为止我们的项目已经有了基本的雏形,后端其实已经可以搭建自己的接口列表了,框架已

从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之六 || API项目整体搭建 6.1

书接上文:前几回文章中,我们花了三天的时间简单了解了下接口文档Swagger框架,已经完全解放了我们的以前的Word说明文档,并且可以在线进行调试,而且当项目开始之中,我们可以定义一些空的接口,或者可以返回假数据,这样真正达到了前后端不等待的缺陷,还是很不错的,当然,这离我说的前后端分离还是相差甚远,今天呢,我们就简单搭建下我们的项目架构. 本项目是我自己的一个真实项目,数据都是真实的,之前搭建过一个MVC + EF Code First的项目,本项目就是基于这个了,前一段时间我已经搭建起来了,