技术背景

当前,我们基于导航菜单的显示和操作按钮的禁用状态,实现了页面可见性和操作可用性的权限验证,或者叫访问控制。但这仅限于页面的显示和操作,我们的后台接口还是没有进行权限的验证,只要知道了后台的接口信息,就可以直接通过swagger或自行发送ajax请求成功调用后台接口,这是非常危险的。接下来,我们就基于Shiro的注解式权限控制方案,来给我们的后台接口提供权限保护。

权限注解

Shiro总共有5个权限注解,实现了不同的权限控制策略。

RequiresPermissions

当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。

这是基于资源权限方式的权限控制主要方案,也是我们项目中进行权限控制使用的注解方案。

RequiresRoles

当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。

RequiresUser

当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

RequiresAuthentication

使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。

RequiresGuest

使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。

注解优先级

Shiro的认证注解处理具有内定处理顺序,如有多个注解,会按照下面优先级逐个检查,只有所有检查通过才允许访问:

  • RequiresRoles
  • RequiresPermissions
  • RequiresAuthentication
  • RequiresUser
  • RequiresGuest

代码实现

添加配置

打开kitty-admin工程,找到shiro配置类。添加如下内容,主要作用是开启Shiro的权限注解。

Shiro通过AOP方式拦截被权限注解的类或方法,然后匹配权限注解值和用户权限列表进行验证。

ShiroConfig.java

  1. /**
  2. * Shiro生命周期处理器
  3. */
  4. @Bean
  5. public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
  6. return new LifecycleBeanPostProcessor();
  7. }
  8.  
  9. /**
  10. * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
  11. * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
  12. */
  13. @Bean
  14. @DependsOn({"lifecycleBeanPostProcessor"})
  15. public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
  16. DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
  17. advisorAutoProxyCreator.setProxyTargetClass(true);
  18. return advisorAutoProxyCreator;
  19. }
  20.  
  21. @Bean
  22. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
  23. AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
  24. authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
  25. return authorizationAttributeSourceAdvisor;
  26. }

添加注解

以菜单管理接口为例,添加 @RequiresPermissions("权限标识") 标识即可。

这个权限标识就是我们的菜单表中对应的权限标识字段(perms)对应的值。

SysMenuController.java

  1. package com.louis.kitty.admin.controller;
  2.  
  3. import java.util.List;
  4.  
  5. import org.apache.shiro.authz.annotation.RequiresPermissions;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.web.bind.annotation.GetMapping;
  8. import org.springframework.web.bind.annotation.PostMapping;
  9. import org.springframework.web.bind.annotation.RequestBody;
  10. import org.springframework.web.bind.annotation.RequestMapping;
  11. import org.springframework.web.bind.annotation.RequestParam;
  12. import org.springframework.web.bind.annotation.RestController;
  13.  
  14. import com.louis.kitty.admin.model.SysMenu;
  15. import com.louis.kitty.admin.sevice.SysMenuService;
  16. import com.louis.kitty.core.http.HttpResult;
  17.  
  18. /**
  19. * 菜单控制器
  20. * @author Louis
  21. * @date Oct 29, 2018
  22. */
  23. @RestController
  24. @RequestMapping("menu")
  25. public class SysMenuController {
  26.  
  27. @Autowired
  28. private SysMenuService sysMenuService;
  29.  
  30. @RequiresPermissions({"sys:menu:add", "sys:menu:edit"})
  31. @PostMapping(value="/save")
  32. public HttpResult save(@RequestBody SysMenu record) {
  33. return HttpResult.ok(sysMenuService.save(record));
  34. }
  35.  
  36. @RequiresPermissions("sys:menu:delete")
  37. @PostMapping(value="/delete")
  38. public HttpResult delete(@RequestBody List<SysMenu> records) {
  39. return HttpResult.ok(sysMenuService.delete(records));
  40. }
  41.  
  42. @RequiresPermissions("sys:menu:view")
  43. @GetMapping(value="/findNavTree")
  44. public HttpResult findNavTree(@RequestParam String userName) {
  45. return HttpResult.ok(sysMenuService.findTree(userName, 1));
  46. }
  47.  
  48. @RequiresPermissions("sys:menu:view")
  49. @GetMapping(value="/findMenuTree")
  50. public HttpResult findMenuTree() {
  51. return HttpResult.ok(sysMenuService.findTree(null, 0));
  52. }
  53. }

