所有文章 > API产品 > 微博热搜API | 高性能热搜API及基于Ak、Sk鉴权方案设计思路与实现

微博热搜API | 高性能热搜API及基于Ak、Sk鉴权方案设计思路与实现

热搜API接口

上篇文章我们在《程序员盒子技术学院》社区分享了Java+jsonp实现微博热搜数据爬虫的方案,这篇文章分享下高性能api方案+开放api接口aksk鉴权方案,建议先学习上篇文章:

https://www.coderutil.com/star/exp/85

免费的微博热搜接口,为了保障接口性能,热搜数据每10分钟刷新一次,有需要的app可以接口,当前依然是免费接入的。

接口文档地址: https://www.coderutil.com/apiopen?tab=hotlist&pk=2000

两级缓存API性能保证

其实做前几天的文章中介绍过程序员盒子使用的多集缓存方案,没有看多的兄弟可以先了解下这篇文章(因为这里我们就不展开讲了,直接show me code):

性能优化|前端LocalStorage + Google Cache + Redis三级缓存性能优化

其实服务端接口这里还是用的Google cache+ redis实现两级缓存,这里并没有入库,完全依赖redis:

接口定义

/**
* 微博热搜
* @return
*/
@GetMapping("/api/resou/v1/weibo")
public APIResponseBean weiboHotSearch(@RequestParam(value = "size", defaultValue = "10") Integer size) {
List<WeiboHotSearchResultBaseVO> result = hotSearchService.getWeiboHotSearchListFromCache(size);
return APIResponseBeanUtil.success(result);
}

针对跨域请求问题,我们还提供了jsonp跨域接口

/**
* 百度热搜
* @return
*/
@RequestMapping(value = "/api/resou/v1/weibo.jsonp", produces = "text/script;charset=UTF-8", method= RequestMethod.GET)
public String weiboHotSearchJsonp(HttpServletRequest request, String callback) {
String sizeParam = request.getParameter("size");
int size = StringUtils.isBlank(sizeParam) ? DEFAULT_SIZE : Integer.valueOf(sizeParam);
List<WeiboHotSearchResultBaseVO> result = hotSearchService.getWeiboHotSearchListFromCache(size);
return callback + "(" + JsonUtil.toJsonString(result) + ")";
}

查询方法

 @Autowired
private RedisService redisService;
@Autowired
private ApiUrlConfig apiUrlConfig;

private static final Cache<String, String> RESOU_LOCAL_CACHE;

static {
RESOU_LOCAL_CACHE = CacheBuilder.newBuilder()
.softValues()
.maximumSize(10L)
.expireAfterWrite(300L, TimeUnit.SECONDS)
.build();
}

/**
* 微博热搜
* @return
*/
public List<WeiboHotSearchResultBaseVO> getWeiboHotSearchListFromCache(int size) {
String localCacheKey = LocalCacheKey.WEIBO.name();
String localCacheVal = RESOU_LOCAL_CACHE.getIfPresent(localCacheKey);
List<WeiboHotSearchResultBaseVO> list;
if (StringUtils.isNotBlank(localCacheVal)) {
list = JsonUtil.fromJson(localCacheVal, new TypeReference<List<WeiboHotSearchResultBaseVO>>() {});
} else {
String key = RedisKeyEnum.WEIBO_HOT_SEARCH_LIST_CACHE.getKey();
String val = redisService.get(key);
if (StringUtils.isNotBlank(val)) {
list = JsonUtil.fromJson(val, new TypeReference<List<WeiboHotSearchResultBaseVO>>() {});
} else {
// 上篇文章中有这个刷新方法的具体实现逻辑
list = refreshAndGetWeiboHotSearchListCache();
}
// 本地缓存如果没有则刷新到本地缓存
RESOU_LOCAL_CACHE.put(localCacheKey, JsonUtil.toJsonString(list));
}
if (CollectionUtils.isNotEmpty(list)) {
list = list.subList(0, size > list.size() ? list.size() : size);
}
return list;
}

ok,两集缓存实现接口响应性能到底怎么样,我们看服务端日志稳定在10ms之内:

接口AKSK鉴权

开放z z鉴权Aksk是做常见的做法之一,这里我们其实实现的也比较简单,具体方案如下图所示:

(1)用户请求接口,header头邀请携带自己的app_key、secret_key

(2)APIFilter拦截API请求,做ak、sk做验证

(3)aksk验证通过请求api,请求缓存获取数据

(4)返回数据

APIFilter定义

