业务场景

       首先说一下使用的场景,整个项目是有权限控制的,各个角色都有不同的权限,现在这个接口就是一个分页查询借阅记录的一个接口,但是业务要求是这样,每个门店的店长只能看到自己门店的借阅记录。线下所有的门店通称为站点,每个门店(站点)都有自己唯一的站点编码,在后台权限系统中,可以对某一个角色进行站点权限的分配,比如站点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中

更多推荐