开放平台之开放中心搭建(三)完结篇

开放中心单独作为一个微服务工程,对三方调用者提供获取接口调用凭证,校验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、当调用测试开放接口的速率在配置的阈值范围内的时候。

如需完整代码,请小伙伴们点赞加关注,私信我领取。创作不易,您的支持是我今后更新的最大动力。感谢!

  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值