SpringBoot使用自定义注解对方法进行AOP切面,从请求的Request中使用SPEL从请求的Request中获取请求的对象中的某一个列表进行get和set操作
SpringBoot使用自定义注解对方法进行AOP切面,从请求的Request中使用SPEL从请求的Request中获取请求的对象中的某一个列表进行get和set操作
业务场景
首先说一下使用的场景,整个项目是有权限控制的,各个角色都有不同的权限,现在这个接口就是一个分页查询借阅记录的一个接口,但是业务要求是这样,每个门店的店长只能看到自己门店的借阅记录。线下所有的门店通称为站点,每个门店(站点)都有自己唯一的站点编码,在后台权限系统中,可以对某一个角色进行站点权限的分配,比如站点A的店长Q在后台中的站点权限是站点A,那么店长Q就只拥有站点A的权限,那么该分页查询借阅记录的接口也只能让店长Q查询出站点A的借阅记录。
业务接口
@StationAuth(scope = "#req.stationCodes")
@PostMapping("/findList")
@ApiOperation("申诉处理记录分页")
public WSResponseResult<WSPage<BorrowHandleResponse>> findList(@RequestBody @Valid PageBorrowHandleReq req) {
//TODO 分页业务代码
}
请求的request:PageBorrowHandleReq.class
@Getter
@Setter
public class PageBorrowHandleReq implements Serializable {
@ApiModelProperty("员工姓名")
private String employeeName;
@ApiModelProperty("员工手机号")
private String phone;
@ApiModelProperty("用户Id")
private String userCode;
@ApiModelProperty("站点编号")
private List<String> stationCodes;
@ApiModelProperty("开始日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime startDate;
@ApiModelProperty("截止日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime endDate;
@ApiModelProperty("ISBN编码")
private String barCode;
@ApiModelProperty("借阅编号")
private String recordCode;
@ApiModelProperty("申诉处理状态 0 待处理 1 已处理 2 已撤销")
private ExceptionHandleEnum handleStatus;
@ApiModelProperty(value = "[分页]当前页码", example = "1")
@Min(value = 1, message = "页码不能小于1")
@NotNull(message = "页码不能小于1")
private Integer pageNum;
@ApiModelProperty(value = "[分页]每页记录数", example = "10")
@NotNull(message = "页面大小不能空")
private Integer pageSize;
}
分析
相信各位小伙伴都可以看出核心点在哪,那就是findList方法上的@StationAuth注解,这是一个自定义注解,用于站点权限分配。
@StationAuth
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface StationAuth {
String scope() default "";
}
SPEL
@StationAuth(scope = "#req.stationCodes")的意思是使用SPEL表达式从request中的PageBorrowHandleReq对象中取出stationCodes(站点编码集合)
AOP切面
Aspect切面主要处理的就是,首先使用SPEL表达式从request对象中取出stationCodes(站点编码集合),然后使用当前登录的用户的userCode和userName去权限系统中获取对应这个userCode的站点权限列表,如果stationCodes为空,返回的站点权限列表不为空,则直接将返回的站点权限列表通过SPEL表达式写入reqeust对象中的stationCodes中,如果stationCodes不为空,则和返回的站点权限列表进行权限校验,取stationCodes和返回的站点权限列表的交集,如果返回的站点权限列表为空则抛异常没有权限。
切面类:StationAuthAspect.class
@Aspect
@Component
public class StationAuthAspect {
@Autowired
private UserStationScopeFeign userStationScopeFeign;
private final String SPEL_FLAG = "#";
@Around("@annotation(stationAuth)")
public Object before(ProceedingJoinPoint thisJoinPoint, StationAuth stationAuth) throws Throwable {
MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
// 获取方法参数
Object[] args = thisJoinPoint.getArgs();
//设置当前角色的站点范围
setScope(stationAuth.scope(), methodSignature, args);
return thisJoinPoint.proceed();
}
//设置范围
private void setScope(String scope, MethodSignature methodSignature, Object[] args) {
// 获取请求范围
Set<String> requestScope = getRequestScope(args, scope, methodSignature.getMethod());
// 已授权范围
Set<String> authorizedScope = userStationScopeFeign.findUserScope(requestScope);
if (CollectionUtils.isEmpty(authorizedScope)) {
throw new AccessDeniedException("101", "没有可访问的权限范围");
}
// 设置新范围
setRequestScope(args, scope, authorizedScope, methodSignature.getMethod());
}
private void setRequestScope(Object[] args, String scopeName, Collection<String> scopeValues, Method method) {
if (StringUtils.isEmpty(scopeName)) {
scopeName = "#stationCode";
}
// 解析 SPEL 表达式
if (scopeName.indexOf(SPEL_FLAG) == 0) {
ParseSPEL.setMethodValue(scopeName, scopeValues, method, args);
}
}
//获取请求权限范围
private Set<String> getRequestScope(Object[] args, String scopeName, Method method) {
Collection<String> scopeList;
if (StringUtils.isEmpty(scopeName)) {
scopeName = "#stationCode";
}
// 解析 SPEL 表达式
if (scopeName.indexOf(SPEL_FLAG) == 0) {
scopeList = ParseSPEL.parseMethodKey(scopeName, method, args, Collection.class);
if (scopeList == null) {
return null;
}
} else {
return null;
}
Set<String> list = new HashSet<String>();
list.addAll(scopeList);
return list;
}
}
代码解释:@Around("@annotation(stationAuth)")切入点,标注了StationAuth注解的方法都会进入此切面,before方法就是当前方法调用之前执行,也就是前置操作。
UserStationScopeFeign.class
public interface UserStationScopeFeign {
/**
* 获取用户站点权限
*
* @return Set<String>
*/
Set<String> findUserScope(Set<String> requestScope);
}
UserStationScopeFeignImpl.class
@Repository
@Slf4j
public class UserStationScopeFeignImpl implements UserStationScopeFeign {
private final OauthFeign oauthFeign;
@Autowired
public UserStationScopeFeignImpl(OauthFeign oauthFeign) {
this.oauthFeign = oauthFeign;
}
@Override
public Set<String> findUserScope(Set<String> requestScope) throws DataException {
String userCode = ToBContext.getUserCode();
String userName = ToBContext.getUserName();
ResponseResult responseResult = new ResponseResult();
try {
responseResult = oauthFeign.findAllStationAndUserScope(userCode, userName);
}catch (Exception e){
throw new BusinessException(e.getMessage());
}
if (null == responseResult) {
return new HashSet<>();
}
if (!"ok".equals(responseResult.getMessage()) || !"000".equals(responseResult.getCode())) {
throw new DataException(responseResult.getCode(), "请登录,或无权限!");
}
if (!responseResult.getResult().containsKey("scopeStations")) {
return null;
}
List<StationDTO> scopeStations = JSONUtil.deserializeObject(JSONUtil.serializeObject(responseResult.getResult().get("scopeStations")), new TypeReference<List<StationDTO>>() {
});
Set<String> authorizeScope = scopeStations.stream().filter(StationDTO::getInScope).map(StationDTO::getStationCode).collect(Collectors.toSet());
if (CollectionUtils.isEmpty(authorizeScope)) {
return null;
} else if (CollectionUtils.isEmpty(requestScope) && !CollectionUtils.isEmpty(authorizeScope)) {
return authorizeScope;
}
// 移除不同的元素
requestScope.retainAll(authorizeScope);
return requestScope;
}
}
代码解释: findUserScope方法内的ToBContext.getUserCode()和ToBContext.getUserName()是从网关的上下文中取出当前访问这个接口的用户的usercode和username,让后使用取出来的code和name调用权限系统的获取用户站点权限的feign接口,调用后将返回的权限列表进行解析,首先使用JSONUtil工具类中的deserializeObject方法将返回的序列化对象转换为json字符串,然后使用serializeObject方法将json字符串转换成对应的对象(JSONUtil工具类代码在下面) 权限系统返回的是站点编码和一个Boolean值,Boolean值为true表示有权限,Boolean值为false表示没权限,使用stream流中的filter过滤掉所有为false的站点编码,只保留为true的站点编码。然后进行判断,如果没有为true的站点编码表示当前用户没有权限直接返回null,如果有为true的站点编码但是request中没有传站点编码,就直接返回所有状态为true的站点编码,如果request中也传了站点编码的,也有为true的站点编码,那么就取两个站点编码集合的交集。
StationDTO.class
@Data
public class StationDTO {
/**
* 标志
*/
private Boolean inScope;
/**
* 站点
*/
private String stationCode;
}
JSONUtil.class
public class JSONUtil {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MAPPER.registerModule(new JavaTimeModule());
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
private JSONUtil() {
}
/**
* 序列化对象为json字符串
*
* @param o
* @return
*/
public static String serializeObject(Object o) {
try {
return MAPPER.writeValueAsString(o);
} catch (JsonProcessingException e) {
throw new DataException("500", "json序列化出错:" + e.getMessage());
}
}
/**
* 反序列化
*
* @param s json字符串
* @param clazz 序列化后的对象的类
* @param <T>
* @return
*/
public static <T> T deserializeObject(String s, Class<T> clazz) {
try {
return MAPPER.readValue(s, clazz);
} catch (IOException e) {
throw new DataException("500", "json反序列化出错:" + e.getMessage());
}
}
/**
* 反序列化json对象
*
* @param s json字符串
* @return
*/
public static JsonNode deserializeObject(String s) {
try {
return MAPPER.readTree(s);
} catch (IOException e) {
throw new DataException("500", "json反序列化出错:" + e.getMessage());
}
}
/**
* 反系列化json字符串
*
* @param s json字符串
* @param typeReference 反序列化后的对象类信息
* @param <T>
* @return
*/
public static <T> T deserializeObject(String s, TypeReference<T> typeReference) {
try {
return MAPPER.readValue(s, typeReference);
} catch (IOException e) {
throw new DataException("500", "json反序列化出错:" + e.getMessage());
}
}
}
ParseSPEL.class
public class ParseSPEL {
/**
* 解析方式上的表达式
* key 定义在注解上,支持SPEL表达式
*
* @param key
* @param method
* @param args
* @return
*/
public static <T> T parseMethodKey(String key, Method method, Object[] args, Class<T> t) {
// 获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
// 使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
// SPEL 上下文
StandardEvaluationContext context = new StandardEvaluationContext();
// 把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
return parser.parseExpression(key).getValue(context, t);
}
/**
* 解析方式上的表达式
* key 定义在注解上,支持SPEL表达式
*
* @param key
* @param method
* @param args
* @return
*/
public static void setMethodValue(String key, Collection<String> values, Method method, Object[] args) {
// 获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
// 使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
// SPEL 上下文
StandardEvaluationContext context = new StandardEvaluationContext();
// 把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
parser.parseExpression(key).setValue(context, values);
}
}
}
代码解释:
parseMethodKey方法用于使用SPEL表达式从request中取出传过来的key的值setMethodValue方法用于使用SPEL表达式将传来的values写入到request对应的key中
更多推荐
所有评论(0)