3 -【 API 開放平臺安全設計 】- 1 基於 AccessToken 方式實現 API 設計

1 互聯網開放平臺設計

  1. 需求:現在A公司與B公司進行合作,B公司需要調用A公司開放的外網接口獲取數據,如何保證外網開放接口的安全性
  2. 常用解決辦法:
    2.1 使用加簽名方式,防止篡改數據
    2.2 使用 Https 加密傳輸
    2.3 搭建 OAuth2.0 認證授權
    2.4 使用令牌方式
    2.5 搭建網關實現黑名單和白名單

2 使用令牌方式搭建搭建 API 開放平臺

原理:爲每個合作機構創建對應的 appidapp_secret,生成對應的access_token(有效期2小時),在調用外網開放接口的時候,必須傳遞有效的 access_token

2.1 數據庫表設計

-- ----------------------------
-- Table structure for snow_app
-- ----------------------------
DROP TABLE IF EXISTS `snow_app`;
CREATE TABLE `snow_app`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `app_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `app_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `app_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `is_flag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `access_token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of snow_app
-- ----------------------------
INSERT INTO `snow_app` VALUES (1, '雪花', 'snow', 'snow123', '0', NULL);
字段名稱 說明
App_Name 表示機構名稱
App_ID 應用id(不可更改)
App_Secret 應用密鑰 (可更改,作用:加密)
Is_flag 是否可用 (是否對某個機構開放)
access_token 上一次 access_token

access_token 的作用:保證只有最後生成的 access_token 纔是可用的,之前生成的是不可用的。

2.2 環境準備

2.2.1 依賴

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.6.RELEASE</version>
    <relativePath/>
</parent>
<properties>
    <java.version>1.8</java.version>
</properties>
<dependencies>
    <!-- SpringBoot web 核心組件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
    </dependency>
    <!-- servlet 依賴 -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <scope>provided</scope>
    </dependency>
    <!-- JSTL 標籤庫 -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
    </dependency>
    <dependency>
        <groupId>taglibs</groupId>
        <artifactId>standard</artifactId>
        <version>1.1.2</version>
    </dependency>
    <!-- tomcat 的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
    <!-- SpringBoot 外部tomcat支持 -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <scope>provided</scope>
    </dependency>
    <!-- mysql 數據庫驅動. -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- spring-boot mybatis依賴 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <!-- SpringBoot 對lombok 支持 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- springboot-aop 技術 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- https://mvnrepository.com/artifact/commons-lang/commons-lang -->
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.47</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

2.2.2 配置文件

spring:
  datasource:
    url: jdbc:mysql://120.78.134.111:3306/springboot?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  mvc:
    view:
      prefix: /WEB-INF/jsp/
      suffix: .jsp
  redis:
    database: 1
    host: 120.78.134.111
    port: 6379
    password:
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0
    timeout: 10000

mybatis:
  type-aliases-package: com.snow.entity # mybatis 別名掃描

2.2.3 啓動項

package com.snow;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.snow.mapper")
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}

2.2.4 工具類

package com.snow.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;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.utils
 * @Description : Redis工具類
 * @date :2020/4/16 18:07
 */
@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);
    }

}

package com.snow.utils;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.utils
 * @Description : 返回結果
 * @date :2020/4/16 17:59
 */
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";

}

package com.snow.utils;

import java.util.UUID;

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

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.utils
 * @Description : 生成Token工具類
 * @date :2020/4/16 18:00
 */
public class TokenUtils {

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

}

2.2.5 實體類

package com.snow.entity;

import lombok.Data;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.entity
 * @Description : App
 * @date :2020/4/16 18:07
 */
@Data
public class App {

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

}

2.2.6 mapper

package com.snow.mapper;

import com.snow.entity.App;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.mapper
 * @Description : AppMapper
 * @date :2020/4/16 18:10
 */
public 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 snow_app "
            + "where app_id=#{appId} and app_secret=#{appSecret}  ")
    App findApp(App app);

    @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 snow_app "
            + "where app_id=#{appId} and app_secret=#{appSecret}  ")
    App findAppId(@Param("appId") String appId);

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

2.2.7 基礎類

package com.snow.base;

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

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.base
 * @Description : ResponseBase
 * @date :2020/4/16 17:58
 */
@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 + "]";
    }

}

package com.snow.base;

import com.snow.utils.Constants;
import org.springframework.stereotype.Component;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.base
 * @Description : BaseApiService
 * @date :2020/4/16 17:58
 */
@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);
    }

}

2.3 獲取 AccessToken 接口

package com.snow.controller;

import com.alibaba.fastjson.JSONObject;
import com.snow.base.BaseApiService;
import com.snow.base.ResponseBase;
import com.snow.entity.App;
import com.snow.mapper.AppMapper;
import com.snow.utils.BaseRedisService;
import com.snow.utils.TokenUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.controller
 * @Description : 獲取 AccessToken
 * @date :2020/4/16 17:54
 */
@RestController
@RequestMapping(value = "/auth")
public class AuthController extends BaseApiService {

    // redis有效期時間
    private long timeToken = 60 * 60 * 2;

    @Autowired
    private BaseRedisService baseRedisService;

    @Autowired
    private AppMapper appMapper;

    /**
     * 使用 appId + appSecret 生成 AccessToken
     *
     * @param app
     * @return
     */
    @RequestMapping("/getAccessToken")
    public ResponseBase getAccessToken(App app) {

        // 1 驗證傳入的 appId 與 appSecret 是否有效
        App appResult = appMapper.findApp(app);
        if (appResult == null) {
            return setResultError("沒有對應機構的認證信息");
        }
        int isFlag = appResult.getIsFlag();
        if (isFlag == 1) {
            return setResultError("該機構的權限未開放,請聯繫管理員");
        }
        // ### 獲取新的accessToken 之前刪除之前老的accessToken
        // 2 從redis中刪除之前的accessToken
        String accessToken = appResult.getAccessToken();
        if (!StringUtils.isEmpty(baseRedisService.getString(accessToken) + "")) {
            baseRedisService.delKey(accessToken);
        }
        // 3 生成的新的accessToken
        String newAccessToken = newAccessToken(appResult.getAppId());
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("accessToken", newAccessToken);
        return setResultSuccessData(jsonObject);
    }

    // 生成新的 Token
    private String newAccessToken(String appId) {
        // 使用appid+appsecret 生成對應的AccessToken 保存兩個小時
        String accessToken = TokenUtils.getAccessToken();
        // 保證在同一個事物redis 事物中
        // 生成最新的token key爲accessToken value 爲 appid
        baseRedisService.setString(accessToken, appId, timeToken);
        // 表中保存當前accessToken
        appMapper.updateAccessToken(accessToken, appId);
        return accessToken;
    }
}


2.4 API開發接口

package com.snow.controller;

import com.snow.base.BaseApiService;
import com.snow.base.ResponseBase;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.controller
 * @Description : API開發接口
 * @date :2020/4/16 19:51
 */
@RestController
@RequestMapping("/openApi")
public class MemberController extends BaseApiService {

    @RequestMapping("/getUser")
    public ResponseBase getUser() {

        return setResultSuccess("獲取會員信息接口");
    }
}

2.5 驗證 AccessToken 的攔截器

package com.snow.handler;

import com.alibaba.fastjson.JSONObject;
import com.snow.base.BaseApiService;
import com.snow.utils.BaseRedisService;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.handler
 * @Description : 驗證 AccessToken 的攔截器
 * @date :2020/4/16 20:38
 */
@Component
public class AccessTokenInterceptor extends BaseApiService implements HandlerInterceptor {

    @Autowired
    private BaseRedisService baseRedisService;

    /**
     * 進入controller層之前攔截請求
     *
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o
     * @return
     * @throws Exception
     */
    @Override
    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;

    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o,
                           ModelAndView modelAndView) throws Exception {
        System.out.println("--------------處理請求完成後視圖渲染之前的處理操作---------------");
    }

    @Override
    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)));
    }

}

2.6 配置攔截器生效

package com.snow.handler;

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;

/**
 * @author :yang-windows
 * @Title : springboot
 * @Package :com.snow.handler
 * @Description : 配置攔截器生效
 * @date :2020/4/16 20:39
 */
@Configuration
public class WebAppConfig {

    @Autowired
    private AccessTokenInterceptor accessTokenInterceptor;

    @Bean
    public WebMvcConfigurer WebMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                // 攔截 /openApi/ 下面的所有請求
                registry.addInterceptor(accessTokenInterceptor).addPathPatterns("/openApi/*");
            };
        };
    }

}

2.7 測試

獲取 accessToken

http://127.0.0.1:8080/auth/getAccessToken?appId=snow&appSecret=snow123

在這裏插入圖片描述

請求開發的接口:

http://127.0.0.1:8080/openApi/getUser?accessToken=c80a1697b216497c86867e681835622b

在這裏插入圖片描述

如果連續兩次獲取 accessToken,而請求開發接口時參數爲第一次的 accessToken,那麼也會請求失敗:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章