微博热搜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
- 1. AI文本生成
- 2. AI图片生成_文生图
- 3. AI图片生成_图生图
- 4. AI图像编辑
- 5. AI视频生成_文生视频
- 6. AI视频生成_图生视频
- 7. AI语音合成_文生语音
- 8. AI文本生成(中国)
最新文章
- 9个最佳Text2Sql开源项目:自然语言到SQL的高效转换工具
- 深入解析API网关策略:认证、授权、安全、流量处理与可观测性
- GraphQL API手册:如何构建、测试、使用和记录
- 自助式入职培训服务API:如何让企业管理更上一层楼?
- Python如何调用Jenkins API自动化发布
- 模型压缩四剑客:量化、剪枝、蒸馏、二值化
- 火山引擎如何接入API:从入门到实践的技术指南
- 为什么每个使用 API 的大型企业都需要一个 API 市场来增强其合作伙伴生态系统
- 构建更优质的API:2025年顶级API开发工具推荐 – Strapi
- 外部函数与内存API – Java 22 – 未记录
- FAPI 2.0 深度解析:下一代金融级 API 安全标准与实践指南
- .NET Core 下的 API 网关