天天看點

伺服器接口安全校驗限流處理方案(java版)

         1.背景

         2.實作思路

         3.實作過程

1.背景

     現在運作項目中,需要單獨給下遊服務商提供資料通路接口以完成項目合作,需要進行以下安全适配處理:

1.授權校驗:對下遊服務商進行差別與現在已有的接口授權資訊校驗;即現在項目需要提供單獨的授權校驗處理;

2.ip限制:僅添加白名單的服務商ip可以通路;

3.通路次數限制:不同的接口實作不同的QPS(每秒查詢的次數)通路.

     現将需求實作過程進行記錄,希望對于有同樣需求的同學有所幫助!

2.實作思路

    使用aop實作方法攔截,切面處添加校驗業務處理;

    1.授權校驗:

        提供服務商授權資訊擷取接口,服務商通路接口均須帶有授權資訊方可通路(header中攜帶認證資訊);

    2.ip限制:

        擷取每次請求中的ip位址,與資料庫的記錄的白名單ip進行比對,沒有記錄的視為非法ip通路;

    3.通路次數限制:

    采用注解方式動态指定不同接口指定不同的通路頻次,輕量級java緩存方案ExpiringMap實作通路次數是否達到設定上限校驗;

3.實作過程

    現有項目原有授權驗證邏輯與新增服務商通路授權邏輯簡要說明:

    現在項目接口均進行shiro校驗,保持原有邏輯不變,對新增的服務商接口放開校驗限制,認證授權采用新的校驗規則(自定義aop實作).具體代碼實作如下:

    自定義aop:

IntelligenceAop.java

:

@Aspect
@Component
@Slf4j
public class IntelligenceAop {



    @Autowired
    private RedisTemplate redisTemplate;

    // 接口通路次數限制緩存,格式:{"接口名":{"通路ip",通路次數}}
    private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> visitCountMap = new ConcurrentHashMap<>();

	// 切入點表達式指定服務商通路接口範圍
    @Pointcut("execution(* com.A.api.intelligence.controller.*.*(..))")
    public void log() {}

	// 對getToken擷取授權資訊不進行安全校驗
    @Pointcut("execution(* com.A.api.intelligenceCode.controller.IntelligenceController.getToken(..))")
    public void excludeLog() {}

	// 最終切入點表達式範圍為排除getToken之外的所有服務商接口
    @Pointcut("log() && !excludeLog()")
    public void allPointcutWeb() {
    }

 	// doAround優先于before執行,三項安全校驗從這裡處理
    @Around("allPointcutWeb()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {

        // 校驗ip是否添加通路白名單
        HttpServletRequest request = HttpServletRequestUtil.getRequest();
        String thirdToken = request.getHeader("thirdToken");
        if(StrUtil.isBlank(thirdToken)){
            log.error("非法通路ip:{}",request.getRemoteAddr());
            throw new BusinessException("非法通路!");
        }

        // 校驗三方請求token是否有效
        checkThirdToken(thirdToken);

        // 校驗接口通路次數
        checkVisitCount(pjp, request);

        Object ob = pjp.proceed();// ob 為方法的傳回值
       
        return ob;
    }

   // 校驗三方請求token是否有效
    private void checkThirdToken(String thirdToken) {
        String thirdTokenRedis = (String)redisTemplate.opsForValue().get("thirdToken");
        if(StrUtil.isBlank(thirdTokenRedis)){
            throw new BusinessException("認證授權資訊已過期,請重新擷取!");
        }
        if(!StrUtil.equals(thirdToken,thirdTokenRedis)){
            log.error("非法token資訊通路,請求token:{},緩存token:{},",thirdToken,thirdTokenRedis);
            throw new BusinessException("非法通路");
        }
    }

    // 校驗通路次數
    private void checkVisitCount(ProceedingJoinPoint pjp, HttpServletRequest request) throws Exception {
        VisitCountAnnotation visitCountAnnotation = getVisitCountAnnotation(pjp);
        if(ObjectUtil.isNotNull(visitCountAnnotation)){
            // 第一個參數是通路路徑, 第二個參數是預設值
            ExpiringMap<String, Integer> map = visitCountMap.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build());
            // 接口通路次數
            Integer visitCount = map.getOrDefault(request.getRemoteAddr(), 0);


            if (visitCount >= visitCountAnnotation.count()) { // 超過次數,不執行目标方法
               throw new BusinessException("非法通路:已超過最大通路次數限制,請稍後重試!");
            } else if (visitCount == 0){ // 第一次請求時,設定開始有效時間
                map.put(request.getRemoteAddr(), visitCount + 1, ExpirationPolicy.CREATED, visitCountAnnotation.time(), TimeUnit.MILLISECONDS);
            } else { // 未超過次數, 記錄資料加一
                map.put(request.getRemoteAddr(), visitCount + 1);
            }
            visitCountMap.put(request.getRequestURI(), map);
        }
    }

   

  // 判斷是否存在VisitCountAnnotation注解
    private VisitCountAnnotation getVisitCountAnnotation(JoinPoint joinPoint) throws Exception
    {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null)
        {
            return method.getAnnotation(VisitCountAnnotation.class);
        }
        return null;
    }
}
           

    通路次數限制注解

