最近有小伙伴在微信群里问SpringSecurity的权限注解:很多时候事情就是这么巧合。松哥最近在做的tienchin也是基于注解来处理权限问题,所以既然大家都有这个问题,那我们就一起来聊聊这个话题。当然,我不会讲一些基础知识。不熟悉SpringSecurity基本用法的朋友可以在公众号后台回复ss,有原创系列教程。1、具体用法首先我们看一下SpringSecurity权限注解的具体用法,如下:@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")@GetMapping("/list")publicTableDataInfogetChannelList(){startPage();Listlist=channelService.list();returngetDataTable(list);}同上,表示当前用户需要有tienchin:channel:query权限才能执行当前接口方法。所以要理解@PreAuthorize注解的原理,我觉得要从两个方面入手:第一,理解Spring中提供的SpEL。其次,理解SpringSecurity中方法注解的处理规则。让我们一一看看。2.SpELSpringExpressionLanguage(简称SpEL)是一种强大的表达式语言,支持查询和操作运行时对象导航图功能。它的语法类似于传统的EL,但提供了额外的功能,最显着的是简单字符串的函数调用和模板函数。SpEL为Spring社区提供了一种简单高效的表达语言,一种运行在整个Spring产品套件中的语言。这门语言的特性是基于Spring产品的需求而设计的,这是它出现的一大特点。虽然我们离不开Spring框架,但也离不开SpEL,因为它是如此的好用和强大,而且SpEL在整个Spring家族中也处于非常重要的地位。但很多时候,我们对它只知道一个大概的概念。其实,如果你系统地学习过SpEL,那么上面的SpringSecurity注解其实就很容易理解了。下面我通过一个简单的例子给大家简单介绍一下SpEL。为了省事,我创建一个SpringBoot工程给大家演示一下。创建的时候不需要额外添加任何依赖,只需要最基本的依赖即可。代码如下:StringexpressionStr="1+2";ExpressionParserparser=newSpelExpressionParser();Expressionexp=parser.parseExpression(expressionStr);expressionStr是一个自定义的表达式字符串,会被转换成解析成一个Expression,然后exp就可以执行了。有两种方法可以执行它。对于上面没有任何额外变量的,我们可以直接执行。直接执行的方式如下:Objectvalue=exp.getValue();System.out.println(value.toString());这样打印出3。记得群里有个朋友问要执行一个字符串表达式,但是他不知道怎么做。js中的eval函数很方便,我们Java中也有SpEL,也很方便。但是,在很多情况下,我们要执行的表达式可能会比较复杂,这时候上面的调用方式就不够用了。此时,我们可以为要调用的表达式设置一个上下文。这时候会用到EvaluationContext或者它的子类,如下:StandardEvaluationContextcontext=newStandardEvaluationContext();System.out.println(exp.getValue(context));当然上面的表达式不需要设置上下文,我举一个需要设置上下文的例子。比如我现在有一个User类,如下:publicclassUser{privateIntegerid;私有字符串用户名;私有字符串地址;//省略getter/setter}现在我的表达式是这样的:Useruser=newUser();user.setAddress("广州");user.setUsername("javaboy");user.setId(99);ctx.setVariable("user",user);Stringvalue=exp.getValue(ctx,String.class);System.out.println("value="+value);this表达式的意思是获取用户对象的用户名属性。以后创建一个user对象,放到StandardEvaluationContext中,根据这个对象执行表达式,就可以打印出想要的结果了。如果我们将用户对象设置为rootObject,则表达式中不需要用户,如下所示:newStandardEvaluationContext();Useruser=newUser();user.setAddress("广州");user.setUsername("javaboy");user.setId(99);ctx.setRootObject(user);Stringvalue=exp.getValue(ctx,String.class);System.out.println("值="+值);该表达式只是一个用户名字符串。以后执行的时候会自动从user中找到username的值并返回。当然,表达式也可以是方法。例如,我在User类中添加了以下两个方法:;}我们可以通过表达式调用这两个方法,如下:callsayHellowithparameters:Stringexpression="sayHello(99)";ExpressionParserparser=newSpelExpressionParser();Expressionexp=parser.parseExpression(expression);StandardEvaluationContextctx=newStandardEvaluationContext();Useruser=newUser();user.setAddress("广州");user.setUsername("javaboy");user.setId(99);ctx.setRootObject(user);Stringvalue=exp.getValue(ctx,String.class);System.out.println("value="+value);只需写上方法名并执行即可。调用无参的sayHello:Stringexpression="sayHello";ExpressionParserparser=newSpelExpressionParser();Expressionexp=parser.parseExpression(expression);StandardEvaluationContextctx=newStandardEvaluationContext();Useruser=newUser();user.setAddress("广州");user.setUsername("javaboy");user.setId(99);ctx.setRootObject(user);Stringvalue=exp.getValue(ctx,String.class);System.out.println("value="+value);这些就都好懂了。甚至,我们的表达式还可以涉及到Spring中的一个Bean。例如,我们向Spring注册以下Bean:@Service("us")publicclassUserService{publicStringsayHello(Stringname){return"hello"+name;}}然后通过SpEL表达式调用名为us的bean中的sayHello方法,如下:@AutowiredBeanFactorybeanFactory;@TestvoidcontextLoads(){Stringexpression="@us.sayHello('javaboy')";ExpressionParser解析器=newSpelExpressionParser();表达式exp=parser.parseExpression(expression);StandardEvaluationContextctx=newStandardEvaluationContext();ctx.setBeanResolver(新的BeanFactoryResolver(beanFactory));字符串值=exp.getValue(ctx,String.class);.out.println("value="+value);}为配置上下文设置一个beanresolver,这个beanresolver会自动按照名称从Spring容器中找到对应的bean并执行对应的方法。当然SpEL的玩法还有很多,我就不一一列举了。这里主要是为了让小伙伴们知道有这样一种技术,让大家了解@PreAuthorize注解的原理。3、@PreAuthorize接下来我们回到SpringSecurity来看@PreAuthorize注解。权限的实现方式有千百种,权限模型也多种多样。但是归结到代码上,无外乎两种:基于URL地址的权限处理和基于方法注解的权限处理。宋歌之前的vhr用的是前者。@PreAuthorize注解当然对应的是后者。我这次做的tienchin项目就是后者,我们来看一个例子:@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")@GetMapping("/list")publicTableDataInfogetChannelList(){首页();Listlist=channelService.list();returngetDataTable(list);}注解说起来容易,@ss.hasPermi('tienchin:channel:query')是什么意思?ss是Spring容器中Bean中的一个注册,对应的类位于org.javaboy.tienchin.framework.web.service.PermissionService中。很明显,hasPermi是这个类中的一个方法。这个hasPermi方法的逻辑其实很简单:publicbooleanhasPermi(Stringpermission){if(StringUtils.isEmpty(permission)){returnfalse;}LoginUserloginUser=SecurityUtils.getLoginUser();如果(StringUtils.isNull(loginUser)||CollectionUtils.isEmpty(loginUser.getPermissions())){返回false;}returnhasPermissions(loginUser.getPermissions(),permission);}privatebooleanhasPermissions(Setpermissions,Stringpermissions){returnpermissions.contains(ALL)_PERMISSI||}permissions.contains(StringUtils.trim(permission));}这个判断逻辑很简单,就是获取当前登录用户,判断当前登录用户的权限集是否有需要的权限当前请求。具体的判断逻辑没什么好说的,就是看集合中是否存在某个字符串。那么这个方法是在哪里调用的呢?大家知道,SpringSecurity中处理权限的过滤器是FilterSecurityInterceptor,所有的权限处理最终都会来到这个过滤器。在这个过滤器中,会用到voter、voters等各种工具。我不会在这里详细介绍。之前的SpringSecurity系列教程中都有详细介绍。在voter中,我们可以看到专门处理@PreAuthorize注解的类Pr??eInvocationAuthorizationAdviceVoter。我们看一下里面的核心方法:@Overridepublicintvote(Authenticationauthentication,MethodInvocationmethod,Collectionattributes){PreInvocationAttributepreAttr=findPreInvocationAttribute(attributes);如果(preAttr==null){返回ACCESS_ABSTAIN;}返回this.preAdvice.before(authentication,method,preAttr)?ACCESS_GRANTED:ACCESS_DENIED;}框架的源码写的很好,看名字就知道你要做什么!这里是最后一句,调用一个Advice预先通知判断权限是否满足:publicbooleanbefore(Authenticationauthentication,MethodInvocationmi,PreInvocationAttributeattr){EvaluationContextctx=this.expressionHandler.createEvaluationContext(authentication,mi);表达式preFilter=preAttr.getFilterExpression();表达式preAuthorize=preAttr.getAuthorizeExpression();if(preFilter!=null){ObjectfilterTarget=findFilterTarget(preAttr.getFilterTarget(),ctx,mi);this.expressionHandler.filter(filterTarget,preFilter,ctx);}返回(预授权!=null)?ExpressionUtils.evaluateAsBoolean(preAuthorize,ctx):true;}现在当你看到这个before方法时,你应该感到熟悉。首先,获取preAttr对象,它实际上将内容存储在您的@PreAuthorize注释中。接下来跟进当前登录用户信息认证创建context对象,此时创建的context对象包含当前用户有哪些权限。获取过滤器(在我们的项目中没有)。获取权限注解。最后执行表达式检查当前用户权限是否包含请求所需要的权限。就这样,是不是很简单?