好久没写文章了,今天有时间,把本身一直以来想说的,写出来,算是一种总结吧! 这篇文章主要说先后端分离模式下(也包括app开发),本身对后台框架和与前端交互的一些理解和见解。前端
先后端分离,通常传递json数据,对于出参,如今通用的作法是,包装一个响应类,里面包含code,msg,data三个属性,code表明状态码,msg是状态码对应的消息,data是返回的数据。git
如 {"code":"10008","message":"手机号不存在","totalRows":null,"data":null}github
对于入参,若是没有规范,但是各式各样的,好比:spring
UserController的getById方法,多是这样的:
json
若是是把变量放在url,是这样的:后端
好比 addUser方法,若是是用user类直接接收参数,是这样的:api
这样在先后端不分离的状况下,本身先后端代码都写,是没有啥问题,可是先后端分离状况下,若是这样用user类接收参数,若是你用了swagger来生成接口文档,那么,User类里面的一些对于前段来讲没用的字段(createTime、isDel、updateTime。。。),也都会给前端展现出来,这时候前端得来问你,哪些参数是有用的,哪些是没用的。其实每一个接口,对前端没用的参数,最好是不要给他展现,因此,你定义了一个AddUserRequest类,去掉了那些没用的字段,来接收addUser方法的参数:安全
若是入参用json格式,你的方法是这样的:springboot
若是多我的开发一个项目,极可能代码风格不统一,你传递 json ,他是 form提交,你用rest在url传递变量,他用?id=100 来传参,,,,mybatis
分页查询,不一样的人不一样的写法:
慢慢你的项目出现了一大堆的自定义请求和响应对象:(请求响应对象和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:感受给你带来了一点用处的话,给个小星星吧谢谢