VisitCountAnnotation

:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VisitCountAnnotation {

    // QPS:2000
    long time() default 1000; // 限制時間 機關:毫秒

    int count() default 2000; // 允許請求的次數

}
           

    控制類

IntelligenceController.java

:

@RequestMapping("/A")
@Validated
@RestController
public class IntelligenceController {

    @Autowired
    private IntelligenceServiceImpl intelligenceService;


    @GetMapping("/getToken")
    @ApiOperation(value = "擷取請求認證資訊")
    public ApiResult getToken() throws Exception {
        String thirdToken = intelligenceService.getToken();
        return ApiResult.ok(thirdToken);
    }

    @VisitCountAnnotation()  // 如有需求也可以指定通路次數與通路時間,否則按照預設處理
    @GetMapping("/findUserInfoList")
    @ApiOperation(value = "查詢使用者資訊")
    public ApiResult<PageInfo<IntelligenceCodeUserInfo>> findUserInfoList(@NotNull(message = "目前頁面碼數不允許為空!")
                                                                                                  @Min(value = 1,message = "目前頁面碼數不允許為0!") Integer currentPage,
                                                                                      @NotNull(message = "每頁顯示條數不允許為空!")
                                                                                      @Min(value = 1,message = "每頁顯示條數不允許為0!") Integer pageSize) throws Exception {
        PageInfo<IntelligenceCodeUserInfo> intelligenceCodeUserInfoPageInfo = intelligenceCodeService.findUserInfoList(currentPage,pageSize);
        return ApiResult.ok(intelligenceCodeUserInfoPageInfo);
    }
}
           

    實作類

IntelligenceServiceImpl.java

:

@Service
@Slf4j
public class IntelligenceServiceImpl implements IntelligenceService {

    @Autowired
    private IntelligenceMapper intelligenceMapper;

    @Autowired
    private RedisTemplate redisTemplate;



    // 三方token緩存時間,機關秒,預設2小時,讀取配置檔案資訊
    @Value("${intelligence.expireTime}")
    @Autowired
    private Integer expireTime;


  // 查詢智能碼會員資訊
    @Override
    public PageInfo<IntelligenceCodeUserInfo> findUserInfoList(Integer currentPage,Integer pageSize) {
        PageHelper.startPage(currentPage,pageSize);
        List<IntelligenceCodeUserInfo> intelligenceUserInfoList = intelligenceCodeMapper.findUserInfoList();
        PageInfo<IntelligenceCodeUserInfo> intelligenceCodeUserInfoPageInfo = new PageInfo<>(intelligenceUserInfoList);
        return intelligenceCodeUserInfoPageInfo;
    }

  // 擷取三方認證授權資訊
    @Override
    public synchronized String getToken() throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {


        // 擷取目前通路ip
        HttpServletRequest request = HttpServletRequestUtil.getRequest();
        String visitIp = request.getRemoteAddr();

        // 判斷目前通路ip是否添加白名單,讀取資料庫添加的ip白名單
        String whiteInfo = intelligenceCodeMapper.findWhiteIp();
        if(StrUtil.isBlank(whiteInfo)){
            throw new BusinessException("資料異常:擷取為空!");
        }

        JSONObject jsonObject = JSONUtil.parseObj(whiteInfo);
        List whiteIps = jsonObject.get("whiteIp", List.class);
        if(!CollectionUtil.contains(whiteIps,visitIp)){
            throw new BusinessException("非法通路:未添加ip白名單!");
        }

        // 查詢是否存在認證緩存資訊,如果存在則直接傳回,不存在則重新生成
        String redisThirdToken = (String) redisTemplate.opsForValue().get("thirdToken");
        if(StrUtil.isNotBlank(redisThirdToken)){
            return redisThirdToken;
        }

        // 生成通路token資訊,可自定義token生成規則,此處不再展開
        String thirdToken = getThirdToken();
        
        // 緩存中設定thirdToken
        redisTemplate.opsForValue().set("thirdToken",thirdToken, expireTime, TimeUnit.SECONDS);


        return thirdToken;
    }

}
           

    以上是服務端處理安全校驗的思路分析以及實作過程,如果感覺有所幫助歡迎點贊收藏或是評論區留言!