@Slf4j
@Order(1)
@WebFilter(filterName = "apiRequestFilter", urlPatterns = {"/api/poster/*", "/api/resou/*", "/api/url/*",
"/api/upload/*", "/api/text/*", "/api/ip/*", "/api/music/*", "/api/yulu/*", "/api/openai/*", "/rmi/*"})
public class ApiRequestFilter implements Filter {

@Autowired
private ApiInvokeRecordService apiInvokeRecordService;

@Autowired
private AccessSecretKeyService accessSecretKeyService;

/***
* 开放API清单
*/
private static Map<String, String> openApiMap = new ConcurrentHashMap<>();

private static List<String> WHITE_API_LIST = new ArrayList<>();

static {

// 需要Ak、Sk鉴权的请求
openApiMap.put("/api/resou/v1/weibo", "微博热搜");

/**
* 白名单
*/
WHITE_API_LIST.add("/api/poster/qr.temp");


}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String uri = request.getRequestURI();
if (isWhiteApiUri(uri)) {
// 跳过鉴权
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if (uri != null && uri.startsWith("rmi")) {
String accessKey = request.getHeader("access-key");
String secretKey = request.getHeader("secret-key");
if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(secretKey)) {
APIResponseBean apiResponse = APIResponseBeanUtil.error(401, "无权限!");
backErrorMessage(response, apiResponse);
return;
}
}
if (isApiUri(uri)) {
// 获取API请求的认证参数:accessKey secretKey
String accessKey = request.getHeader("access-key");
String secretKey = request.getHeader("secret-key");
// AK、SK 鉴权(走缓存查询)
APIResponseBean apiResponseBean = accessSecretKeyService.checkRequestAkSk(accessKey, secretKey);
if (!apiResponseBean.getSuccess()) {
log.error("ApiRequestFilter.checkRequestAkSk error, AK:{}, SK:{}, uri:{}", accessKey, secretKey, uri);
backErrorMessage(response, apiResponseBean);
return;
}
if (StringUtils.isNotBlank(openApiMap.get(uri))) {
// API调用埋点
apiInvokeRecordService.point(uri, openApiMap.get(uri), accessKey);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}

/***
* 是否需要鉴权API
* @param uri
* @return
*/
private boolean isApiUri(String uri) {
if (openApiMap.keySet().contains(uri)) {
return true;
}
for (String api : openApiMap.keySet()) {
if (uri.startsWith(api)) {
return true;
}
}
return false;
}

/**
* 是否需要鉴权API
* @param uri
* @return
*/
private boolean isRmiApiUri(String uri) {
if (openApiMap.keySet().contains(uri)) {
return true;
}
for (String api : openApiMap.keySet()) {
if (uri.startsWith(api)) {
return true;
}
}
return false;
}

/**
* 是否白名单API
* @param uri
* @return
*/
private boolean isWhiteApiUri(String uri) {
return WHITE_API_LIST.contains(uri);
}

private void backErrorMessage(HttpServletResponse response, APIResponseBean apiResponseBean) {
response.setContentType("application/json; charset=UTF-8");
try {
response.getWriter().print(JsonUtil.toJsonString(apiResponseBean));
} catch (IOException e) {
e.printStackTrace();
}
}
}

Filter中我们维护了需要鉴权和跳过鉴权的白名单API、当前请求如果需要鉴权,获取header头中的ak、sk参数,查询缓存进行aksk认证,认证通过请求接口。

扩展点1

这里的AK、SK是什么时候生成的、生成规则又是什么?

答:ak、sk盒子实在用户注册的时候就为每个用户分配了(这了设计的不好,造成aksk的浪费,因为大部分用户其实没有自己的应用、也没有api调用的需求,最好是用户需要的时候自己出发生成按钮,在生成!)

AK、SK在哪里查看?

答:目前在个人中心和开放api服务平台都可以看到自己的专属ak、sk:

AKSK生成规则?

答:跟ID生成器一样,只要保证唯一就ok,我这里用的简单的uuid

既然ak、sk都是唯一的,为啥需要两个,只生成一个token不行吗?

答:也是可以的,能够做的鉴权就可以。

为什么业界一般的做法都有两个配对使用的,有的叫ak、有的叫appId其实是一样的,用来标识一个接入方,而secretKey其实可以有多个的,即一个用户有多个sk,方便统计、计费等,同时双重保障API调用更安全。

扩展点2

当前分享的方案是一种静态sk方案,有同学在接入云或者其他第三方api的时候用到了动态sk(token)的方式,看字面意思,静态sk指生成一次不会在变化,动态token及没次调用前需要重新请求获取一次(一般会有一定的实效性,避免没次都要获取造成的资源浪费,或者永久有效失去了动态的特点)

本文章转载微信公众号@coderutil技术

#你可能也喜欢这些API文章!