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;
}
}
以上是服务端处理安全校验的思路分析以及实现过程,如果感觉有所帮助欢迎点赞收藏或是评论区留言!