APP下载
报价宝  ›  科技  › 

Spring Boot使用AOP实现REST界面简易灵活的安全认证

报价宝 来源:baojiabao.com 发布时间:2019-10-11 01:53:00 11月15日更新
报价宝综合消息Spring Boot使用AOP实现REST界面简易灵活的安全认证

本文将通过AOP的方式实现一个相对更加简易灵活的API安全认证服务,我们先看实现,然后介绍和分析AOP基本原理和常用术语。

一、Authorized实现

1、定义注解

package com.power.demo.common;

import java.lang.annotation.*;

/*

* 安全认证

* */

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface Authorized {

String value() default "";

}

这个注解看上去什么都没有,仅仅是一个占位符,用于标志是否需要安全认证。

2、表现层使用注解

@Authorized

@RequestMapping(value = "/getinfobyid", method = RequestMethod.POST)

@ApiOperation("根据商品Id查询商品资讯")

@ApiImplicitParams({

@ApiImplicitParam(paramType = "header", name = "authtoken", required = true, value = "authtoken", dataType =

"String"),

})

public GetGoodsByGoodsIdResponse getGoodsByGoodsId(@RequestHeader String authtoken, @RequestBody GetGoodsByGoodsIdRequest request) {

return _goodsApiService.getGoodsByGoodsId(request);

}

看上去就是在一个方法上加了Authorized注解,其实它也可以作用于类上,也可以类和方法混合使用。

3、请求认证切面

下面的程式码是实现灵活的安全认证的关键:

package com.power.demo.controller.tool;

import com.power.demo.common.AppConst;

import com.power.demo.common.Authorized;

import com.power.demo.common.BizResult;

import com.power.demo.service.contract.AuthTokenService;

import com.power.demo.util.PowerLogger;

import com.power.demo.util.SerializeUtil;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.aspectj.lang.annotation.Pointcut;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

import java.lang.annotation.Annotation;

/**

* 请求认证切面,验证自定义请求header的authtoken是否合法

**/

@Aspect

@Component

public class AuthorizedAspect {

@Autowired

private AuthTokenService authTokenService;

@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")

public void requestMapping() {

}

@Pointcut("execution(* com.power.demo.controller.*Controller.*(..))")

public void methodPointCut() {

}

/**

* 某个方法执行前进行请求合法性认证 注入Authorized注解 (先)

*/

@Before("requestMapping() && methodPointCut()&&@annotation(authorized)")

public void doBefore(JoinPoint joinPoint, Authorized authorized) throws Exception {

PowerLogger.info("方法认证开始...");

Class type = joinPoint.getSignature().getDeclaringType();

Annotation[] annotations = type.getAnnotationsByType(Authorized.class);

if (annotations != null && annotations.length > 0) {

PowerLogger.info("直接类认证");

return;

}

//获取当前http请求

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = attributes.getRequest();

String token = request.getHeader(AppConst.AUTH_TOKEN);

BizResult bizResult = authTokenService.powerCheck(token);

System.out.println(SerializeUtil.Serialize(bizResult));

if (bizResult.getIsOK() == true) {

PowerLogger.info("方法认证通过");

} else {

throw new Exception(bizResult.getMessage());

}

}

/**

* 类下面的所有方法执行前进行请求合法性认证 (后)

*/

@Before("requestMapping() && methodPointCut()")

public void doBefore(JoinPoint joinPoint) throws Exception {

PowerLogger.info("类认证开始...");

Annotation[] annotations = joinPoint.getSignature().getDeclaringType().getAnnotationsByType(Authorized.class);

if (annotations == null || annotations.length == 0) {

PowerLogger.info("类不需要认证");

return;

}

//获取当前http请求

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = attributes.getRequest();

String token = request.getHeader(AppConst.AUTH_TOKEN);

BizResult bizResult = authTokenService.powerCheck(token);

System.out.println(SerializeUtil.Serialize(bizResult));

if (bizResult.getIsOK() == true) {

PowerLogger.info("类认证通过");

} else {

throw new Exception(bizResult.getMessage());

}

}

}

需要注意的是,对类和方法上的Authorized处理,定义了过载的处理方法doBefore。AuthTokenService和上文介绍的处理逻辑一样,如果安全认证不通过,则丢掷异常。

如果我们在类上或者方法上都加了Authorized注解,不会进行重复安全认证,请放心使用。

4、统一异常处理

