开放中心单独作为一个微服务工程,对三方调用者提供获取接口调用凭证,校验access_token,接口验签,接口限流,开放接口等功能。而开放平台无非就是两大核心功能,一是提供三方调用者获取access_token。二是提供相关开放接口(开放接口的安全设计业界一般需要经过三个步骤,1.校验access_token的有效性 2.接口验签 3.接口限流)接下来我将从0到1搭建开放平台。
1.数据库表创建
CREATE TABLE api_rate_limit_config (
id VARCHAR(36) NOT NULL COMMENT 'id',
client_id VARCHAR(255) NOT NULL COMMENT '客户端ID',
api_path VARCHAR(255) NOT NULL COMMENT '接口路径',
daily_request_limit INT COMMENT '每日请求限制次数,默认为1000次',
config_status CHAR(1) NOT NULL DEFAULT 'Y' COMMENT 'Y 生效 N 失效',
created_at VARCHAR(19) COMMENT '创建时间',
updated_at VARCHAR(19) COMMENT '更新时间',
PRIMARY KEY (`id`)
);
-- 初始化测试开放接口的限制速率为1天2次。
insert into `rate_limit_config` (`id`, `client_id`, `api_path`, `daily_request_limit`, `config_status`, `created_at`, `updated_at`) values('4286e4a4-b629-4f4f-ad9a-0d3cd8c5cb10','70c203bc','/open/test','2','Y','2024-02-04 00:00:00','2024-02-04 00:00:00');
重要参数说明:
1.client_id:应用id(唯一)。
2.api_path:接口路径。
3.daily_request_limit:接口限制速率,默认一天1000调用。
4.config_status:配置是否有效。
2.引入pom文件
<dependencyManagement>
<dependencies>
<!-- SpringCloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringBoot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- oauth2支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>${spring-cloud-starter-oauth2.version}</version>
</dependency>
<!-- redis支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${common-pool.version}</version>
</dependency>
<!-- lombok支持 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- mp支持 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool-all.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
</dependencies>
3.获取access_token(调用认证中心微服务)
package com.open.admin.controller;
import com.open.admin.vo.TokenReqVo;
import com.open.admin.vo.TokenResVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @desc token相关
* @author mj
*/
@RestController
public class TokenController {
private static final Logger log = LoggerFactory.getLogger(TokenController.class);
private static final String AUTH_URL="http://localhost:8112/oauth/token";
// 获取access_token
@PostMapping("/token/get")
public TokenResVo getAccessToken(@RequestBody TokenReqVo tokenReqVo){
TokenResVo tokenResVo=new TokenResVo();
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setGrantType(tokenReqVo.getGrantType());
resourceDetails.setAccessTokenUri(AUTH_URL);
resourceDetails.setClientId(tokenReqVo.getClientId());
resourceDetails.setClientSecret(tokenReqVo.getClientSecret());
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails);
restTemplate.setAccessTokenProvider(new ClientCredentialsAccessTokenProvider());
OAuth2AccessToken authInfo = restTemplate.getAccessToken();
tokenResVo.setAccessToken(authInfo.getValue());
tokenResVo.setExpiresIn(authInfo.getExpiresIn());
return tokenResVo;
}
}
4.校验access_token(开放接口安全校验第一关)
package com.open.admin.filter;
import cn.hutool.extra.spring.SpringUtil;
import com.open.admin.exception.CommonExceptionHandle;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;
/**
* @desc 校验token
* @author mj
*/
@Component
public class CheckTokenFilter implements Filter {
private static final String ACCESS_TOKEN="access_token";
private static final String CLIENT_ID="clientId";
private TokenStore tokenStore= SpringUtil.getBean(TokenStore.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
String accessToken = String.valueOf(request.getParameter(ACCESS_TOKEN));
OAuth2AccessToken accessTokenVal = this.tokenStore.readAccessToken(accessToken);
if (accessTokenVal == null) {
CommonExceptionHandle.printRes(res,"无效token"+accessToken);
return ;
} else if (accessTokenVal.isExpired()) {
this.tokenStore.removeAccessToken(accessTokenVal);
CommonExceptionHandle.printRes(res,"token"+accessToken+"过期");
return ;
} else {
OAuth2Authentication result = this.tokenStore.readAuthentication(accessTokenVal);
if (result == null) {
CommonExceptionHandle.printRes(res,"无效token"+accessToken);
return ;
} else {
String clientId =result.getOAuth2Request().getClientId();
req.setAttribute(CLIENT_ID,clientId);
}
}
chain.doFilter(request, response);
}
}
5.接口签名验证(开放接口安全校验第二关)
package com.open.admin.filter;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson.JSON;
import com.open.admin.exception.CommonExceptionHandle;
import com.open.admin.wrapper.RequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
/**
* @desc 验签
* @author mj
*/
@Component
public class CheckSignFilter implements Filter {
private static final String SIGN="sign";
private static final String CLIENT_ID="clientId";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
String clientId = String.valueOf(req.getAttribute(CLIENT_ID));
String sign = String.valueOf(request.getParameter(SIGN));
RequestWrapper requestWrapper=new RequestWrapper(req);
Map map = JSON.parseObject(requestWrapper.getData(), Map.class);
if(!this.createSign(map, clientId).equals(sign)){
CommonExceptionHandle.printRes(res,"签名"+sign+"不通过");
return ;
}
chain.doFilter(requestWrapper, response);
}
//构建签名
private String createSign(Map<String, ?> paramsMap,String clientId) {
if (ObjectUtil.isEmpty(paramsMap)) {
return "";
}
Set<String> keySet = paramsMap.keySet();
List<String> paramNames = new ArrayList<String>(keySet);
Collections.sort(paramNames);
StringBuilder sb = new StringBuilder();
for (String paramName : paramNames) {
sb.append(paramName).append("=").append(paramsMap.get(paramName)).append("&");
}
String source = sb.toString() + clientId;
return DigestUtil.md5Hex(source, "utf-8");
}
}
6.接口限流(开放接口安全校验第三关)
package com.open.admin.filter;
import cn.hutool.extra.spring.SpringUtil;
import com.open.admin.exception.CommonExceptionHandle;
import com.open.admin.service.RateLimitCfgService;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* @desc 接口限流
* @author mj
*/
@Component
public class RateLimitFilter implements Filter {
private static final String CLIENT_ID="clientId";
private static final String Y="Y";
private RateLimitCfgService rateLimitCfgService= SpringUtil.getBean(RateLimitCfgService.class);
private StringRedisTemplate redisTemplate= SpringUtil.getBean(StringRedisTemplate.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
String clientId = String.valueOf(req.getAttribute(CLIENT_ID));
//接口
String requestURI = req.getRequestURI();
int interFaceLimitCount = rateLimitCfgService.getInterFaceRateLimit(clientId,requestURI);
String msg = rateLimit(clientId, requestURI, String.valueOf(interFaceLimitCount));
if(!StringUtils.isEmpty(msg)){
CommonExceptionHandle.printRes(res,msg);
return ;
}
chain.doFilter(request, response);
}
//redis队列实现限流
public String rateLimit(String clientId,String requestURI,String interFaceLimitCount) {
String rtnMsg = "";
String queueKey = clientId + requestURI + "QUEUE_KEY";
String lock = clientId + requestURI + "LOCK";
String lockKey = redisTemplate.opsForValue().get(lock);
if(!StringUtils.isEmpty(lockKey)){
rtnMsg = "接口"+requestURI+"超过调用限制,锁定24小时。";
}else{
long currentTime = System.currentTimeMillis();
if (redisTemplate.opsForList().size(queueKey) < Long.parseLong(interFaceLimitCount)) {
redisTemplate.opsForList().rightPush(queueKey, String.valueOf(currentTime));
} else {
// 当队列数大于限制速率 取出队列中最早时间
String oldTime = (String)redisTemplate.opsForList().index(queueKey, 0);
long oldTimes = Long.parseLong(oldTime);
if (currentTime - oldTimes <= (24*3600 * 1000)) {
// 清空队列
redisTemplate.delete(queueKey);
//设置锁的时间
redisTemplate.opsForValue().set(lock,Y,24*60*60, TimeUnit.SECONDS);
rtnMsg = "接口"+requestURI+"超过调用限制,锁定24小时。";
} else {
// 删除队列中最早时间
redisTemplate.opsForList().leftPop(queueKey);
redisTemplate.opsForList().rightPush(queueKey, String.valueOf(currentTime));
}
}
}
return rtnMsg;
}
}
7.待测试的开放接口
package com.open.admin.controller;
import com.open.admin.vo.OpenTestReqVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @desc 开放接口controller
* @author mj
*/
@RestController
public class OpenController {
private static final Logger log = LoggerFactory.getLogger(OpenController.class);
// 测试接口
@PostMapping("/open/test")
public Boolean openTest(@RequestBody OpenTestReqVo openTestReqVo){
return Boolean.TRUE;
}
}
8.测试-(获取access_token)
9.测试-(校验access_token)
1、当传入的access_token是错误或者是过期的。
2、当传入的access_token是正确的。
10.测试-(接口签名验证)
1、当传入的签名sign是错误的。
2、当传入的签名sign是正确的。
11.测试-(接口限流)
1、当调用测试开放接口的速率超过1天2次(数据库初始化限流配置)的时候。
2、当调用测试开放接口的速率在配置的阈值范围内的时候。
如需完整代码,请小伙伴们点赞加关注,私信我领取。创作不易,您的支持是我今后更新的最大动力。感谢!