• Spring Boot + Spring Cloud 实现权限管理系统 权限控制(Shiro 注解)


    技术背景

    当前,我们基于导航菜单的显示和操作按钮的禁用状态,实现了页面可见性和操作可用性的权限验证,或者叫访问控制。但这仅限于页面的显示和操作,我们的后台接口还是没有进行权限的验证,只要知道了后台的接口信息,就可以直接通过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

    复制代码
        /**
         * Shiro生命周期处理器
         */
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
        
        /**
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
         * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
         */
        @Bean
        @DependsOn({"lifecycleBeanPostProcessor"})
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
            return authorizationAttributeSourceAdvisor;
        }
    复制代码

    添加注解

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

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

    SysMenuController.java

    复制代码
    package com.louis.kitty.admin.controller;
    
    import java.util.List;
    
    import org.apache.shiro.authz.annotation.RequiresPermissions;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.louis.kitty.admin.model.SysMenu;
    import com.louis.kitty.admin.sevice.SysMenuService;
    import com.louis.kitty.core.http.HttpResult;
    
    /**
     * 菜单控制器
     * @author Louis
     * @date Oct 29, 2018
     */
    @RestController
    @RequestMapping("menu")
    public class SysMenuController {
    
        @Autowired
        private SysMenuService sysMenuService;
        
        @RequiresPermissions({"sys:menu:add", "sys:menu:edit"})
        @PostMapping(value="/save")
        public HttpResult save(@RequestBody SysMenu record) {
            return HttpResult.ok(sysMenuService.save(record));
        }
    
        @RequiresPermissions("sys:menu:delete")
        @PostMapping(value="/delete")
        public HttpResult delete(@RequestBody List<SysMenu> records) {
            return HttpResult.ok(sysMenuService.delete(records));
        }
    
        @RequiresPermissions("sys:menu:view")
        @GetMapping(value="/findNavTree")
        public HttpResult findNavTree(@RequestParam String userName) {
            return HttpResult.ok(sysMenuService.findTree(userName, 1));
        }
        
        @RequiresPermissions("sys:menu:view")
        @GetMapping(value="/findMenuTree")
        public HttpResult findMenuTree() {
            return HttpResult.ok(sysMenuService.findTree(null, 0));
        }
    }
    复制代码

    测试效果

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

    复制代码
    {
      "timestamp": "2018-11-19T07:58:21.532+0000",
      "status": 500,
      "error": "Internal Server Error",
      "message": "Subject does not have permission [sys:menu:view]",
      "path": "/menu/findMenuTree"
    }
    复制代码

    原理剖析

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

    复制代码
        /**
         * Shiro生命周期处理器
         */
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
        
        /**
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
         * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
         */
        @Bean
        @DependsOn({"lifecycleBeanPostProcessor"})
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
            return authorizationAttributeSourceAdvisor;
        }
    复制代码

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

    复制代码
    public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES = new Class[] {
                        RequiresPermissions.class, RequiresRoles.class,
                        RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
                };
    
       ...
    public AuthorizationAttributeSourceAdvisor() { setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor()); } }
    复制代码

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

    复制代码
    public class AopAllianceAnnotationsAuthorizingMethodInterceptor
            extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor {
    
        public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
            List<AuthorizingAnnotationMethodInterceptor> interceptors =
                    new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
            //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
            //raw JDK resolution process.
            AnnotationResolver resolver = new SpringAnnotationResolver();
            //we can re-use the same resolver instance - it does not retain state:
            interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
            interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
            interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
            interceptors.add(new UserAnnotationMethodInterceptor(resolver));
            interceptors.add(new GuestAnnotationMethodInterceptor(resolver));
    
            setMethodInterceptors(interceptors);
        }
    复制代码

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

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

    调用父类 AuthorizingMethodInterceptor 的 invoke 方法。

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

    调用 AopAllianceAnnotationsAuthorizingMethodInterceptor 的 assertAuthorized 方法。

    复制代码
        protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {
            //default implementation just ensures no deny votes are cast:
            Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
            if (aamis != null && !aamis.isEmpty()) {
                for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
                    if (aami.supports(methodInvocation)) {
                        aami.assertAuthorized(methodInvocation);
                    }
                }
            }
        }
    复制代码

    调用 AuthorizingAnnotationMethodInterceptor 的 assertAuthorized 方法。

    复制代码
      public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
            try {
                ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));
            }
            catch(AuthorizationException ae) {
                ...
            }         
        }
    复制代码

    调用 PermissionAnnotationHandler 的 assertAuthorized 方法。

    复制代码
        public void assertAuthorized(Annotation a) throws AuthorizationException {
            if (!(a instanceof RequiresPermissions)) return;
    
            RequiresPermissions rpAnnotation = (RequiresPermissions) a;
            String[] perms = getAnnotationValue(a);
            Subject subject = getSubject();
    
            if (perms.length == 1) {
                subject.checkPermission(perms[0]);
                return;
            }
            ...
        }
    复制代码

    调用 DelegatingSubject  的 checkPermission方法。

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

    调用 AuthorizingSecurityManager 的 checkPermission方法。

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

    调用 ModularRealmAuthorizer 的 checkPermission方法。

        public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException {
            assertRealmsConfigured();
            if (!isPermitted(principals, permission)) {
                throw new UnauthorizedException("Subject does not have permission [" + permission + "]");
            }
        }
    复制代码
        public boolean isPermitted(PrincipalCollection principals, String permission) {
            assertRealmsConfigured();
            for (Realm realm : getRealms()) {
                if (!(realm instanceof Authorizer)) continue;
                if (((Authorizer) realm).isPermitted(principals, permission)) {
                    return true;
                }
            }
            return false;
        }
    复制代码

    调用 AuthorizingRealm 的 isPermitted方法。

        public boolean isPermitted(PrincipalCollection principals, String permission) {
            Permission p = getPermissionResolver().resolvePermission(permission);
            return isPermitted(principals, p);
        }
        public boolean isPermitted(PrincipalCollection principals, Permission permission) {
            AuthorizationInfo info = getAuthorizationInfo(principals);
            return isPermitted(permission, info);
        }
    复制代码
        protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
    
          ...
    
            if (info == null) {
                // Call template method if the info was not found in a cache
                info = doGetAuthorizationInfo(principals);
           ... } return info; }
    复制代码

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

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

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

    复制代码
        protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
            Collection<Permission> perms = getPermissions(info);
            if (perms != null && !perms.isEmpty()) {
                for (Permission perm : perms) {
                    if (perm.implies(permission)) {
                        return true;
                    }
                }
            }
            return false;
        }
    复制代码

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

  • 相关阅读:
    更新主窗口控件的内容1:子线程工作时同时更新主线程内的控件内容
    静态类和非静态类中静态变量
    js $的扩展写法
    js 获取时区
    export to excel
    使用Sqlserver事务发布实现数据同步(zhuanqian)
    json to entity in api
    automapper demo
    autoMapper的介绍
    Springboot的热部署
  • 原文地址:https://www.cnblogs.com/7788IT/p/10693138.html
Copyright © 2020-2023  润新知