首页 > 技术文章 > 互联网开放平台API安全设计

toov5 2019-01-24 12:41 原文

互联网开放平台设计
1.需求:现在A公司与B公司进行合作,B公司需要调用A公司开放的外网接口获取数据,
如何保证外网开放接口的安全性。
2.常用解决办法:
2.1 使用加签名方式,防止篡改数据
2.2 使用Https加密传输
2.3 搭建OAuth2.0认证授权
2.4 使用令牌方式
2.5 搭建网关实现黑名单和白名单

 

案例:A公司调用B公司的外网接口获取数据,如何保证外网开放接口的安全性。

  

如何保证外网开放接口的安全性:

 1.搭建API网关控制接口访问权限

 2.开放平台设计  开放凭他设计oauth2.0协议   QQ授权 第三方联合登录

 3.采用Https加密传输协议 (使用Nginx配置Https)

 4.API接口数字签名(移动的接口)非对称加密RSA   

 5. 基于令牌方式实现API接口调用。基于accessToken实现API调用   防止抓包分析篡改数据  基于accessToken实现AP调用

 

 

基于令牌方式实现:

 表的设计:

  开放平台提供者需要为每个合作机构提供对应的APPID   APPSerect 

  基于令牌方式实现   AppId区分不同结构 永远不能变   AppSerect 在传输中实现加密功能(密钥) 可以发生改变   Appid+AppSerect生成对应access_token

 
表字段:

  App_Name 表机构名称

  App_ID  应用id 

  App_Serect  应用密钥(可更改)

  Is_flag  是否可用 

  acess_token  上一次access_token

 

开发步骤:

  Appid+AppSerect 对应生成accessToken 

  对应accessToken去表里查询。查询出的结果要求is_flag 是1, 0 代表不开放权限了

  

 

 

