1. 程式人生 > >前後端分離後臺api介面框架探索

前後端分離後臺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

 

 

 

 

 

  

&n