测试效果

启动服务,通过Swagger分别使用超级管理员和测试人员角色账户访问接口,发现admin可以正常访问,无权限的账户访问返回如下权限验证失败信息。

  1. {
  2. "timestamp": "2018-11-19T07:58:21.532+0000",
  3. "status": 500,
  4. "error": "Internal Server Error",
  5. "message": "Subject does not have permission [sys:menu:view]",
  6. "path": "/menu/findMenuTree"
  7. }

原理剖析

首先在Shiro配置的时候,我们配置了一个 AuthorizationAttributeSourceAdvisor 类。

  1. /**
  2. * Shiro生命周期处理器
  3. */
  4. @Bean
  5. public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
  6. return new LifecycleBeanPostProcessor();
  7. }
  8.  
  9. /**
  10. * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
  11. * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
  12. */
  13. @Bean
  14. @DependsOn({"lifecycleBeanPostProcessor"})
  15. public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
  16. DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
  17. advisorAutoProxyCreator.setProxyTargetClass(true);
  18. return advisorAutoProxyCreator;
  19. }
  20.  
  21. @Bean
  22. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
  23. AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
  24. authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
  25. return authorizationAttributeSourceAdvisor;
  26. }

在 AuthorizationAttributeSourceAdvisor 类中,我们看到了有关五个权限注解的信息,以及关联一个拦截器 AopAllianceAnnotationsAuthorizingMethodInterceptor。

  1. public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES = new Class[] {
  2. RequiresPermissions.class, RequiresRoles.class,
  3. RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
  4. };
  5.  
  6.    ...
  7. public AuthorizationAttributeSourceAdvisor() {
  8. setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
  9. }
  10. }

在 AopAllianceAnnotationsAuthorizingMethodInterceptor 中,我们看到了关联了五种权限控制注解对象的拦截器,这样在添加了权限注解的方法被调用时,就会被对应的拦截器拦截,并进行相关的权限验证。

  1. public class AopAllianceAnnotationsAuthorizingMethodInterceptor
  2. extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor {
  3.  
  4. public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
  5. List<AuthorizingAnnotationMethodInterceptor> interceptors =
  6. new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
  7. //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
  8. //raw JDK resolution process.
  9. AnnotationResolver resolver = new SpringAnnotationResolver();
  10. //we can re-use the same resolver instance - it does not retain state:
  11. interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
  12. interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
  13. interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
  14. interceptors.add(new UserAnnotationMethodInterceptor(resolver));
  15. interceptors.add(new GuestAnnotationMethodInterceptor(resolver));
  16.  
  17. setMethodInterceptors(interceptors);
  18. }

接口被调用时,AOP拦截器 AopAllianceAnnotationsAuthorizingMethodInterceptor 的invoke方法被调用。

  1. public Object invoke(MethodInvocation methodInvocation) throws Throwable {
  2. org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation);
  3. return super.invoke(mi);
  4. }

调用父类 AuthorizingMethodInterceptor 的 invoke 方法。

  1. public Object invoke(MethodInvocation methodInvocation) throws Throwable {
  2. assertAuthorized(methodInvocation);
  3. return methodInvocation.proceed();
  4. }

调用 AopAllianceAnnotationsAuthorizingMethodInterceptor 的 assertAuthorized 方法。

  1. protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {
  2. //default implementation just ensures no deny votes are cast:
  3. Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
  4. if (aamis != null && !aamis.isEmpty()) {
  5. for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
  6. if (aami.supports(methodInvocation)) {
  7. aami.assertAuthorized(methodInvocation);
  8. }
  9. }
  10. }
  11. }

调用 AuthorizingAnnotationMethodInterceptor 的 assertAuthorized 方法。

  1. public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
  2. try {
  3. ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));
  4. }
  5. catch(AuthorizationException ae) {
  6. ...
  7. }
  8. }

