• 以一个小需求开始:

现在A公司与B公司进行合作,B公司需要调用A公司开放的外网接口获取数据,如何保证外网开放接口的安全性?

  • 如何保证外网开放接口的安全性
    (1)搭建API网关接口,控制接口访问权限
    (2)开放平台设计,oAuth2.0协议。第三方联合登录的时候会使用,是用来针对API是否能够让外网访问到的权限,做安全认证
    (3)采用Https加密传输协议,使用Nginx配置Https证书
    (4)对API接口做签名认证(加密),防止抓包分析修改数据
    (5)基于令牌方式实现API接口调用。基于accessToken的方式实现API的接口调用

利用accessToken实现接口调用

  • 一般情况下appId和appSecret是一对的,一起出现的
  • appleId和appSecret是用来区分不同的机构
  • 我们用appleId和appSecret生成accessToken,如果我们生成了新的accessToken会将之前的删除掉。
  • 数据库表设计
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;

(1)app_id:表示应用的id

(2)app_name:表示机构名字

(3)app_secret:表示应用密钥(可以更改)

(4)is_flag:表示是否可用(是否对某一个机构开放,当前接口是否可以被当前对应的机构使用)

(5)access_token:记录当前的token值(用于确定删除redis中的哪一个token)

acesse接口PVID_API

  • 表的目的:
    (1)假如有A,B两个服务器,B服务器开放外网接口(还有别的服务器需要访问)
    (2)我们有app_id和app_secret两个字段,B服务器为需要访问B服务器的服务器提供对应的这两个字段的值,这两个字段确定了一个第三方合作的机构,app_id是区分不同机构的,app_secret是在传输中提供加密功能的,就是密钥,app_id永远不能改变,但是app_secret是可以改变的(避免别人模拟请求,模拟得到token,加强了安全性),根据这两个值生成对应的access_token
    (3)这样只要我们允许谁访问,就给其一个app_id和app_secret,生成token,然后访问该API接口的时候,判断token是否与redis中一致,一致的话,允许访问。
  • 生成access_token的开发步骤:
    (1)得到用户的app_id和app_secret,验证是否可用(查询数据库中的is_flag是否是可用的,0可用,1不可用)
    (2)使用对应机构的app_id和app_secret生成对应的access_token,将其存放在redis中,key是access_token,value是app_id并设置其过期时间。
    (3)根据app_id等信息,查询出数据库中的access_token,删除该token对应在redis中的值。(数据库中的access_token是用来确定删除redis中的哪一个值的)这里一定要进行删除redis中对应token,因为如果不删除,即使我们更新了redis中的值,旧的token对应的也是可以成功拿到app_id的。
    (4)将生成的access_token存放在数据库中,app_id+app_secret保证了唯一生成对应的access_token
    (5)返回最新的access_token给用户
  • 利用access_token调用API接口的开发步骤:
    (1)获取对应的access_token
    (2)使用access_token查询redis中对应的value(value存放的是app_id),如果没有获取到对应的app_id表示是一个无效token,进行返回错误信息
    (3)如果能够从redis中获取到对应的app_id,使用app_id查询数据库
    (4)判断数据库中该app_id对应的记录的is_flag是否可用(0可用),否则也是不能够调用。
代码实现:

(1)代码架构

acesse接口PVID_spring_02


(2)pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xiyou</groupId>
    <artifactId>accessToken</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>
    <dependencies>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- mysql 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- SpringBoot 对lombok 支持 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- 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>
        <!-- SpringBoot 外部tomcat支持 -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

        <!-- springboot-log4j -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>1.3.8.RELEASE</version>
        </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>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
        <dependency>
            <groupId>taglibs</groupId>
            <artifactId>standard</artifactId>
            <version>1.1.2</version>
        </dependency>
    </dependencies>

</project>

(3)配置文件yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3307/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: 127.0.0.1
    port: 6379
    password:
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0
    timeout: 10000

domain:
  name: www.xiyou.com

server:
  port: 8889

(4)主启动类

package com.xiyou;

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

/**
 * 启动类
 * @author
 */
@MapperScan("com.xiyou.mapper")
@SpringBootApplication
public class TokenApplication {
    public static void main(String[] args) {
        SpringApplication.run(TokenApplication.class, args);
    }
}

(5)配置类(配置拦截器)

package com.xiyou.conf;

import com.xiyou.handler.AccessTokenInterceptor;
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;

/**
 * 拦截器的配置信息
 * 我们自己作的token判断都是在拦截器中的
 */
@Configuration
public class WebAppConfig {

    @Autowired
    private AccessTokenInterceptor accessTokenInterceptor;

    /**
     * 实现自己的拦截器的配置,拦截指定url
     * @return
     */
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(accessTokenInterceptor).addPathPatterns("/openApi/*");
            }
        };
    }
}

(6)controller

  • AuthController:用来获取token,访问别的接口前先调用其获取token
package com.xiyou.controller;

import com.xiyou.entity.AppEntity;
import com.xiyou.mapper.AppMapper;
import com.xiyou.utils.BaseRedisService;
import com.xiyou.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用来生成token
 * @author
 */
