对于在Springboot中,利用自定义注解+切面来实现接口权限的控制这个大家应该都很熟悉,也有大量的博客来介绍整个的实现过程,整体来说思路如下:
- 自定义一个权限校验的注解,包含参数value
- 配置在对应的接口上
- 定义一个切面类,指定切点
- 在切入的方法体里写上权限判断的逻辑
乍一看,没毛病,学到了,学到了~,收藏起来。但是呢,等到实际用到的时候就傻眼了,为什么呢?在实际的开发中,你会发现,对于权限校验的需求场景是很多的,比如:
- 只要配置了任何角色,就可以访问
- 有某个权限就可以访问
- 放行所有请求
- 只有超级管理员角色才可以访问
- 只有登录后才可以访问
- 在指定时间段内可以访问
- 有某个角色的情况下才可以访问
- 同时具有指定的多个角色情况下才可以访问
- 等
傻眼了不,按照上面的实现逻辑的话怎么搞?加注解?写各种判断?这时候,其实我们就可以通过SpEL表达式来帮我们处理这个问题。
Spring Security实现
Spring Security已经帮我们实现了如何通过注解,直接上例子:
java
@PreAuthorize("@pms.hasPermission('1-6')")
@ApiOperation("查 询")
@GetMapping
public PageResponse<RoleCO> pageRole(RolePageQry qry){
return roleService.listPage(qry);
}
通过@PreAuthorize("@pms.hasPermission('1-6')")
实现了访问该接口,需要有1-6
这个权限。我们需要实现pms
java
public interface Pms {
/**
* 判断接口是否有任意xxx,xxx权限
*
* @param permission 权限
* @return {boolean}
*/
default boolean hasPermission(String permission) {
if (StrUtil.isBlank(permission)) {
return false;
}
return listPermissions().contains(permission);
}
//
default boolean hasAnyPermission(String... permissions) {
if (CollectionUtil.isEmpty(Arrays.asList(permissions))) {
return false;
}
return listPermissions().stream().filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
}
List<String> listPermissions();
}
java
@Data
public class Pms implements com.jjb.saas.framework.api.system.Pms {
/**
* 菜单下有哪些按钮权限
*
* @param
* @return {boolean}
*/
@Override
public List<String> listPermissions() {
MultiResponse multiResponse = FacadeServiceFactory.getInstance().genericInvoke("com.jjb.saas.system.client.permsgroup.facade.PermsGroupFacade", "listPermssionsByAppKey", MultiResponse.class);
if (multiResponse.isSuccess()) {
List datas = multiResponse.getData();
if (ObjectUtil.isNotEmpty(datas)) {
return datas;
}
return Collections.emptyList();
}
return Collections.emptyList();
}
}
注入到spring中
java
@ComponentScan
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
@AllArgsConstructor
public class SecurityConfiguration {
@Bean("pms")
@ConditionalOnMissingBean
public Pms pms() {
Pms pms = new Pms();
return pms;
}
}
接下来,我们就来看看如何自己造轮子
SpEL表达式
提到SpEL,那么到底SpEL是啥呢?
SpEL的全称为Spring Expression Language,即Spring表达式语言。是Spring3.0提供的。他最强大的功能是可以通过运行期间执行的表达式将值装配到我们的属性或构造函数之中。
如果有小伙伴之前没有接触过,不太理解这句话的含义,那么不要紧,继续往下看,通过后续的实践你就能明白他的作用了。
造轮子
自定义注解
当然,万变不离其宗,自定义注解我们还是需要滴。这里呢,我们仅需要定义一个value属性用于接收表达式即可。
java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {
/**
*
*
* permissionAll()-----只要配置了角色就可以访问
* hasPermission("MENU.QUERY")-----有MENU.QUERY操作权限的角色可以访问
* permitAll()-----放行所有请求
* denyAll()-----只有超级管理员角色才可访问
* hasAuth()-----只有登录后才可访问
* hasTimeAuth(1,,10)-----只有在1-10点间访问
* hasRole('管理员')-----具有管理员角色的人才能访问
* hasAllRole('管理员','总工程师')-----同时具有管理员、总工程师角色的人才能访问
*
* Spring el
* 文档地址:https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#expressions
*/
String value();
}
定义切面
注解定义好了,我们就需要定义切面了。这里要考虑一个点。我们希望的是如果方法上有注解,则对方法进行限制,若方法上无注解,单是类上有注解,那么类上的权限注解对该类下所有的接口生效。因此,我们切点的话要用@within注解。代码如下:
java
@Around(
"@annotation(PreAuth注解路径) || " +
"@within(PreAuth注解路径)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
if (handleAuth(point)) {
return point.proceed();
}
throw new SecureException(ResultCode.REQ_REJECT);
}
private boolean handleAuth(ProceedingJoinPoint point) {
//TODO 逻辑判断,返回true or false
}
权限校验
关键点来了。这里我们要引入SpEL。
首先,引入SpEL:
java
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
然后,从注解上获取我们需要的表达式:
java
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
Method method = ms.getMethod();
// 读取权限注解,优先方法上,没有则读取类
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 判断表达式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
//TODU 表达式解析
}
表达式解析
java
private boolean handleAuth(ProceedingJoinPoint point) {
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
Method method = ms.getMethod();
// 读取权限注解,优先方法上,没有则读取类
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 判断表达式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
Expression expression = EXPRESSION_PARSER.parseExpression(condition);
// 方法参数值
Object[] args = point.getArgs();
StandardEvaluationContext context = getEvaluationContext(method, args);
//获取解析计算的结果
return expression.getValue(context, Boolean.class);
}
return false;
}
/**
* 获取方法上的参数
*
* @param method 方法
* @param args 变量
* @return {SimpleEvaluationContext}
*/
private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
// 初始化Sp el表达式上下文,并设置 AuthFun
StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
// 设置表达式支持spring bean
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
for (int i = 0; i < args.length; i++) {
// 读取方法参数
MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
// 设置方法 参数名和值 为spel变量
context.setVariable(methodParam.getParameterName(), args[i]);
}
return context;
}
自定义解析方法
看完上面的解析处理是不是很蒙蔽,只看到了获取表达式,获取参数,设置参数,然后expression.getValue
就完事了。有的同学会问,你权限校验的逻辑呢?
别急,关键点在这:StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
在上文代码中找到了吧。这个AuthFun就是我们进行权限校验的对象。
所以呢,我们还得在定义一下这个对象。进行具体的权限校验逻辑处理,这里定的每一个方法都可以作为表达式在权限注解中使用。代码如下:
java
public class AuthFun {
/**
* 判断角色是否具有接口权限
*
* @return {boolean}
*/
public boolean permissionAll() {
//TODO
}
/**
* 判断角色是否具有接口权限
*
* @param permission 权限编号,对应菜单的MENU_CODE
* @return {boolean}
*/
public boolean hasPermission(String permission) {
//TODO
}
/**
* 放行所有请求
*
* @return {boolean}
*/
public boolean permitAll() {
return true;
}
/**
* 只有超管角色才可访问
*
* @return {boolean}
*/
public boolean denyAll() {
return hasRole(RoleConstant.ADMIN);
}
/**
* 是否已授权
*
* @return {boolean}
*/
public boolean hasAuth() {
if(Func.isEmpty(AuthUtil.getUser())){
// TODO 返回异常提醒
}else{
return true;
}
}
/**
* 是否有时间授权
*
* @param start 开始时间
* @param end 结束时间
* @return {boolean}
*/
public boolean hasTimeAuth(Integer start, Integer end) {
Integer hour = DateUtil.hour();
return hour >= start && hour <= end;
}
/**
* 判断是否有该角色权限
*
* @param role 单角色
* @return {boolean}
*/
public boolean hasRole(String role) {
return hasAnyRole(role);
}
/**
* 判断是否具有所有角色权限
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAllRole(String... role) {
for (String r : role) {
if (!hasRole(r)) {
return false;
}
}
return true;
}
/**
* 判断是否有该角色权限
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAnyRole(String... role) {
//获取当前登录用户
BladeUser user = AuthUtil.getUser();
if (user == null) {
return false;
}
String userRole = user.getRoleName();
if (StringUtil.isBlank(userRole)) {
return false;
}
String[] roles = Func.toStrArray(userRole);
for (String r : role) {
if (CollectionUtil.contains(roles, r)) {
return true;
}
}
return false;
}
}
实际使用
在使用的时候,我们只需要在类上或者接口上,加上@PreAuth的直接,value值写的时候要注意一下,value应该是我们在AuthFun类中定义的方法和参数,如我们定义了解析方法hasAllRole(String... role)
,那么在注解中,我们就可以这样写@PreAuth("hasAllRole('角色1','角色2')")
,需要注意的是,参数要用单引号包括。
java
@PreAuth("hasPermission('LM_QUERY,LM_QUERY_ALL')")
public T 接口名称....
原理
根据上面的实际使用,可以看到。SpEL表达式解析将我们注解中的"hasAllRole('角色1','角色2')
"这样的字符串,给动态解析为了hasAllRole(参数1,参数1)
,并调用我们注册类中同名的方法。
总结
通过SpEL的使用,让我们的权限配置校验更加灵活。当出现新的场景时,我们仅需要在自定的表达式解析类中增加对应场景的解析方法即可。相对于之前的实现方式,这不得不说是更好的一个选择。