簡介
OpenAPI
Open API 即開放 API,也稱開放平台。 所謂的開放 API(OpenAPI)是服務型網站常見的一種應用,網站的服務商將自己的網站服務封裝成一系列
API(Application Programming Interface,應用編程接口)開放出去,供第三方開發者使用,這種行為就叫做開放網站的 API,所開放的 API 就被稱作 OpenAPI(開放 API )。
RESTful API
Representational State Transfer,翻譯是”表現層狀態轉化”。可以總結為一句話:REST 是所有 Web 應用都應該遵守的架構設計指導原則。
面向資源是 REST 最明顯的特征,對於同一個資源的一組不同的操作。資源是服務器上一個可命名的抽象概念,資源是以名詞為核心來組織的,首先關注的是名詞。REST 要求,必須通過統一的接口來對資源執行各種操作。對於每個資源只能執行一組有限的操作。
什么是 RESTful API?
符合 REST 設計標准的 API,即 RESTful API。REST 架構設計,遵循的各項標准和准則,就是 HTTP 協議的表現,換句話說,HTTP 協議就是屬於 REST 架構的設計模式。比如,無狀態,請求-響應。。。
簡單實踐
那如何構建咱們自己的Open API,這里做了簡單的代碼示例,包括基礎的權限驗證、限流控制,方便筆者自己構建其他應用服務時的調用。
- 部署環境(阿里雲ECS服務器)
- 操作系統:Centos7.1
- 容器管理:Docker version 1.13.1
- 微服務注冊中心鏡像:webapp/eureka-server
- 微服務配置中心鏡像:webapp/config-server
- 微服務API應用鏡像:webapp/open-api
- 微服務API網關鏡像:webapp/api-gateway
- 源碼:https://github.com/lizzie2008/spring-cloud-app.git
API工程
創建工程
- 創建Gateway工程(open-api),引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
暴露API
open-api工程很簡單,實現業務邏輯,對外暴露接口即可,這里我們簡單示例,新建一個測試Controller,返回一行文本。
@RestController
@RequestMapping("/v1")
public class TestController {
@GetMapping("/info")
public String info(){
return "Hello World!";
}
}
啟動服務,可以通過 http://localhost:8081/v1/info 正常訪問。
Gateway工程
創建工程
創建Gateway工程(api-gateway),導入相關依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
訪問權限控制
- 數據庫建權限表
CREATE TABLE `access_info` (
`access_key` varchar(32) NOT NULL COMMENT '訪問碼',
`access_desc` varchar(32) NOT NULL COMMENT '訪問說明',
`visit_module` varchar(32) NOT NULL COMMENT '訪問模塊',
`access_status` tinyint(3) NOT NULL DEFAULT '0' COMMENT '訪問狀態, 0:不允許訪問 1:允許訪問',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`access_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 從數據庫獲取權限
@Data
@Entity
public class AccessInfo {
/**
* 訪問碼.
*/
@Id
private String accessKey;
/**
* 訪問說明.
*/
private String accessDesc;
/**
* 訪問模塊.
*/
private String visitModule;
/**
* 訪問狀態, 0:不允許訪問 1:允許訪問
*/
private AccessStatus accessStatus;
/**
* 創建時間.
*/
private Date createTime;
/**
* 更新時間.
*/
private Date updateTime;
}
@Repository
public interface AccessInfoRepository extends JpaRepository<AccessInfo, String> {
}
@Service
public class AccessInfoService {
private final AccessInfoRepository accessInfoRepository;
public AccessInfoService(AccessInfoRepository accessInfoRepository) {
this.accessInfoRepository = accessInfoRepository;
}
/**
* 獲取所有訪問權限信息
*
* @return
*/
public List<AccessInfo> findAll() {
return accessInfoRepository.findAll();
}
}
- 新建
AccessFilter,繼承ZuulFilter,來實現權限驗證
@Component
public class AccessFilter extends ZuulFilter {
private final AccessInfoService accessInfoService;
public AccessFilter(AccessInfoService accessInfoService) {
this.accessInfoService = accessInfoService;
}
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
if (!isAuthorized(request)) {
HttpStatus httpStatus = HttpStatus.UNAUTHORIZED;
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(httpStatus.value());
}
return null;
}
/**
* 判斷請求是否有權限
*
* @param request
* @return
*/
private boolean isAuthorized(HttpServletRequest request) {
// 檢查請求參數是否包含 access_key
String access_key = request.getParameter("access_key");
if (!StringUtils.isEmpty(access_key)) {
// 檢查 access_key 是否匹配
List<AccessInfo> accessInfos = accessInfoService.findAll();
Optional<AccessInfo> accessInfo = accessInfos.stream()
.filter(s -> access_key.equals(s.getAccessKey())).findAny();
if (accessInfo.isPresent()) {
return true;
}
return false;
}
return false;
}
}
- 啟動網關服務,訪問 http://localhost:8080/open-api/v1/info
- 如果請求參數不帶
access_key,網關服務會直接返回 401 未授權的錯誤; - 如果請求參數帶
access_key,但是與我們數據庫的安全驗證不匹配,網關服務也會直接返回 401 錯誤;
- 如果請求參數不帶

- 請求
access_key,也通過后台數據庫驗證,則調用成功

權限緩存
以上已經實現了基本權限的驗證,但是每次api的請求,都會進行數據庫的校驗。
2020-01-13 16:44:43.591 INFO 25028 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration
Hibernate: select accessinfo0_.access_key as access_k1_0_, accessinfo0_.access_desc as access_d2_0_, accessinfo0_.access_status as access_s3_0_, accessinfo0_.create_time as create_t4_0_, accessinfo0_.update_time as update_t5_0_, accessinfo0_.visit_module as visit_mo6_0_ from access_info accessinfo0_
Hibernate: select accessinfo0_.access_key as access_k1_0_, accessinfo0_.access_desc as access_d2_0_, accessinfo0_.access_status as access_s3_0_, accessinfo0_.create_time as create_t4_0_, accessinfo0_.update_time as update_t5_0_, accessinfo0_.visit_module as visit_mo6_0_ from access_info accessinfo0_
Hibernate: select accessinfo0_.access_key as access_k1_0_, accessinfo0_.access_desc as access_d2_0_, accessinfo0_.access_status as access_s3_0_, accessinfo0_.create_time as create_t4_0_, accessinfo0_.update_time as update_t5_0_, accessinfo0_.visit_module as visit_mo6_0_ from access_info accessinfo0_
實際生產中肯定不能這么操作,對數據庫的壓力太大,所以,我們要對權限驗證的數據進行緩存。
- 首先,在啟動類上增加
@EnableCaching注解
@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
@EnableCaching
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
AccessInfo需要實現Serializable接口,方便序列化后保存在redis中。
@Data
@Entity
public class AccessInfo implements Serializable {
//...
}
- 獲取所有訪問權限信息的方法上增加緩存處理
@Cacheable(value = "api-gateway:accessInfo")
public List<AccessInfo> findAll() {
return accessInfoRepository.findAll();
}
- 重新啟動服務后,多調用幾次api接口,發現第一次加載時會調用一次數據庫,后面都是取緩存中的權限信息,不再查詢數據庫。
限流控制
RateLimiter是guava提供的基於令牌桶算法的實現類,可以非常簡單的完成限流特技,並且根據系統的實際情況來調整生成token的速率。
- 新建
RateLimitFilter繼承ZuulFilter,定義一個RateLimiter,這里為了測試方便,每秒設置最多2個請求
/**
* 限流
*/
@Component
public class RateLimitFilter extends ZuulFilter {
//每秒產生N個令牌
private static final RateLimiter rateLimiter = RateLimiter.create(2);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return SERVLET_DETECTION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
if (!rateLimiter.tryAcquire()) {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS;
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(httpStatus.value());
}
return null;
}
}
- 重啟服務后,1s內如果多次訪問接口,會提示 429 Too Many Requests錯誤,這樣限流的功能就完成了

部署環境
微服務注冊
將打包的jar文件生成docker鏡像,然后部署在個人服務器上,之前筆者已經部署過服務注冊中心(eureka-server)和統一配置中心(config-server),所以把兩個新應用注冊並部署即可。
這里是微服務部署,將服務注冊到服務中心,並從統一配置中心獲取配置屬性,后面可以通過實例名稱來進行訪問。
- 配置open-api工程
eureka:
client:
serviceUrl:
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/ # 指定服務注冊地址
spring:
application:
name: open-api # 應用名稱
server:
port: 8081
- 配置api-gateway工程
eureka:
client:
serviceUrl:
defaultZone: http://eureka-server:8761/eureka/ #指定服務注冊地址
spring:
application:
name: api-gateway #應用名稱
cloud:
config:
discovery:
enabled: true
service-id: config-server

啟動服務
依次啟動eureka-server、config-server、open-api、api-gateway服務,這樣我們就可以通過訪問域名地址來訪問自己的API了。這里尤其注意open-api啟動后再啟動api-gateway服務,不然api-gateway服務在eureka-server上無法找到open-api服務,所以不會配置默認的路由規則,會導致服務不可用。

