专栏首页朝雨忆轻尘Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十四):权限控制(Shiro 注解)

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十四):权限控制(Shiro 注解)

在线演示

演示地址:http://139.196.87.48:9002/kitty

用户名:admin 密码:admin

技术背景

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

源码下载

后端:https://gitee.com/liuge1988/kitty

前端:https://gitee.com/liuge1988/kitty-ui.git


作者:朝雨忆轻尘 出处:https://www.cnblogs.com/xifengxiaoma/  版权所有,欢迎转载,转载请注明原文作者及出处。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java实现动态代理的两种方式

    Java领域中,常用的动态代理实现方式有两种,一种是利用JDK反射机制生成代理,另外一种是使用CGLIB代理。

    朝雨忆轻尘
  • Vue笔记:VS Code 常用快捷键

    1、Auto Rename Tag 修改 html 标签,自动帮你完成尾部闭合标签的同步修改,和 webstorm 一样。

    朝雨忆轻尘
  • 消息队列常用应用场景介绍

    消息队列作为分布式系统中重要的组件,可以解决应用耦合,异步消息,流量削锋等系列问题

    朝雨忆轻尘
  • 电脑的 ip 是怎么来的呢?我又没有配置过

    显然,这里有两种配置方式,一种是自动获取 ip 地址,一种是我们手动来设置,我相信大部分人都是通过自动获取的方式来得到 ip 的,那么问题来了,它是如何自动获得...

    帅地
  • Java基于ssm框架的restful应用开发

    好几年都没写过java的应用了,这里记录下使用java ssm框架、jwt如何进行rest应用开发,文中会涉及到全局异常拦截处理、jwt校验、token拦截器等...

    用户1141560
  • Go 语言错误及异常处理篇(三):panic 和 recover

    前面学院君介绍了 Go 语言通过 error 接口统一进行错误处理,但这些错误都是我们在编写代码时就已经预见并返回的,对于某些运行时错误,比如数组越界、除数为0...

    学院君
  • 大数据面试问题

    spark工作原理 spark运行原理 Spark Streaming Storm的ack是干嘛的 kalfka干嘛的 job提交到yarn上的工作流程 10x...

    Albert陈凯
  • Java基于ssm框架的restful应用开发

    用户1141560
  • Golang语言RPC Authorization进行简单ip安全验证的方法

    前言:写网络服务,总要考虑安全机制,对ip和网段进行判断是最简单的一个验证机制。之后想做一个类似注册式的安全验证机制,既可以减少配置文件的麻烦,又可以很好的进行...

    李海彬
  • 如果是你,你对信安之路会有哪些期望

    昨天在 信安之路学习交流群 发了一个小调查,来了解大家对于文章的喜好以及对信安之路未来的期望,希望能够尽可能的满足小伙伴的期望,让信安之路走的更远,让更多的小伙...

    信安之路

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动