@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private BaseRedisService baseRedisService;

    /**
     * 数据库交互
     */
    @Autowired
    private AppMapper appMapper;

    /**
     * 设置过期时间长短,这里的redis的过期时间被设置成秒了
     */
    private long timeout = 60 * 2 * 60;

    /**
     * 使用app_id和app_secret生成access_token
     * @param appEntity
     * @return
     */
    @GetMapping("/getAccessToken")
    public String getAccessToken(AppEntity appEntity) {

        // 通过数据库查询,当前的对象
        AppEntity app = appMapper.findApp(appEntity);
        // 判断是否为空,为空则不用生成token,直接不允许访问
        if (app == null) {
            return "没有对应的认证信息";
        }

        // 判断其flag是否是1 为1表示拒绝访问,0表示可以访问
        int isFlag = app.getIsFlag();
        if (isFlag == 1) {
            return "没有权限访问该接口";
        }

        // 执行到这里表示允许访问,需要生成token
        // 先删除老的token,如果这不是第一次生成,需要删除旧的token,否则用户还是可以根据旧的token访问
        // 得到存放在数据库中的token(上一次的token,更新前的token,此时还没有将其更新成最新的token)
        String accessToken = app.getAccessToken();
        // 以数据库中的token做key,删除redis中的数据,如果是第一次存,则删除也什么操作没做,如果不是第一次,会删除之前的token
        baseRedisService.delKey(accessToken);
        // 生成新的token
        String newAccessToken = newAccessToken(app.getAppId());
        return "生成的最新的token是: " + newAccessToken;
    }


    /**
     * 生成新的token,该token会放到redis中,token是key value是app_id
     * @param appId
     * @return
     */
    private String newAccessToken(String appId) {
        // 生成token
        String accessToken = TokenUtils.getAccessToken();
        // 存放到数据库中
        baseRedisService.setString(accessToken, appId, timeout);
        // 更新数据库中的token
        appMapper.updateAccessToken(accessToken, appId);
        return accessToken;
    }
}
  • MemberController:定义公共的API方法
package com.xiyou.controller;

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

/**
 * 因为我们设置了拦截器,拦截的请求必须是openAPI开头
 */
@RequestMapping("/openApi")
@RestController
public class MemberController {

    @GetMapping("/getUser")
    public String getUser() {
        return "获取了会员的接口";
    }

}

(7)mapper

package com.xiyou.mapper;

import com.xiyou.entity.AppEntity;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/**
 * 数据库之间的交互
 */
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 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}")
    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);
}

(8)工具类

  • 封装redis的常用方法
package com.xiyou.utils;

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

import java.util.concurrent.TimeUnit;

/**
 * 自定义实现常用的redis(增删改功能)
 */
@Component
public class BaseRedisService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 向redis中赋值
     * @param key
     * @param data
     * @param timeout
     */
    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) {
            // 设置过期时间是s数
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    /**
     * 从redis中得到指定key的值
     * @param key
     * @return
     */
    public Object getString(String key){
        return stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 删除指定的key
     * @param key
     */
    public void delKey(String key) {
        stringRedisTemplate.delete(key);
    }
}
  • 生成token
package com.xiyou.utils;

import java.util.UUID;

/**
 * Token的相关工具类,生成token等
 * @author
 */
public class TokenUtils {

    /**
     * 生成Token
     * @return
     */
    public static String getAccessToken(){
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
}

(9)实体类

package com.xiyou.entity;

import lombok.Data;

/**
 * 实体类
 */
@Data
public class AppEntity {
    private long id;
    private String appId;
    private String appName;
    private String appSecret;
    private String accessToken;
    private int isFlag;
}

(10)拦截器:该拦截器主要的作用是根据当前请求中的token能否在redis中获取,如果可以且flag为0则允许调用

package com.xiyou.handler;

import com.xiyou.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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * 拦截器,该拦截器的作用就是实现调用方法之前判断其token是否相同
 */
@Component
public class AccessTokenInterceptor implements HandlerInterceptor{

    /**
     * redis的常用操作
     */
    @Autowired
    private BaseRedisService baseRedisService;

    /**
     * 拦截请求之前的方法,执行方法之前需要判断其是否有权限调用该接口(就是判断其是否有token,token值是否正确,是否在redis中),进行token的判断
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("##### 计入请求地址拦截");
        // 得到参数中的token
        String accessToken = request.getParameter("accessToken");

        if (StringUtils.isEmpty(accessToken)) {
            System.out.println("##### 当前的token为空");
            resultError("当前的token为空", response);
            return false;
        }

        // 从redis中取值,redis中的key是token值
        String appId = (String)baseRedisService.getString(accessToken);
        if (StringUtils.isEmpty(appId)) {
            // 如果appid是空,表示当前token值不正确
            System.out.println("#### 当前的token值不正确");
            resultError("当前的token值不正确", response);
            return false;
        }

        // 执行到这里,表示一切正常
        return true;
    }

    /**
     * 自己实现的方法,输入到浏览器中
     * @param errorMsg
     * @param response
     * @throws Exception
     */
    private void resultError(String errorMsg, HttpServletResponse response) throws Exception{
        PrintWriter writer = response.getWriter();
        writer.write(errorMsg);
    }
}