欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

3 -【 API 开放平台安全设计 】- 1 基于 AccessToken 方式实现 API 设计

程序员文章站 2022-03-26 10:36:53
...

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

3 -【 API 开放平台安全设计 】- 1 基于 AccessToken 方式实现 API 设计

请求开发的接口:

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

3 -【 API 开放平台安全设计 】- 1 基于 AccessToken 方式实现 API 设计

如果连续两次获取 accessToken,而请求开发接口时参数为第一次的 accessToken,那么也会请求失败:

3 -【 API 开放平台安全设计 】- 1 基于 AccessToken 方式实现 API 设计