- 以一个小需求开始:
现在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)
- 表的目的:
(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)代码架构
(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);
}
}