先後端分離後臺api接口框架探索

 前言

  好久沒寫文章了,今天有時間,把本身一直以來想說的,寫出來,算是一種總結吧!  這篇文章主要說先後端分離模式下(也包括app開發),本身對後臺框架和與前端交互的一些理解和見解。html

     先後端分離,通常傳遞json數據,對於出參,如今通用的作法是,包裝一個響應類,裏面包含code,msg,data三個屬性,code表明狀態碼,msg是狀態碼對應的消息,data是返回的數據。前端

  如  {"code":"10008","message":"手機號不存在","totalRows":null,"data":null}git

  對於入參,若是沒有規範,但是各式各樣的,好比:github

  UserController的getById方法,多是這樣的:
spring

    

    若是是把變量放在url,是這樣的:json

    

  好比 addUser方法,若是是用user類直接接收參數,是這樣的:後端

  

  這樣在先後端不分離的狀況下,本身先後端代碼都寫,是沒有啥問題,可是先後端分離狀況下,若是這樣用user類接收參數,若是你用了swagger來生成接口文檔,那麼,User類裏面的一些對於前段來講沒用的字段(createTime、isDel、updateTime。。。),也都會給前端展現出來,這時候前端得來問你,哪些參數是有用的,哪些是沒用的。其實每一個接口,對前端沒用的參數,最好是不要給他展現,因此,你定義了一個AddUserRequest類,去掉了那些沒用的字段,來接收addUser方法的參數:api

  

  若是入參用json格式,你的方法是這樣的:安全

  

  若是多我的開發一個項目,極可能代碼風格不統一,你傳遞 json ,他是 form提交,你用rest在url傳遞變量,他用?id=100 來傳參,,,,springboot

  分頁查詢,不一樣的人不一樣的寫法:

  

    慢慢你的項目出現了一大堆的自定義請求和響應對象:(請求響應對象和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; } }

關於接口安全:

一、基於Token安全機制認證
  a. 登錄鑑權
  b. 防止業務參數篡改
  c. 保護用戶敏感信息
  d. 防簽名僞造
二、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

相關文章
相關標籤/搜索