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,那么也会请求失败:

在这里插入图片描述

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