AOP(Aspect Oriented Programming):面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的技术。
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提供程序的可重用性,同时提高了开发的效率
通知(Advice):通知描述了切面要完成的工作以及何时执行。
连接点(JoinPoint):通知功能被应用的时机。可以拿到目标方法的参数、返回值、签名等
ProceedingJoinPoint
:用于环绕通知,是JoinPoint的子接口,在JoinPoint的基础上,增加了两个方法(最多使用)
Object proceed() throws Throwable // 执行目标方法
Object proceed(Object[] var1) throws Throwable // 传入的新的参数去执行目标方法
切点(Pointcut):切点定义了通知功能被应用的范围。某个方法,还是某个类,还是所有的类…
切面(Aspect):就是通知和切点的结合,定义了何时、何地应用通知功能
引入(Introduction):在无需修改现有类的情况下,向现有的类添加新方法或属性。
织入(Weaving):把切面应用到目标对象并创建新的代理对象的过程
制定了通知被应用的范围,具体的格式为:
execution(访问修饰符 返回值类型 包名.类名.方法名称(方法参数))
示例:com.lwclick.java.controller包下所有类的public方法
execution(public com.lwlick.java.controller.*.*(..))
定义一个接口控制切面,在环绕通知中,获取用户请求头中携带的信息进行鉴别,判断是否允许执行目标方法(其余的功能,都可与该功能合并实现)
@Aspect
@Component
@Order(1)
public class LoginAccessAspect {
private static final Logger LOG = LoggerFactory.getLogger(LoginAccessAspect.class);
/**
* 所有controller下的方法
*/
@Pointcut("execution(public * com.lwclick.*controller.*(..))")
public void pointCut() {
}
/**
* 登录方法
*/
@Pointcut("execution(public String com.lwclick.SysController.login(..))")
public void login() {
}
/**
* 拦截除【登录方法】之外的,所有controller下的方法
*
* @return
*/
@Around("pointCut() && !login()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 此处无需判断 requestAttributes 是否为空
HttpServletRequest request = requestAttributes.getRequest();
// 获取请求头中的 Authorization 信息
String authorization = request.getHeader("Authorization");
// TODO: 根据 authorization 信息进行判断
if ("通过".equals(authorization)) {
// 通过了判断,可以认为是携带着登录信息进行请求的,则【允许目标方法继续执行】!!!!!!
return joinPoint.proceed();
}
// 返回错误信息,此处通过反射使用了 R 或 AjaxResult 的 error方法(若依框架中的R类及AjaxResult类)
LOG.info("存在未登录系统的请求,IP为:{}", getIpAddress(request));
LOG.info("存在未登录系统的请求,具体参数为:{}", getParameter(method, joinPoint.getArgs()));
Class<?> returnType = method.getReturnType();
Method error = returnType.getMethod("error", String.class);
return error.invoke(returnType, "未登录系统,禁止访问!");
}
/**
* 获取request中的IP
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
/**
* 根据方法和传入的参数获取请求参数
*
* @param method 具体的方法
* @param args 参数列表
* @return
*/
private Object getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
// 将RequestBody注解修饰的参数作为请求参数
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
// 将RequestParam注解修饰的参数作为请求参数
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>();
String key = parameters[i].getName();
if (!StringUtils.isEmpty(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.size() == 0) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
}
在接口鉴权访问的基础上,当获取到用户的请求IP时,从数据库或缓存中取出白名单组,判断该IP是否在白名单组中即可。
在接口鉴权访问的基础上,通过request获取请求的信息,进行记录即可
// 获取请求的URL http://127.0.0.1:8080/api/getName
String urlStr = request.getRequestURL().toString();
// 获取请求的方式 POST
String methodStr = request.getMethod();
// 获取请求的URI /api/getName
String uri = request.getRequestURI();
// 获取请求的参数
getParameter(method, joinPoint.getArgs()); // getParameter,上面【接口鉴权访问】的代码中有
// 获取方法执行后的结果
Object result = joinPoint.proceed();
实现思路:
注意:count 为访问次数,time 为指定时间,结合起来就是在某段时间内,如果请求达到一定次数,则不予响应
@Retention(RetentionPolicy.RUNTIME) // 运行时
@Target({ElementType.TYPE, ElementType.METHOD}) // 可以被用在类及方法上
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级
public @interface LimitRequest {
/**
* 允许的请求次数,默认 MAX_VALUE
*
* @return
*/
int count() default Integer.MAX_VALUE;
/**
* 时间段,单位为毫秒数(和redis统一),默认 1分钟
*
* @return
*/
int time() default 60 * 1000;
}
在需要限制请求次数的类或者方法上,添加 @LimitRequest注解:1分钟内,该接口仅允许被调用 2 次
@LimitRequest(count = 2)
@GetMapping("/getInfo")
public Map<String, Object> getInfo(@RequestParam String deptCode) {
// TODO
}
当用户进行请求时,切面拦截到请求开始判断,通过设置 redis的数据,key 为 ip+接口名称,value为已经调用次数,redis数据有效时间为注解定义的时间
@Aspect
@Component
@Order(1)
public class ApiControllerAspect {
private static final Logger logger = LoggerFactory.getLogger(ApiControllerAspect.class);
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 需要拦截的位置(所有controller中返回值为 Map的)
*/
@Pointcut("execution(public java.util.Map com.lwclick.controller.*Controller.*(..)) ")
public void webAspect() {
}
@Around("webAspect()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取当前请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
boolean flag = true;
String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
HttpServletRequest request = attributes.getRequest();
// 获取类注释
Class<ApiController> aClass = ApiController.class;
InterfaceReqLimit classAnnotation = aClass.getAnnotation(InterfaceReqLimit.class);
// 获取方法注释
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
InterfaceReqLimit methodAnnotation = method.getAnnotation(InterfaceReqLimit.class);
String uri = request.getRequestURI();
if (methodAnnotation != null) {
flag = validRequestCount(ipAddr, uri.replace("/", "_"), methodAnnotation.count(), methodAnnotation.time());
} else if (classAnnotation != null) {
flag = validRequestCount(ipAddr, uri.replace("/", "_"), classAnnotation.count(), classAnnotation.time());
}
logger.info("{}请求{}超过请求次数,请稍后再试", ipAddr, uri);
if (flag) {
return joinPoint.proceed();
}
return new HashMap<String, Object>() {
{
put("msg", "超过请求次数,请稍后再试");
}
};
}
/**
* 判断某一个ip请求接口的次数是否超过限制
*
* @param ipAddr
* @param uri
* @param limitCount
* @param timeOut
* @return
*/
private boolean validRequestCount(String ipAddr, String uri, int limitCount, long timeOut) {
try {
// /api/queryInfo
String redisKey = "req_limit_".concat(ipAddr).concat(uri);
long count = redisTemplate.opsForValue().increment(redisKey, 1);
if (count == 1) {
redisTemplate.expire(redisKey, timeOut, TimeUnit.MILLISECONDS);
}
if (count > limitCount) {
return false;
}
} catch (Exception e) {
return false;
}
return true;
}
}