上文已经提到过,对所有发生异常的API,都返回统一格式的报文至呼叫方。主要程式码大致如下:

package com.power.demo.controller.exhandling;

import com.power.demo.common.ErrorInfo;

import org.springframework.http.HttpStatus;

import org.springframework.web.bind.annotation.ControllerAdvice;

import org.springframework.web.bind.annotation.ExceptionHandler;

import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

import java.util.Date;

/**

* 全域性统一异常处理增强

**/

@ControllerAdvice

public class GlobalExceptionHandler {

/**

* API统一异常处理

**/

@ExceptionHandler(value = Exception.class)

@ResponseBody

public ErrorInfo jsonApiErrorHandler(HttpServletRequest request, Exception e) {

ErrorInfo errorInfo = new ErrorInfo();

try {

System.out.println("统一异常处理...");

e.printStackTrace();

Throwable innerEx = e.getCause();

while (innerEx != null) {

//innerEx.printStackTrace();

if (innerEx.getCause() == null) {

break;

}

innerEx = innerEx.getCause();

}

if (innerEx == null) {

errorInfo.setMessage(e.getMessage());

errorInfo.setError(e.toString());

} else {

errorInfo.setMessage(innerEx.getMessage());

errorInfo.setError(innerEx.toString());

}

errorInfo.setData(e);

errorInfo.setTimestamp(new Date());

errorInfo.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//500错误

errorInfo.setUrl(request.getRequestURL().toString());

errorInfo.setPath(request.getServletPath());

} catch (Exception ex) {

ex.printStackTrace();

errorInfo.setMessage(ex.getMessage());

errorInfo.setError(ex.toString());

}

return errorInfo;

}

}

认证不通过的API呼叫结果如下:

异常的整个堆叠可以非常非常方便地帮助我们排查到问题。

我们再结合上文来看安全认证的时间先后,根据理论分析和实践发现,过滤器Filter先于拦截器Interceptor先于自定义Authorized方法认证先于Authorized类认证。

到这里,我们发现通过AOP框架AspectJ,一个@Aspect注解外加几个方法几十行业务程式码,就可以轻松实现对REST API的拦截处理。

那么为什么会有@Pointcut,既然有@Before,是否有@After?

其实上述简易安全认证功能实现的过程主要利用了Spring的AOP特性。

下面再简单介绍下AOP常见概念(主要参考Spring实战),加深理解。AOP概念较多而且比较乏味,经验丰富的老鸟到此就可以忽略这一段了。

二、AOP

1、概述

AOP(Aspect Oriented Programming),即面向切面程式设计,可以处理很多事情,常见的功能比如日志记录,效能统计,安全控制,事务处理,异常处理等。

AOP可以认为是一种更高阶的“复用”技术,它是OOP(Object Oriented Programming,面向物件程式设计)的补充和完善。AOP的理念,就是将分散在各个业务逻辑程式码中相同的程式码通过横向切割的方式抽取到一个独立的模组中。将相同逻辑的重复程式码横向抽取出来,使用动态代理技术将这些重复程式码织入到目标物件方法中,实现和原来一样的功能。这样一来,我们在写业务逻辑时就只关心业务程式码。

OOP引入封装、继承、多型等概念来建立一种物件层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志程式码往往横向地散布在所有物件层次中,而与它对应的物件的核心功能毫无关系对于其他型别的程式码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的程式码被称为横切(cross cutting),在OOP设计中,它导致了大量程式码的重复,而不利于各个模组的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的物件内部,并将那些影响了多个类的公共行为封装到一个可重用模组,并将其命名为"Aspect",即切面。

所谓"切面",简单说就是那些与业务无关,却为业务模组所共同呼叫的逻辑或责任封装起来,便于减少系统的重复程式码,降低模组之间的耦合度,并有利于未来的可操作性和可维护性。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。

业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,它们经常发生在核心关注点的多处,而各处基本相似,比如许可权认证、日志、事务。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

2、AOP术语

深刻理解AOP,要掌握的术语可真不少。

Target:目标类,需要被代理的类,如:UserService

Advice:通知,所要增强或增加的功能,定义了切面的“什么”和“何时”,模式有Before、After、After-returning,、After-throwing和Around

Join Point:连线点,应用执行过程中,能够插入切面的所有“点”(时机)

Pointcut:切点,实际执行中,选择插入切面的连线点,即定义了哪些点得到了增强。切点定义了切面的“何处”。我们通常使用明确的类和方法名称,或是利用正则表示式定义所匹配的类和方法名称来指定这些切点。

Aspect:切面,把横切关注点模组化为特殊的类,这些类称为切面,切面是通知和切点的结合。通知和切点共同定义了切面的全部内容:它是什么,在何时和何处完成其功能

Introduction:引入,允许我们向现有的类新增新方法或属性

Weaving:织入,把切面应用到目标物件并建立新的代理物件的过程,切面在指定的连线点被织入到目标物件中。在目标物件的生命周期里有多个点可以进行织入:编译期、类载入期、执行期

下面参考自网上图片,可以比较直观地理解上述这几个AOP术语和流转过程。

3、AOP实现

(1)动态代理

使用动态代理可以为一个或多个界面在执行期动态生成实现物件,生成的物件中实现界面的方法时可以新增增强程式码,从而实现AOP:

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

import java.lang.reflect.Proxy;

/**

* 动态代理类

*/

public class DynamicProxy implements InvocationHandler {

/**

* 需要代理的目标类

*/

private Object target;

/**

* 写法固定,aop专用:系结委托物件并返回一个代理类

*

* @param target

* @return

*/

public Object bind(Object target) {

this.target = target;

return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);

}

/**

* 呼叫 InvocationHandler界面定义方法

*

* @param proxy 指被代理的物件。

* @param method 要呼叫的方法

* @param args 方法呼叫时所需要的引数

*/

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

Object result = null;

// 切面之前执行

System.out.println("[动态代理]切面之前执行");

// 执行业务

result = method.invoke(target, args);

// 切面之后执行

System.out.println("[动态代理]切面之后执行");

return result;

}

}

缺点是只能针对界面进行代理,同时由于动态代理是通过反射实现的,有时可能要考虑反射呼叫的开销,否则很容易引发效能问题。

(2)字节码生成

动态字节码生成技术是指在执行时动态生成指定类的一个子类物件(注意是针对类),并覆盖其中特定方法,覆盖方法时可以新增增强程式码,从而实现AOP。

最常用的工具是CGLib:

import org.springframework.cglib.proxy.Enhancer;

import org.springframework.cglib.proxy.MethodInterceptor;

import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**

* 使用cglib动态代理

*

* JDK中的动态代理使用时,必须有业务界面,而cglib是针对类的

*/

public class CglibProxy implements MethodInterceptor {

private Object target;

/**

* 建立代理物件

*

* @param target

* @return

*/

public Object getInstance(Object target) {

this.target = target;

Enhancer enhancer = new Enhancer();

enhancer.setSuperclass(this.target.getClass());

// 回拨方法

enhancer.setCallback(this);

// 建立代理物件

return enhancer.create();

}

@Override

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

Object result = null;

System.out.println("[cglib]切面之前执行");

result = methodProxy.invokeSuper(proxy, args);

System.out.println("[cglib]切面之后执行");

return result;

}

}

(3)定制的类载入器

当需要对类的所有物件都新增增强,动态代理和字节码生成本质上都需要动态构造代理物件,即最终被增强的物件是由AOP框架生成,不是开发者new出来的。

解决的办法就是实现自定义的类载入器,在一个类被载入时对其进行增强。

JBoss就是采用这种方式实现AOP功能。

这种方式目前只是道听途说,本人没有在实际专案中实践过。

(4)程式码生成

利用工具在已有程式码基础上生成新的程式码,其中可以新增任何横切程式码来实现AOP。

(5)语言扩充套件

可以对构造方法和属性的赋值操作进行增强,AspectJ是采用这种方式实现AOP的一个常见的Java语言扩充套件。

比较:根据日志,上述流程的执行顺序依次为:过滤器、拦截器、AOP方法认证、AOP类认证

附:记录API日志

最后通过记录API日志,记录日志时加入API耗时统计(其实我们在开发.NET应用的过程中通过AOP这种记录日志的方式也已经是标配),加深上述AOP的几个核心概念的理解:

package com.power.demo.controller.tool;

import com.power.demo.apientity.BaseApiRequest;

import com.power.demo.apientity.BaseApiResponse;

import com.power.demo.util.DateTimeUtil;

import com.power.demo.util.SerializeUtil;

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.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Component;

import org.springframework.util.StopWatch;

/**

* 服务日志切面,主要记录界面日志及耗时

**/

@Aspect

@Component

public class SvcLogAspect {

@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")

public void requestMapping() {

}

@Pointcut("execution(* com.power.demo.controller.*Controller.*(..))")

public void methodPointCut() {

}

@Around("requestMapping() && methodPointCut()")

public Object around(ProceedingJoinPoint pjd) throws Throwable {

System.out.println("Spring AOP方式记录服务日志");

Object response = null;//定义返回资讯

BaseApiRequest baseApiRequest = null;//请求基类

int index = 0;

Signature curSignature = pjd.getSignature();

String className = curSignature.getClass().getName();//类名

String methodName = curSignature.getName(); //方法名

Logger logger = LoggerFactory.getLogger(className);//日志

StopWatch watch = DateTimeUtil.StartNew();//用于统计呼叫耗时

// 获取方法引数

Object[] reqParamArr = pjd.getArgs();

StringBuffer sb = new StringBuffer();

//获取请求引数集合并进行遍历拼接

for (Object reqParam : reqParamArr) {

if (reqParam == null) {

index++;

continue;

}

try {

sb.append(SerializeUtil.Serialize(reqParam));

//获取继承自BaseApiRequest的请求实体

if (baseApiRequest == null && reqParam instanceof BaseApiRequest) {

index++;

baseApiRequest = (BaseApiRequest) reqParam;

}

} catch (Exception e) {

sb.append(reqParam.toString());

}

sb.append(",");

}

String strParam = sb.toString();

if (strParam.length() > 0) {

strParam = strParam.substring(0, strParam.length() - 1);

}

//记录请求

logger.info(String.format("【%s】类的【%s】方法,请求引数:%s", className, methodName, strParam));

response = pjd.proceed(); // 执行服务方法

watch.stop();

//记录应答

logger.info(String.format("【%s】类的【%s】方法,应答引数:%s", className, methodName, SerializeUtil.Serialize(response)));

// 获取执行完的时间

logger.info(String.format("界面【%s】总耗时(毫秒):%s", methodName, watch.getTotalTimeMillis()));

//标准请求-应答模型

if (baseApiRequest == null) {

return response;

}

if ((response != null && response instanceof BaseApiResponse) == false) {

return response;

}

System.out.println("Spring AOP方式记录标准请求-应答模型服务日志");

Object request = reqParamArr[index];

BaseApiResponse bizResp = (BaseApiResponse) response;

//记录日志

String msg = String.format("请求:%s======应答:%s======总耗时(毫秒):%s", SerializeUtil.Serialize(request),

SerializeUtil.Serialize(response), watch.getTotalTimeMillis());

if (bizResp.getIsOK() == true) {

logger.info(msg);

} else {

logger.error(msg);//记录错误日志

}

return response;

}

}

标准的请求-应答模型,我们都会定义请求基类和应答基类,本文示例给到的是BaseApiRequest和BaseApiResponse,搜集日志时,可以对错误日志加以区分特殊处理。

注意上述程式码中的@Around环绕通知,引数型别是ProceedingJoinPoint,而前面第一个示例的@Before前置通知,引数型别是JoinPoint。

下面是AspectJ通知和增强的5种模式:

@Before前置通知,在目标方法执行前实施增强,请求引数JoinPoint,用来连线当前连线点的连线细节,一般包括方法名和引数值。在方法执行前进行执行方法体,不能改变方法引数,也不能改变方法执行结果。

@After 后置通知,请求引数JoinPoint,在目标方法执行之后,无论是否发生异常,都进行执行的通知。在后置通知中,不能访问目标方法的执行结果(因为有可能发生异常),不能改变方法执行结果。

@AfterReturning 返回通知,在目标方法执行后实施增强,请求引数JoinPoint,其能访问方法执行结果(因为正常执行)和方法的连线细节,但是不能改变方法执行结果。(注意和后置通知的区别) 

@AfterThrowing 异常通知,在方法丢掷异常后实施增强,请求引数JoinPoint,throwing属性代表方法体执行时候丢掷的异常,其值一定与方法中Exception的值需要一致。

@Around 环绕通知,请求引数ProceedingJoinPoint,环绕通知类似于动态代理的全过程,ProceedingJoinPoint型别的引数可以决定是否执行目标方法,而且环绕通知必须有返回值,返回值即为目标方法的返回值。

本文转自于部落格园

原文:https://www.cnblogs.com/jeffwongishandsome/p/spring-boot-use-aop-to-implement-an-easy-and-flexible-auth-system.html

文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 报价宝 笔记本价格 笔记本电脑价格 电视机价格 红米手机价格