欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

前后端分离后台api接口框架探索

程序员文章站 2023-01-01 20:38:21
前言 很久没写文章了,今天有时间,把自己一直以来想说的,写出来,算是一种总结吧! 这篇文章主要说前后端分离模式下(也包括app开发),自己对后台框架和与前端交互的一些理解和看法。 前后端分离,一般传递json数据,对于出参,现在通用的做法是,包装一个响应类,里面包含code,msg,data三个属性 ......

 前言

  很久没写文章了,今天有时间,把自己一直以来想说的,写出来,算是一种总结吧!  这篇文章主要说前后端分离模式下(也包括app开发),自己对后台框架和与前端交互的一些理解和看法。

     前后端分离,一般传递json数据,对于出参,现在通用的做法是,包装一个响应类,里面包含code,msg,data三个属性,code代表状态码,msg是状态码对应的消息,data是返回的数据。

  如  {"code":"10008","message":"手机号不存在","totalrows":null,"data":null}

  对于入参,如果没有规范,可是各式各样的,比如:

  usercontroller的getbyid方法,可能是这样的:

    前后端分离后台api接口框架探索

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

    前后端分离后台api接口框架探索

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

  前后端分离后台api接口框架探索

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

  前后端分离后台api接口框架探索

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

  前后端分离后台api接口框架探索

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

  分页查询,不同的人不同的写法:

  前后端分离后台api接口框架探索

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

    前后端分离后台api接口框架探索

    而且随着项目代码的增多,service、controller方法越来越多,自己写的代码,自己还得找一会才能找到某个方法。出了问题,定位问题不方便,团队技术水平参差不齐(都这样的),无法约束每个人的代码按照同一个套路去写的规范些。

    等等等。。。

  正文

    鉴于此,个人总结了工作中遇到的好的设计,开发了这个前后端分离的api接口框架(逐渐完善中):

    前后端分离后台api接口框架探索

    技术选型:springboot,mybatis

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

    前后端分离后台api接口框架探索

 

 

     先简单介绍下框架,先从接口设计说起,前后端以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代码分两部分

前后端分离后台api接口框架探索

 

 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生成签名

前后端分离后台api接口框架探索

token表结构说明:

前后端分离后台api接口框架探索

具体代码看 github:感觉给你带来了一点用处的话,给个小星星吧谢谢

  https://github.com/lhy1234/nb-api