调用 PermissionAnnotationHandler 的 assertAuthorized 方法。

  1. public void assertAuthorized(Annotation a) throws AuthorizationException {
  2. if (!(a instanceof RequiresPermissions)) return;
  3.  
  4. RequiresPermissions rpAnnotation = (RequiresPermissions) a;
  5. String[] perms = getAnnotationValue(a);
  6. Subject subject = getSubject();
  7.  
  8. if (perms.length == 1) {
  9. subject.checkPermission(perms[0]);
  10. return;
  11. }
  12. ...
  13. }

调用 DelegatingSubject  的 checkPermission方法。

  1. public void checkPermission(String permission) throws AuthorizationException {
  2. assertAuthzCheckPossible();
  3. securityManager.checkPermission(getPrincipals(), permission);
  4. }

调用 AuthorizingSecurityManager 的 checkPermission方法。

  1. public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException {
  2. this.authorizer.checkPermission(principals, permission);
  3. }

调用 ModularRealmAuthorizer 的 checkPermission方法。

  1. public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException {
  2. assertRealmsConfigured();
  3. if (!isPermitted(principals, permission)) {
  4. throw new UnauthorizedException("Subject does not have permission [" + permission + "]");
  5. }
  6. }
  1. public boolean isPermitted(PrincipalCollection principals, String permission) {
  2. assertRealmsConfigured();
  3. for (Realm realm : getRealms()) {
  4. if (!(realm instanceof Authorizer)) continue;
  5. if (((Authorizer) realm).isPermitted(principals, permission)) {
  6. return true;
  7. }
  8. }
  9. return false;
  10. }

调用 AuthorizingRealm 的 isPermitted方法。

  1. public boolean isPermitted(PrincipalCollection principals, String permission) {
  2. Permission p = getPermissionResolver().resolvePermission(permission);
  3. return isPermitted(principals, p);
  4. }
  1. public boolean isPermitted(PrincipalCollection principals, Permission permission) {
  2. AuthorizationInfo info = getAuthorizationInfo(principals);
  3. return isPermitted(permission, info);
  4. }
  1. protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
  2.  
  3.      ...
  4.  
  5. if (info == null) {
  6. // Call template method if the info was not found in a cache
  7. info = doGetAuthorizationInfo(principals);
  8.        ...
  9. }
  10. return info;
  11. }

调用我们自定义的 OAuth2Realm 的 doGetAuthorizationInfo 方法,也是返回自定义权限验证的逻辑。

  1. /**
  2. * 授权(接口保护,验证接口调用权限时调用)
  3. */
  4. @Override
  5. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  6. SysUser user = (SysUser)principals.getPrimaryPrincipal();
  7. // 用户权限列表,根据用户拥有的权限标识与如 @permission标注的接口对比,决定是否可以调用接口
  8. Set<String> permsSet = sysUserService.findPermissions(user.getName());
  9. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  10. info.setStringPermissions(permsSet);
  11. return info;
  12. }

AuthorizingRealm 查询到用户权限信息,将注解权限值跟用户权限信息列表进行匹配,决定权限验证是否通过。

  1. protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
  2. Collection<Permission> perms = getPermissions(info);
  3. if (perms != null && !perms.isEmpty()) {
  4. for (Permission perm : perms) {
  5. if (perm.implies(permission)) {
  6. return true;
  7. }
  8. }
  9. }
  10. return false;
  11. }

到这里,关于Shiro注解式权限控制方案的配置和执行流程就剖析的差不多了。