使用令牌方式搭建搭建API开放平台
原理:为每个合作机构创建对应的appid、app_secret,生成对应的access_token(有效期2小时),在调用外网开放接口的时候,必须传递有效的access_token。

 

 生成accessToken之后

 用accessToken对接口进行调用 此时会被 配置  API/* 拦截到,获取到携带哦的accessToken,然后去redis中查询对应的appId,如果redis中有对应的appId,利用appId去表中查询对应的app信息

 信息包括isFlag 看权限是否开启 如果开启 继续往下走

 放行到被拦截的 业务逻辑中

 

 

生成accessToken:

 controller:

  

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSONObject;
import com.itmayiedu.base.BaseApiService;
import com.itmayiedu.base.ResponseBase;
import com.itmayiedu.entity.AppEntity;
import com.itmayiedu.mapper.AppMapper;
import com.itmayiedu.utils.BaseRedisService;
import com.itmayiedu.utils.TokenUtils;

// 创建获取getAccessToken
@RestController
@RequestMapping(value = "/auth")
public class AuthController extends BaseApiService {
    @Autowired
    private BaseRedisService baseRedisService;
    private long timeToken = 60 * 60 * 2;
    @Autowired
    private AppMapper appMapper;

    // 使用appId+appSecret 生成AccessToke
    @RequestMapping("/getAccessToken")
    public ResponseBase getAccessToken(AppEntity appEntity) {
        AppEntity appResult = appMapper.findApp(appEntity);
        if (appResult == null) {
            return setResultError("没有对应机构的认证信息");
        }
        int isFlag = appResult.getIsFlag();
        if (isFlag == 1) {
            return setResultError("您现在没有权限生成对应的AccessToken");
        }
        // ### 获取新的accessToken 之前删除之前老的accessToken
        // 从redis中删除之前的accessToken
        String accessToken = appResult.getAccessToken();
        baseRedisService.delKey(accessToken);
        // 生成的新的accessToken
        String newAccessToken = newAccessToken(appResult.getAppId());
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("accessToken", newAccessToken);
        return setResultSuccessData(jsonObject);  //继承的属性
    }

    private String newAccessToken(String appId) {
        // 使用appid+appsecret 生成对应的AccessToken 保存两个小时   保证唯一且临时
        String accessToken = TokenUtils.getAccessToken();
        // 保证在同一个事物redis 事务中
        // 生成最新的token key为accessToken value 为 appid
        baseRedisService.setString(accessToken, appId, timeToken);  //key为accessToken  
        // 表中保存当前accessToken
        appMapper.updateAccessToken(accessToken, appId);
        return accessToken;
    }
}

去调用API接口:

拦截器去进行处理验证:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.alibaba.fastjson.JSONObject;
import com.itmayiedu.base.BaseApiService;
import com.itmayiedu.utils.BaseRedisService;

//验证AccessToken 是否正确
@Component
public class AccessTokenInterceptor extends BaseApiService implements HandlerInterceptor {
    @Autowired
    private BaseRedisService baseRedisService;
    
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o)
            throws Exception {
        System.out.println("---------------------开始进入请求地址拦截----------------------------");
        String accessToken = httpServletRequest.getParameter("accessToken");
        // 判断accessToken是否空
        if (StringUtils.isEmpty(accessToken)) {
            // 参数Token accessToken
            resultError(" this is parameter accessToken null ", httpServletResponse);
            return false;
        }
        String appId = (String) baseRedisService.getString(accessToken);
        if (StringUtils.isEmpty(appId)) {
            // accessToken 已经失效!
            resultError(" this is  accessToken Invalid ", httpServletResponse);
            return false;
        }
        // 正常执行业务逻辑...
        return true;

    }

    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o,
            ModelAndView modelAndView) throws Exception {
        System.out.println("--------------处理请求完成后视图渲染之前的处理操作---------------");
    }

    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
            Object o, Exception e) throws Exception {
        System.out.println("---------------视图渲染之后的操作-------------------------0");
    }

    // 返回错误提示
    public void resultError(String errorMsg, HttpServletResponse httpServletResponse) throws IOException {
        PrintWriter printWriter = httpServletResponse.getWriter();
        printWriter.write(new JSONObject().toJSONString(setResultError(errorMsg)));
    }

}

拦截器的配置类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebAppConfig {
    @Autowired
    private AccessTokenInterceptor accessTokenInterceptor;

    @Bean
    public WebMvcConfigurer WebMvcConfigurer() {
        return new WebMvcConfigurer() {
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(accessTokenInterceptor).addPathPatterns("/openApi/*");
            };
        };
    }

}

 

接口controller:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.itmayiedu.base.BaseApiService;
import com.itmayiedu.base.ResponseBase;

@RestController
@RequestMapping("/openApi")
public class MemberController extends BaseApiService {

    @RequestMapping("/getUser")
    public ResponseBase getUser() {
        return setResultSuccess("获取会员信息接口");
    }
}

 

Base类:

@Component
public class BaseApiService {

    public ResponseBase setResultError(Integer code, String msg) {
        return setResult(code, msg, null);
    }

    // 返回错误,可以传msg
    public ResponseBase setResultError(String msg) {
        return setResult(Constants.HTTP_RES_CODE_500, msg, null);
    }

    // 返回成功,可以传data值
    public ResponseBase setResultSuccessData(Object data) {
        return setResult(Constants.HTTP_RES_CODE_200, Constants.HTTP_RES_CODE_200_VALUE, data);
    }

    public ResponseBase setResultSuccessData(Integer code, Object data) {
        return setResult(code, Constants.HTTP_RES_CODE_200_VALUE, data);
    }

    // 返回成功,沒有data值
    public ResponseBase setResultSuccess() {
        return setResult(Constants.HTTP_RES_CODE_200, Constants.HTTP_RES_CODE_200_VALUE, null);
    }

    // 返回成功,沒有data值
    public ResponseBase setResultSuccess(String msg) {
        return setResult(Constants.HTTP_RES_CODE_200, msg, null);
    }

    // 通用封装
    public ResponseBase setResult(Integer code, String msg, Object data) {
        return new ResponseBase(code, msg, data);
    }

}

 

package com.itmayiedu.base;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Getter
@Setter
@Slf4j
public class ResponseBase {

    private Integer rtnCode;
    private String msg;
    private Object data;

    public ResponseBase() {

    }

    public ResponseBase(Integer rtnCode, String msg, Object data) {
        super();
        this.rtnCode = rtnCode;
        this.msg = msg;
        this.data = data;
    }

    public static void main(String[] args) {
        ResponseBase responseBase = new ResponseBase();
        responseBase.setData("123456");
        responseBase.setMsg("success");
        responseBase.setRtnCode(200);
        System.out.println(responseBase.toString());
        log.info("itmayiedu...");
    }

    @Override
    public String toString() {
        return "ResponseBase [rtnCode=" + rtnCode + ", msg=" + msg + ", data=" + data + "]";
    }

}

entity:表的实体类

public class AppEntity {

    private long id;
    private String appId;
    private String appName;
    private String appSecret;
    private String accessToken;
    private int isFlag;

    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getAppId() {
        return appId;
    }
    public void setAppId(String appId) {
        this.appId = appId;
    }
    public String getAppName() {
        return appName;
    }

    public void setAppName(String appName) {
        this.appName = appName;
    }
    public String getAppSecret() {
        return appSecret;
    }
    public void setAppSecret(String appSecret) {
        this.appSecret = appSecret;
    }
    public int getIsFlag() {
        return isFlag;
    }
    public void setIsFlag(int isFlag) {
        this.isFlag = isFlag;
    }
    public String getAccessToken() {
        return accessToken;
    }
    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

}

Mapper:

 

ublic interface AppMapper {

    @Select("SELECT ID AS ID ,APP_NAME AS appName, app_id as appId, app_secret as appSecret ,is_flag as isFlag , access_token as accessToken from m_app "
            + "where app_id=#{appId} and app_secret=#{appSecret}  ")
    AppEntity findApp(AppEntity appEntity);

    @Select("SELECT ID AS ID ,APP_NAME AS appName, app_id as appId, app_secret as appSecret ,is_flag as isFlag  access_token as accessToken from m_app "
            + "where app_id=#{appId} and app_secret=#{appSecret}  ")
    AppEntity findAppId(@Param("appId") String appId);

    @Update(" update m_app set access_token =#{accessToken} where app_id=#{appId} ")
    int updateAccessToken(@Param("accessToken") String accessToken, @Param("appId") String appId);
}

 

 Utils类:

 

import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class BaseRedisService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public void setString(String key, Object data, Long timeout) {
        if (data instanceof String) {
            String value = (String) data;
            stringRedisTemplate.opsForValue().set(key, value);
        }
        if (timeout != null) {
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    public Object getString(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    public void delKey(String key) {
        stringRedisTemplate.delete(key);
    }

}
public interface Constants {
    // 响应请求成功
    String HTTP_RES_CODE_200_VALUE = "success";
    // 系统错误
    String HTTP_RES_CODE_500_VALUE = "fial";
    // 响应请求成功code
    Integer HTTP_RES_CODE_200 = 200;
    // 系统错误
    Integer HTTP_RES_CODE_500 = 500;
    // 未关联QQ账号
    Integer HTTP_RES_CODE_201 = 201;
    // 发送邮件
    String MSG_EMAIL = "email";
    // 会员token
    String TOKEN_MEMBER = "TOKEN_MEMBER";
    // 支付token
    String TOKEN_PAY = "TOKEN_pay";
    // 支付成功
    String PAY_SUCCESS = "success";
    // 支付白
    String PAY_FAIL = "fail";
    // 用户有效期 90天
    Long TOKEN_MEMBER_TIME = (long) (60 * 60 * 24 * 90);
    int COOKIE_TOKEN_MEMBER_TIME = (60 * 60 * 24 * 90);
    Long PAY_TOKEN_MEMBER_TIME = (long) (60 * 15);
    // cookie 会员 totoken 名称
    String COOKIE_MEMBER_TOKEN = "cookie_member_token";

}

 

public class TokenUtils {

    @RequestMapping("/getToken")
    public static String getAccessToken() {
        return UUID.randomUUID().toString().replace("-", "");
    }

}

yml:

spring:
  mvc:
    view:
      # 页面默认前缀目录
      prefix: /WEB-INF/jsp/
      # 响应页面默认后缀
      suffix: .jsp

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    test-while-idle: true
    test-on-borrow: true
    validation-query: SELECT 1 FROM DUAL
    time-between-eviction-runs-millis: 300000
    min-evictable-idle-time-millis: 1800000
  redis:
    database: 1
    host: 106.15.185.133
    port: 6379
    password: meitedu.+@
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0
    timeout: 10000

 

表单设计: 

  

CREATE TABLE `m_app` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(255) DEFAULT NULL,
`app_id` varchar(255) DEFAULT NULL,
`app_secret` varchar(255) DEFAULT NULL,
`is_flag` varchar(255) DEFAULT NULL,
`access_token` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

 

App_Name       表示机构名称

App_ID          应用id

App_Secret      应用密钥  (可更改)

Is_flag           是否可用 (是否对某个机构开放)

access_token  上一次access_token

 

小结: 第一步 先去生成accessToken。 如果获取最新的accessToken(使用appid+appsecret 生成对应的AccessToken 保存两个小时   保证唯一且临时),需要从redis删除原先的accessTokne。

           通过两个字段去查询:

           

 

            第二步去调用接口时候 会去拦截器进行限制 ,从redis获取appId,然后去app表中获取信息,看看是否有权限。

   

 

原理:为每个合作机构创建对应的appid、app_secret,生成对应的access_token(有效期2小时),在调用外网开放接口的时候,必须传递有效的access_token。

 

 

 

 

 

 

 

  

推荐阅读