有如下的场景:你们公司业务越做越大,有很多的机构或者客户想跟你们公司合作,这时候,你们公司就需要提供对外的开放接口,以便合作伙伴调用。这时候,如何保证 API 接口调用的安全性?

一般来说,解决办法有以下几种:

1、API 请求使用加签名方式,防止篡改数据。

2、使用Https 协议加密传输。

3、搭建OAuth2.0认证授权平台。

4、使用令牌方式 accessToken。

5、搭建网关实现黑名单和白名单。

 

OK,我们来学习使用令牌方式搭建搭建API开放平台。

解决思路是:客户先申请一个有效的 AppID,确保是激活状态,然后调用获取 token 的接口(token 有效期设置 2 小时,需要保存到客户的本地缓存,然后开定时任务刷新),获取到有效的 token 后,接下来的 API 请求都需要携带 token。新的 token 被刷新后,旧的 token 就不能再使用了。

1、下载脚手架代码:https://pan.baidu.com/s/1zRZLxWZ-1P_6iMXmRv38WA  提取码:755u

2、设计表,增加测试数据:

drop table if exists t_app;
CREATE TABLE `t_app` (
	`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
	`app_name` varchar(255) DEFAULT NULL COMMENT '机构名称',
	`app_id` varchar(64) DEFAULT NULL COMMENT 'AppID,唯一(不可更改)',
	`app_secret` varchar(255) DEFAULT NULL COMMENT '密码(可更改)',
	`active_flag` char(1) DEFAULT NULL COMMENT '状态:1=激活,0=停用',
	`access_token` varchar(255) DEFAULT NULL COMMENT '上一次的 accessToken',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='机构管理表';

INSERT INTO `t_app` VALUES (1, '流放深圳', 'good123', '123', '1', 'c4d8dcb4d8b54bdd857094e35712ebb7');
INSERT INTO `t_app` VALUES (2, '乐视网', 'ls456', '456', '0', 'ras8dcb4d8b54bdd857094e35712eecw');

说明:合作结构一旦申请成功(一般我们提供一个管理平台给他们的),我们就分配一个唯一的 AppID 给他们,这个 AppID 不可更改。他们通过 AppID 和 appSecret 来获取 accessToken(一般是2小时内有效),在调用外网开放接口的时候,必须传递有效的access_token。【上一次的 accessToken】,主要是用来根据 token 删除旧的 token。

3、controller 类:

package com.study.controller;

import com.alibaba.fastjson.JSONObject;
import com.study.entity.AppEntity;
import com.study.service.AppService;
import com.study.util.BaseResponse;
import com.study.util.CodeUtil;
import com.study.util.TokenUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-01 下午 11:19
 */
@RestController
public class AppController {

    @Autowired
    private AppService appService;

    @Autowired
    private TokenUtil tokenUtil;

    //获取token
    @GetMapping(value = "/getToken")
    public BaseResponse getToken(AppEntity appEntity){
        BaseResponse response = new BaseResponse();
        AppEntity dbEntity = appService.findByAppId(appEntity.getAppId());
        if(null == dbEntity){
            response.setCode(CodeUtil.appInvalid);
            response.setMessage("无此账号信息");
            return response;
        }

        if(dbEntity.getActiveFlag().equals("0")){
            response.setCode(CodeUtil.appNoActive);
            response.setMessage("账号未激活");
            return response;
        }

        if(!appEntity.getAppSecret().equals(dbEntity.getAppSecret())){
            response.setCode(CodeUtil.appInfoError);
            response.setMessage("账号信息错误");
            return response;
        }

        //生成 token,并存入 Redis
        String token = tokenUtil.getToken(dbEntity.getAppId());
        //获取上一次的 token,并删除
        tokenUtil.deleteKey(dbEntity.getAccessToken());
        //更新数据库的 token
        dbEntity.setAccessToken(token);
        appService.updateToken(dbEntity);

        //返回 json 格式给调用者
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("token",token);
        response.setCode(CodeUtil.success);
        response.setMessage("success");
        response.setData(jsonObject);
        return response;
    }

    //根据 token 获取信息
    @GetMapping(value = "/getInfo")
    public BaseResponse getInfo(String token){
        BaseResponse response = new BaseResponse();
        if(StringUtils.isBlank(token)){
            response.setCode(CodeUtil.accessTokenInvalid);
            response.setMessage("token无效");
            return response;
        }

        //根据 token 查询 Redis
        String appId = tokenUtil.findToken(token);
        if(StringUtils.isNotBlank(appId)){
            AppEntity appEntity = appService.findByAppId(appId);
            if(null != appEntity){
                //只返回重要信息
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("appId",appEntity.getAppId());
                jsonObject.put("appName",appEntity.getAppName());
                jsonObject.put("activeFlag",appEntity.getActiveFlag().equals("1") ? "激活":"停用");

                response.setCode(CodeUtil.success);
                response.setMessage("success");
                response.setData(jsonObject);
                return response;
            }
        }
        response.setCode(CodeUtil.appInvalid);
        response.setMessage("无此账号");
        return response;
    }

}

4、MyRedisTemplate 类:

package com.study.util;

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

import java.util.concurrent.TimeUnit;

/**
 * @author biandan
 * @description 封装的 Redis 操作类
 * @signature 让天下没有难写的代码
 * @create 2021-06-02 上午 11:46
 */
@Component
public class MyRedisTemplate {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //设置 String 对象
    public Boolean setString(String key,Object data,Long timeout){
        if(data instanceof String){
            if(null != timeout){
                stringRedisTemplate.opsForValue().set(key,(String)data,timeout, TimeUnit.SECONDS);
            }else{
                stringRedisTemplate.opsForValue().set(key,(String)data);
            }
            return true;
        }else{
            return false;
        }
    }

    //获取 String 对象
    public Object getString(String key){
        return stringRedisTemplate.opsForValue().get(key);
    }

    //删除某个 key
    public void delKey(String key){
        stringRedisTemplate.delete(key);
    }

}

注意:使用 StringRedisTemplate 时,要设置 key 的有效期,需要4个参数:

stringRedisTemplate.opsForValue().set(key,(String)data,timeout, TimeUnit.SECONDS);

否则报错,数据乱码,\u0000

API安全网关系统架构_拦截器

 

测试:

1、获取 token,浏览器地址输入:http://127.0.0.1/getToken?appId=good123&appSecret=123

API安全网关系统架构_spring_02

2、根据 token 获取信息:http://127.0.0.1/getInfo?token=3f126cc35a984070a4f6b8e4e41f1eda

API安全网关系统架构_json_03

3、输入错误的 token:

API安全网关系统架构_API安全网关系统架构_04

OK,accessToken 的核心思想如上。接下来我们把 token 放在拦截器中进行判断,不然每次请求接口都需要判断,导致代码冗余。

首先,增加拦截器:

package com.study.interceptor;

import com.alibaba.fastjson.JSON;
import com.study.util.BaseResponse;
import com.study.util.CodeUtil;
import com.study.util.TokenUtil;
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.PrintWriter;

/**
 * @author biandan
 * @description accessToken 拦截器
 * @signature 让天下没有难写的代码
 * @create 2021-06-03 下午 2:24
 */
@Component
public class AccessTokenInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenUtil tokenUtil;

    //该方法将在Controller处理之前进行调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        BaseResponse msg = new BaseResponse();
        String token = request.getParameter("token");
        if (StringUtils.isBlank(token)) {
            msg.setCode(CodeUtil.accessTokenInvalid);
            msg.setMessage("token无效");
            writeResult(msg, response);
            return false;
        }

        //根据 token 查询 Redis
        String appId = tokenUtil.findToken(token);
        if (StringUtils.isBlank(appId)) {
            msg.setCode(CodeUtil.accessTokenInvalid);
            msg.setMessage("token无效");
            writeResult(msg, response);
            return false;
        }else{
            request.setAttribute("appId",appId);
        }
        //执行正常逻辑
        return true;
    }

    //它的执行时间是在处理器进行处理之后,也就是在Controller的方法调用之后执行,但是它会在DispatcherServlet进行视图的渲染之前执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("***处理请求完成后视图渲染之前的处理操作***");
    }

    //该方法将在整个请求完成之后,也就是DispatcherServlet渲染了视图执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("******视图渲染之后的操作******");
    }

    //相应错误信息
    private void writeResult(BaseResponse msg, HttpServletResponse response) throws Exception{
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        try {
            writer.println(JSON.toJSONString(msg));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writer.close();
        }
    }
}

将拦截器注册到 Spring 容器:

package com.study.interceptor;

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

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-03 下午 3:11
 */
@Configuration
public class MyWebAppConfig implements WebMvcConfigurer{

    @Autowired
    private AccessTokenInterceptor accessTokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 可添加多个拦截器的对象
        registry.addInterceptor(accessTokenInterceptor).addPathPatterns("/**");
    }

}

controller 层增加方法;

//根据 token 获取信息(使用拦截器)
    @GetMapping(value = "/findInfo")
    public BaseResponse findInfo() {
        BaseResponse response = new BaseResponse();
        //从拦截器中获取上下文的请求
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String appId = (String) request.getAttribute("appId");
        AppEntity appEntity = appService.findByAppId(appId);
        if (null != appEntity) {
            //只返回重要信息
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("appId", appEntity.getAppId());
            jsonObject.put("appName", appEntity.getAppName());
            jsonObject.put("activeFlag", appEntity.getActiveFlag().equals("1") ? "激活" : "停用");

            response.setCode(CodeUtil.success);
            response.setMessage("success");
            response.setData(jsonObject);
            return response;
        }

        response.setCode(CodeUtil.appInvalid);
        response.setMessage("无此账号信息");
        return response;
    }

 

重启服务,浏览器地址输入:http://127.0.0.1/findInfo?token=3f126cc35a984070a4f6b8e4e41f1eda

{"code":"0","message":"success","data":{"appName":"流放深圳","appId":"good123","activeFlag":"激活"}}

控制台输出:

API安全网关系统架构_拦截器_05

说明进入到了我们的拦截器。

 

代码地址:https://pan.baidu.com/s/1Ci9Ta-PdwfvFz2bFKp5uGQ  提取码:yp5y