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