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
,那麼也會請求失敗: