使用spring-boot aop处理访问权限
spring-boot 2.0.1
1、pom.xml 添加aop-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、配置权限注解类 RequiresPermissions
package com.common.permission.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.common.permission.aop.authz.Logical;
/**
*
* Target: 注解类型,级别 Retention:RetentionPolicy.RUNTIME 运行时注解
*
* @author kouht
*
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {
String[] value();
Logical logical() default Logical.AND;
}
字段说明:value为权限值, logical为多个权限之间的链接关系,and和or的关系
3、Logical枚举类
package com.common.permission.aop.authz;
public enum Logical {
AND, OR
}
4、切面处理
package com.common.permission.aop.aspect;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.common.permission.aop.annotation.RequiresPermissions;
import com.common.permission.aop.authz.Logical;
import com.common.permission.aop.exception.AuthenticationException;
import com.util.CookieUtils;
import com.util.JsonUtil;
import com.web.constant.FxtxConstant;
import com.web.menu.FxtxCookie;
import com.web.menu.FxtxPermission;
/**
* 权限校验,负责对RequiresRoles、RequiresPermissions标记的controller进行权限校验
* 他执行的时机是interceptor之后、方法执行之前 Aspect 切面标识 Component 交给spring管理
* Order 设置aop优先级执行顺序,数字越小越先执行
* @author kouht
*
*/
@Order(2)
@Aspect
@Component
public class PermissionAspect {
private static final Logger logger = LoggerFactory.getLogger(PermissionAspect.class);
/**
* 定义一个切入点方法,即某方法执行时进行切入, 会拦截含有此注解的方法
*/
@Pointcut("@annotation(com.fxtx.oss.common.permission.aop.annotation.RequiresPermissions)")
private void permissionAnnotation() {
// 切入点签名
}
@Around("permissionAnnotation()")
public Object executeAround(ProceedingJoinPoint jp) throws Throwable {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse();
// 获取用户所有权限
Set<String> userPermissions = getLoginUserPermission(request);
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
// 根据被代理的类对象获取要执行的方法
Method realMethod = jp.getTarget().getClass().getDeclaredMethod(signature.getName(), targetMethod.getParameterTypes());
if (hasPermission(realMethod, userPermissions)) {
// 用户拥有该方法权限时执行方法里面的内容
return jp.proceed();
} else {
// 用户没有权限,则直接返回没有权限的通知
if (realMethod.isAnnotationPresent(ResponseBody.class)) {
// 判断是否有返回体
response.setHeader("Content-type", "application/json; charset=UTF-8");
OutputStream outputStream = response.getOutputStream();
Map<String, String> resultMsg = new HashMap<>();
resultMsg.put("msg", "权限不足,禁止访问!");
outputStream.write(new ObjectMapper().writeValueAsString(resultMsg).getBytes("UTF-8"));
return null;
} else {
throw new AuthenticationException("校验用户权限失败,权限不足");
}
}
}
/**
* 获取登录用户权限
*
* @param request
* @return
*/
@SuppressWarnings("unchecked")
public Set<String> getLoginUserPermission(HttpServletRequest request) {
String value = CookieUtils.getCookieValue(request, FxtxConstant.FXTX_COOKIE_KEY, true);
if (value != null) {
FxtxCookie fxtxCookie = JsonUtil.jsonToPojo(value, FxtxCookie.class);
String code = fxtxCookie.getCode();
Long tenantId = fxtxCookie.getTenantId();
String key = code + "-" + tenantId;
List<FxtxPermission> permissionsList = (List<FxtxPermission>) request.getSession().getAttribute(key);
if (!CollectionUtils.isEmpty(permissionsList)) {
Set<String> permissions = new HashSet<>();
for (int p = 0; p < permissionsList.size(); p++) {
if (!permissionsList.get(p).getPermissionValue().isEmpty()) {
permissions.add(permissionsList.get(p).getPermissionValue());
}
}
return permissions;
}
}
return new HashSet<>();
}
/**
* 判断用户是否拥有权限
*
* @param realMethod 访问当前的方法
* @param permissions 已拥有的所有权限
* @return
*/
private boolean hasPermission(Method realMethod, Set<String> permissions) {
try {
if (realMethod.isAnnotationPresent(RequiresPermissions.class)) {
// 获取该方法的指定注解
RequiresPermissions requiresPermissions = realMethod.getAnnotation(RequiresPermissions.class);
// 获取方法的权限
String[] values = requiresPermissions.value();
Set<String> requiresPermissionsSet = new HashSet<>();
StringBuilder permissionStr = new StringBuilder();
for (int i = 0; i < values.length; i++) {
requiresPermissionsSet.add(values[i]);
permissionStr.append(values[i] + ", ");
}
logger.info(realMethod + " RequiresPermissions = " + permissionStr);
// 获取权限值的链接关系
if (requiresPermissions.logical() == Logical.OR) {
// 任一权限
return hasAnyPermission(permissions, requiresPermissionsSet);
} else {
// 所有权限
return hasAllPermission(permissions, requiresPermissionsSet);
}
}
} catch (Exception e) {
return false;
}
return false;
}
/**
* 是否有所有权限
*
* @param permissionNames
* @return
*/
private boolean hasAllPermission(Set<String> permissions, Set<String> requiresPermissions) {
return !permissions.isEmpty() && !requiresPermissions.isEmpty() && permissions.containsAll(requiresPermissions);
}
/**
* 是否有任一权限
*
* @param permissionNames
* @return
*/
private boolean hasAnyPermission(Set<String> permissions, Set<String> requiresPermissions) {
boolean hasAnyPermission = false;
if (!permissions.isEmpty() && !requiresPermissions.isEmpty()) {
for (String permission : requiresPermissions) {
// 判断权限是否存在
if (permissions.contains(permission)) {
hasAnyPermission = true;
break;
}
}
}
return hasAnyPermission;
}
}
5、异常处理
package com.common.permission.aop.exception;
public class PermissionsException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* Creates a new PermissionsException.
*/
public PermissionsException() {
super();
}
/**
* Constructs a new PermissionsException.
*
* @param message the reason for the exception
*/
public PermissionsException(String message) {
super(message);
}
/**
* Constructs a new PermissionsException.
*
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public PermissionsException(Throwable cause) {
super(cause);
}
/**
* Constructs a new PermissionsException.
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public PermissionsException(String message, Throwable cause) {
super(message, cause);
}
}
package com.common.permission.aop.exception;
public class AuthenticationException extends PermissionsException {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* Creates a new AuthenticationException.
*/
public AuthenticationException() {
super();
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
*/
public AuthenticationException(String message) {
super(message);
}
/**
* Constructs a new AuthenticationException.
*
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(Throwable cause) {
super(cause);
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}
6、处理权限异常处理跳转,权限异常跳转到403页面
package com.web;
import org.springframework.web.bind.annotation.ExceptionHandler;
import com.common.permission.aop.exception.AuthenticationException;
public abstract class BaseController {
/**
* 授权登录异常
*/
@ExceptionHandler({AuthenticationException.class})
public String authenticationException() {
return "error/403";
}
}
7、所需拦截方法添加权限拦截控制
package com.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.common.permission.aop.annotation.RequiresPermissions;
import com.common.permission.aop.authz.Logical;
import com.web.BaseController;
@Controller
@RequestMapping("/test")
public class Controller extends BaseController {
/**
* 房东信息
* @return
*/
@RequiresPermissions("employee:view")
@RequestMapping(value = {"/view" }, method = RequestMethod.GET)
public String landlordManagementIndex() {
return "view.html";
}
@RequiresPermissions(value={"employee:create", "employee:hello"}, logical=Logical.AND)
@GetMapping("hello")
@ResponseBody
public String hello() {
return "{\"ret\":\"0\",\"data\":\"你好aop\"}";
}
}
@RequiresPermissions(value={"employee:create", "employee:hello"}, logical=Logical.AND)
表示访问需要有employee:create和employee:hello这两个权限,logical=Logical.AND表示这两个权限都必须有才可以访问
logical=Logical.OR表示任何一个权限都可以访问
注意: 问如果访问aop不生效,需要看切面的文件目录是否和主程序目录是否在相同目录下,spring-boot主程序启动默认扫描主程序同目录及同目录下的所有文件,如果不在同目录,则扫描不到切面,这个aop不生效,如果需要生效,解决方法两个:
1、将切面方放主程序目录下
2、主程序添加配置 @ComponentScan("aop目录")