Spring Boot + Spring Cloud 实现权限管理系统 权限控制(Shiro 注解)的更多相关文章

  1. 新书上线:《Spring Boot+Spring Cloud+Vue+Element项目实战:手把手教你开发权限管理系统》,欢迎大家买回去垫椅子垫桌脚

    新书上线 大家好,笔者的新书<Spring Boot+Spring Cloud+Vue+Element项目实战:手把手教你开发权限管理系统>已上线,此书内容充实.材质优良,乃家中必备垫桌脚 ...

  2. [权限管理系统(四)]-spring boot +spring security短信认证+redis整合

    [权限管理系统]spring boot +spring security短信认证+redis整合   现在主流的登录方式主要有 3 种:账号密码登录.短信验证码登录和第三方授权登录,前面一节Sprin ...

  3. Spring Boot(十四):spring boot整合shiro-登录认证和权限管理

    Spring Boot(十四):spring boot整合shiro-登录认证和权限管理 使用Spring Boot集成Apache Shiro.安全应该是互联网公司的一道生命线,几乎任何的公司都会涉 ...

  4. 部署spring boot + Vue遇到的坑(权限、刷新404、跨域、内存)

    部署spring boot + Vue遇到的坑(权限.刷新404.跨域.内存) 项目背景是采用前后端分离,前端使用vue,后端使用springboot. 工具 工欲善其事必先利其器,我们先找一个操作L ...

  5. spring Boot+spring Cloud实现微服务详细教程第二篇

    上一篇文章已经说明了一下,关于spring boot创建maven项目的简单步骤,相信很多熟悉Maven+Eclipse作为开发常用工具的朋友们都一目了然,这篇文章主要讲解一下,构建spring bo ...

  6. spring Boot+spring Cloud实现微服务详细教程第一篇

    前些天项目组的大佬跟我聊,说项目组想从之前的架构上剥离出来公用的模块做微服务的开发,恰好去年的5/6月份在上家公司学习了国内开源的dubbo+zookeeper实现的微服务的架构.自己平时对微服务的设 ...

  7. Cola Cloud 基于 Spring Boot, Spring Cloud 构建微服务架构企业级开发平台

    Cola Cloud 基于 Spring Boot, Spring Cloud 构建微服务架构企业级开发平台: https://gitee.com/leecho/cola-cloud

  8. spring boot、cloud v2.1.0.RELEASE 使用及技术整理

    2018年10月30日 springboot v2.1.0.RELEASE 发布: https://github.com/spring-projects/spring-boot/releases/ta ...

  9. Spring Boot/Spring Cloud、ESB、Dubbo

    如何使用Spring Boot/Spring Cloud 实现微服务应用spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理.服务发现. ...

  10. 使用Spring Boot,Spring Cloud和Docker实现微服务架构

    https://github.com/sqshq/PiggyMetrics     Microservice Architecture with Spring Boot, Spring Cloud a ...

随机推荐

  1. XMLHttpRequest请求被劫持

    十几个请求中随机一个转到 <html><head><script language="javascript">setTimeout(" ...

  2. phpstorm----------phpstorm2017基本使用

    1.关闭2017版本的,函数参数提示.关闭方式如下: 2.如何设置代码里面的变量等号对齐,和key => value 对齐     ctrl+alt+l 3.修改PHP文件类创建的默认注释 4. ...

  3. 下拉选择插件select2赋值、创建、清空

    在select2中,设置指定值为选中状态 $("#select2_Id").val("XXXXX").select2()或者$("#latnId&qu ...

  4. 2014西安赛区C题

    将A[i]同他后面比他小的建边,然后求最大密度子图 #include <iostream> #include <algorithm> #include <string.h ...

  5. sparse-table模板

    预处理: void init(int n) { ;i < n;i++) { dp[i][] = a[i]; } int bitn = (int)(log(n)/log(2.0)); ;j < ...

  6. JSOIWC2019游记

    世除我WC...都去广二了qaq,就我还在nj ycs至少也去了pkuwc啊 这个JSOIWC2019的内容看起来很水,进入条件简单,但窝啥都不会,肯定垫底 内容清单: 1.26 上午听机房dalao ...

  7. kettle 连接 Oracle 异常

    场景重现 新安装的 kettle(pdi-ce-7.0.0.0-25) 连接 Oracle 11G R2 报错如下: 解决办法 到 Oracle 官网 JDBC Downloads 下载对应的 ojd ...

  8. 线上问题排查(2)——JDK内置工具

    https://www.cnblogs.com/keanuyaoo/p/3253663.html 常用命令目录: jps命令(Java Virtual Machine Process Status T ...

  9. Vue常见组件

    每一个组件都是一个vue实例 每个组件均具有自身的模板template,根组件的模板就是挂载点 每个组件模板只能拥有一个根标签 子组件的数据具有作用域,以达到组件的复用 根组件 <div id= ...

  10. Lintcode97-Maximum Depth of Binary Tree-Easy

    97. Maximum Depth of Binary Tree Given a binary tree, find its maximum depth. The maximum depth is t ...