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

      

  • 相关阅读:
    four day (linux基础学习1)
    three day(网络基础)
    two day(操作系统)
    Oldboy One day(计算机基础)
    mybatis调用存储过程
    调用jiathis分享接口
    汉字转换拼音工具类
    Ajax前后台交互 返回普通格式和JSON格式
    Bootstrap与EasyUI入门
    MD5,sha1加密工具类
  • 原文地址:https://www.cnblogs.com/lihaoyang/p/11334925.html
Copyright © 2020-2023  润新知