3 -【 API 开放平台安全设计 】- 1 基于 AccessToken 方式实现 API 设计
程序员文章站
2022-03-26 10:36:53
...
1 互联网开放平台设计
- 需求:现在A公司与B公司进行合作,B公司需要调用A公司开放的外网接口获取数据,如何保证外网开放接口的安全性。
- 常用解决办法:
2.1 使用加签名方式,防止篡改数据
2.2 使用Https
加密传输
2.3 搭建OAuth2.0
认证授权
2.4 使用令牌方式
2.5 搭建网关实现黑名单和白名单
2 使用令牌方式搭建搭建 API
开放平台
原理:为每个合作机构创建对应的 appid
、app_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
,那么也会请求失败:
上一篇: 图论(三) -- 最短路径基础
下一篇: 图的遍历--广搜bfs(邻接矩阵)