需求背景
在互联网这种多数面向C端用户场景下的产品功能研发完成交付后,通常并不会直接发布上线。尤其是在一个原有服务功能已经沉淀了大量用户时,不断的迭代开发新增需求下,更不会贸然发布上线。
虽然在测试环境、预发环境都有了相应功能的验证,但在真实的用户场景下可能还会存在其他隐患问题。那么为了更好的控制系统风险,通常需要研发人员在代码的接口层,提供白名单控制。上线初期先提供可配置的白名单用户进行访问验证,控制整体的交付风险程度。
方案设计
技术实现
自动配置类和类属性
自动配置类
@Configuration
@ConditionalOnClass(WhiteListProperties.class)
@EnableConfigurationProperties(WhiteListProperties.class)
public class WhiteListAutoConfigure {
private final Logger logger = LoggerFactory.getLogger(WhiteListAutoConfigure.class);
@Bean("whiteListConfig")
@ConditionalOnMissingBean
public String whiteListConfig(WhiteListProperties properties){
return properties.getUsers();
}
}
自动配置属性类用来读取加载了本工程的application.yml中配置的属性信息
@ConfigurationProperties(prefix = "whitelist")
public class WhiteListProperties {
private String users;
public String getUsers() {
return users;
}
public void setUsers(String users){
this.users = users;
}
}
使用时可以配置application.yml为
whitelist:
users: user001,user002
自定义接口
用于标识哪些接口需要进行白白名单过滤
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface DoWhiteList {
// 配置当前接口入参需要提取的属性
String key() default "";
// 拦截到用户请求后需要给一个返回信息。
String returnJson() default "";
}
定义切面
package com.rzp.middleware.whitelist;
import com.alibaba.fastjson.JSON;
import com.rzp.middleware.whitelist.annotation.DoWhiteList;
import org.apache.commons.beanutils.BeanUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
@Aspect
@Component
public class DoJoinPoint {
private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);
// 获取自动配置类中配置的白名单用户id信息
@Resource
private String whiteListConfig;
// 对应的注解切点
@Pointcut("@annotation(com.rzp.middleware.whitelist.annotation.DoWhiteList)")
public void aopPoint(){
}
@Around("aopPoint()")
public Object doRouter(ProceedingJoinPoint jp) throws Throwable{
Method method = getMethod(jp);
DoWhiteList whiteList = method.getAnnotation(DoWhiteList.class);
// 拿到注解标注的key对应的值
String keyValue = getFiledValue(whiteList.key(), jp.getArgs());
logger.info("middleware whitelist handler method:{} value:{}", method.getName(), keyValue);
// 如果为空 证明不需要校验直接执行..
if (null == keyValue || "".equals(keyValue)) return jp.proceed();
// 拿到白名单用户群体
String[] split = whiteListConfig.split(",");
// 白名单过滤
for (String str : split) {
if (keyValue.equals(str)) {
return jp.proceed();
}
}
// 白名单没有当前用户id,返回一个json数据
return returnObject(whiteList, method);
}
private Object returnObject(DoWhiteList whiteList, Method method) throws InstantiationException, IllegalAccessException {
String returnJson = whiteList.returnJson();
Class<?> returnType = method.getReturnType();
if ("".equals(returnJson)) {
return returnType.newInstance();
}
return JSON.parseObject(returnJson, returnType);
}
// 拿到当前加了注解的接口方法
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return jp.getTarget().getClass().getMethod(methodSignature.getName(),methodSignature.getParameterTypes());
}
// 获取属性值 field代表要获取的字段值, args代表接口的全部字段
private String getFiledValue(String filed, Object[] args) {
String filedValue = null;
for (Object arg : args) {
try {
// 一个一个去找到对应的属性值
if (null == filedValue || "".equals(filedValue)) {
filedValue = BeanUtils.getProperty(arg, filed);
}else{
break;
}
}catch (Exception e){
if(args.length == 1){
return args[0].toString();
}
}
}
return filedValue;
}
}
测试验证
Controller编写
@RestController
public class UserController {
private Logger logger = LoggerFactory.getLogger(UserController.class);
@DoWhiteList(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
logger.info("查询用户信息,userId:{}", userId);
return new UserInfo("小新:" + userId, 19, "上海大别野");
}
}
application.yml配置
server:
port: 8081
whitelist:
users: user001,user002
测试结果
(git地址: https://gitcode.net/weixin_44794897/whitelist-project/)
不足之处
可以在中间件中添加一种注册中心来配置白名单,这样就可以在不重启服务的情况下动态变化白名单列表了。