SpringBoot Shiro 详细教程

在参考中发现了 《Apache Shiro 参考手册》,强烈建议参看学习。


Shiro 简介

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。


首先创建一个 SpringBoot 项目,并在 pom.xml 文件中引入如下会用到的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <!-- 导入shiro和spring整合依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

resources 文件夹下创建 shiro.ini 文件

[users]
zhangsan=123
lisi=123
wangwu=123

测试 Shiro

@Test
public void test01() {
    // 创建安全管理器,设置它的 Realm
    DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
    defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
    //将安装工具类中设置默认安全管理器
    SecurityUtils.setSecurityManager(defaultSecurityManager);
    //获取主体对象
    Subject subject = SecurityUtils.getSubject();
    //创建token令牌
    UsernamePasswordToken token = new UsernamePasswordToken("xiaochen1", "123");
    try {
        subject.login(token);//用户登录
        System.out.println("登录成功~~");
    } catch (UnknownAccountException e) {
        e.printStackTrace();
        System.out.println("用户名错误!!");
    }catch (IncorrectCredentialsException e){
        e.printStackTrace();
        System.out.println("密码错误!!!");
    }

}

在这个测试中,我们可以把 ini 当做是个账号的数据库,UsernamePasswordToken token = new UsernamePasswordToken("xiaochen1", "123"); 这一行看作是用户登陆时发来的账号和密码的通行证令牌

subject.login(token); 这里进行登陆,开始进行验证上面的 token,这时设置的 IniRealm 对象会查找数据库(当前是 shiro.ini 文件)里是否有匹配的账号,然后验证密码,如果没有报错就是登陆成功,报错那就是不对。

该方法主要执行以下操作:
1. 检查提交的进行认证的令牌信息
2. 根据令牌信息从数据源(通常为数据库)中获取用户信息
3. 对用户信息进行匹配验证。
4. 验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
5. 验证失败则抛出AuthenticationException异常信息。

以下是几种出现的错误:

  • UnknownAccountException (账号错误/没有账号)

  • IncorrectCredentialsException (密码错误)

  • DisabledAccountException(帐号被禁用)

  • LockedAccountException(帐号被锁定)

  • ExcessiveAttemptsException(登录失败次数过多)

  • ExpiredCredentialsException(凭证过期)等

上面代码运行过程:

上边的程序使用的是读取本地 ini 文件的方式进行测试进行验证判断用户名和密码是否正确,但正式开发中是不能用这种的,都需要从数据库中读取对应登陆的用户的信息,所以需要自定义 Realm 处理这些逻辑,以及添加更多样的操作。

Subject 即主体,外部应用与 subject 进行交互,subject 记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权

Realm 即领域,相当于 datasource 数据源,SecurityManager 进行安全认证需要通过 Realm 获取用户权限数据,比如:如果用户身份数据在数据库那么 Realm 就需要从数据库获取用户身份信息。就是在 Realm 里写认证和授权逻辑的,执行的时候会执行 Realm 里的认证和授权逻辑。

注意:不要把 Realm 理解成只是从数据源取数据,在 Realm 中还有认证授权校验的相关的代码。

一般在真实的项目中,我们不会直接实现 Realm 接口,也不会直接继承最底层的功能贼复杂的 IniRealm。我们一般的情况就是直接继承 AuthorizingRealm,能够继承到认证与授权功能。它需要强制重写两个方法:doGetAuthenticationInfodoGetAuthorizationInfo

Shiro 提供的 Realm 体系较为复杂,一般我们为了使用 Shiro 的基本目的就是:认证授权。可以看以下 SimpleAccountRealm 的部分源码中的两个方法。授权(doGetAuthorizationInfo)、认证(doGetAuthenticationInfo)。两部分的源码:

public class SimpleAccountRealm extends AuthorizingRealm {
        //.......省略
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        SimpleAccount account = getUser(upToken.getUsername());

        if (account != null) {

            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }
            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }

        }

        return account;
    }

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = getUsername(principals);
        USERS_LOCK.readLock().lock();
        try {
            return this.users.get(username);
        } finally {
            USERS_LOCK.readLock().unlock();
        }
    }
}

上面就是他的一个认证授权时的逻辑,我们可以自定义一个 UserRealm,以实现更复杂更强大的认证、授权逻辑。

public class UserRealm extends AuthorizingRealm {
    //授权方法(访问不同的 controller 里的接口时进行授权)
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 这个 username 是下面 doGetAuthenticationInfo 方法 return 的对象的第一个参数的值
        final String username = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("执行授权:授权的用户名 " + username);

        final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 查询设置用户的权限 permission(下面假装是数据库查询到的数据)
        Set<String> permissions = new HashSet<>();
        permissions.add("auth:add_user");
        permissions.add("auth:update_user");
        info.setStringPermissions(permissions);

        // 查询设置用户的角色 Role(下面假装是数据库查询到的数据)
        Set<String> roles = new HashSet<>();
        roles.add("管理员");
        info.setRoles(roles);

        return info;
    }

    //认证方法,在登陆的时候会进行验证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行认证");
        final UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        /* 
        //按数据库查询应该是以下代码,但为了方便起见,后面的写个假数据
        User user = userService.findUser(token.getUsername());
        if(user != null){
            // 当前 Realm 中的 doGetAuthorizationInfo 方法那个参数就是这个第一个参数,保存了当前用户信息
            return new SimpleAuthenticationInfo(
                    user,
                    user.getPassword(),
                    this.getName()
            );
        }
        */

        // 数据库查询用户名和密码(这里假作已经查询到了结果)
        String name = "zhangsan";
        String password = "123";
        if (name.equals(token.getUsername())) {
            // new SimpleAuthenticationInfo 的时候会自动校验密码
            return new SimpleAuthenticationInfo(
                    token.getPrincipal(),
                    password,
                    this.getName()
            );
        }

        return null;
    }
}

测试

@Test
public void test01() {
    // 创建安全管理器,设置它的 Realm
    final DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
    defaultSecurityManager.setRealm(new UserRealm());
    // 设置处理认证和授权的安全管理器
    SecurityUtils.setSecurityManager(defaultSecurityManager);

    final Subject subject = SecurityUtils.getSubject();
    // 前端发送来的账号和密码生成的通行令牌,用来让 Realm 的 doGetAuthenticationInfo 方法里执行认证过程
    UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");
    try {
        subject.login(token);
    } catch (UnknownAccountException e) {
        e.printStackTrace();
        System.out.println("用户名错误!!");
    } catch (IncorrectCredentialsException e) {
        e.printStackTrace();
        System.out.println("密码错误!!!");
    }
}

上面运行过程

配置 Shiro

上面是单个测试时全都写到一个方法里了,做项目的时候我们需要给 Shiro 默认配置好,后面不用再一个个创建设置了,所以我们开始创建 ShiroConfig,用来配置 shiro 的类:

import com.example.shirotest.common.UserRealm;  // 引用自定义的 UserRealm
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro
 *
 * @author z
 * @datetime 2022-5-16
 */
@Configuration
public class ShiroConfig {

    /**
     * 创建 Realm,bean会让方法返回的对象放入到spring的环境,以便使用
     */
    @Bean(name = "userRealm")
    public UserRealm getRealm() {
        return new UserRealm();
    }

    /**
     * @Qualifier 注释指定注入 Bean 的名称,用来消除歧义的
     */
    @Bean(name = "defaultWebSecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(userRealm);
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置一个安全管理器来关联 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        // 默认登陆界面
        shiroFilterFactoryBean.setLoginUrl("/loginPage");

        // 设置权限(非注解方式设置对应 url 的权限)
        // 注意要用 LinkedHashMap 遍历列表时时候按顺序进行匹配判断 URL
        // 所以下面的 /** 要放在最后,否则如果放在第一个则所有的 URL 都
        // 可以被匹配到,那么之后的就失效了
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/loginPage", "anon");
        filterMap.put("/user/login", "anon");

        filterMap.put("/add", "perms[user:add, admin]");
        filterMap.put("/update", "perms[user:update]");

        filterMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

        return shiroFilterFactoryBean;
    }

    /**
     *  开启Shiro的注解 (如@RequiresRoles,@RequiresPermissions)
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
}

上面的 ShiroFilterFactoryBean 方法中主要主要配置接口的角色权限,确定接口由哪些角色或者哪些权限的用户可以访问。至于 shiro 是怎么知道当前用户是否具有某个角色或权限需要用到后面的 doGetAuthorizationInfo() 方法。

Filter 解释
anon 无参,开放权限,可以理解为匿名用户或游客
authc 无参,需要认证
user 无参,表示必须存在用户,当登入操作时不做检查
perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user:add, admin"],当有多个参数时必须每个参数都通过才算通过
roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin, user"],当有多个参数时必须每个参数都通过才算通过

常见过滤器

  • 注意: shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限:
配置缩写 对应的过滤器 功能
anon AnonymousFilter 指定url可以匿名访问
authc FormAuthenticationFilter 指定url需要form表单登录,默认会从请求中获取usernamepassword,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。
authcBasic BasicHttpAuthenticationFilter 指定url需要basic登录
logout LogoutFilter 登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreation NoSessionCreationFilter 禁止创建会话
perms PermissionsAuthorizationFilter 需要指定权限才能访问
port PortFilter 需要指定端口才能访问
rest HttpMethodPermissionFilter 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
roles RolesAuthorizationFilter 需要指定角色才能访问
ssl SslFilter 需要https请求才能访问
user UserFilter 需要已登录或“记住我”的用户才能访问

Shiro 权限字符串

  1. 组成规则
    在Shiro中使用权限字符串必须按照Shiro指定的规则。

权限字符串组合规则为:”资源类型标识符:操作:资源实例标识符

  • 资源类型标识符: 一般会按模块,对系统划分资源。比如user模块,product 模块,order模块等,对应的资源类型标识符就是:userproductorder
  • 操作: 一般为增删改查(createdeleteupdatefind),还有 * 标识统配。
  • 资源实例标识符: 如果Subject控制的是资源类型,那么资源实例标识符就是 “*” ;如果Subject控制的是资源实例,那么就需要在资源实例标识符就是该资源的唯一标识(ID等)。
  1. 示例
  • *:*:* 表示 Subject 对所有类型的所有实例有所有操作权限,相当于超级管理员。

  • user:create:* 表示 Subject 对 user 类型的所有实例有创建的权限,可以简写为:user:create

  • user:update:001 表示 Subject 对 ID001 的 user 实例有修改的权限。

  • user:*:001 表示 Subject 对 ID001user 实例有所有权限。

Filter Chain定义说明

  • 1、一个URL可以配置多个Filter,使用逗号分隔
  • 2、当设置多个过滤器时,全部验证通过,才视为通过
  • 3、部分过滤器可指定参数,如perms,roles

上面的自定义的 UserRealmShiroConfig 创建的步骤你可以把它算作一个固定的套路,只要使用 Shiro 就难以避免,必须要创建这两个类,以及实现其中的方法。

MD5 加密

我们创建一个 MD5Utils 工具类,对密码进行加密,加密后的密码破解难度是“不可能破解出来”,即便黑客获取到数据库,也无法知道具体密码。(一般建议使用随机盐值,然后保存盐值到用户的数据库中,这样更加安全)

import org.apache.shiro.crypto.hash.Md5Hash;
import java.util.Random;


/**
 * @author z
 */
public class MD5Utils {

    /*** 加密盐值 */
    public static final String SALT = "^3&5as@9.[km0";

    /*** 密码进行MD5加密 (固定盐值) */
    public static String md5Password(String password) {
        return md5Password(password, SALT, 1024);
    }

    /*** 密码进行MD5加密 */
    public static String md5Password(String password, String salt) {
        // 加密 1024 次
        Md5Hash md5Hash = new Md5Hash(password, salt, 1024);
        return md5Hash.toHex();
    }

    **
     * 生成随机盐 salt 的静态方法
     *
     * @length 生成长度
     */
    public static String getSalt(int length) {
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            char aChar = chars[new Random().nextInt(chars.length)];
            sb.append(aChar);
        }
        return sb.toString();
    }

}

用户注册的时候,我们对他们的密码进行加密保存到数据库中,
那么在提交用户输入原始的密码的时候,我们需要进行加密跟我们已经保存的加密后的密码进行匹配,如果两个加密后的密码相同,那自然两个密码都是一样的

对测试类中的 token 添加加密,也就是对发来的密码进行 MD5 加密

UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", MD5Utils.md5Password("123"));

UserRealm 中的 doGetAuthenticationInfo() 因为我们写的假数据,所以也要改一下,改为如下:

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行认证");

        // 数据库查询用户名和密码(这里假作已经查询到了结果)
        // 如果是从数据库查找到的则不用进行 MD5Utils.md5Password 这个操作,因为数据库里已经加过密了
        // 这里因为这是我们手动设置的假数据,所以需要 md5 加密一下
        String name = "zhangsan";
        String password = MD5Utils.md5Password("123");

        final UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        if (name.equals(token.getUsername())) {
            // new SimpleAuthenticationInfo 的时候会自动校验密码
            return new SimpleAuthenticationInfo(
                    token.getPrincipal(),
                    password,
                    this.getName()
            );
        }

        return null;
    }

权限注解

注:shiro 提供了相应的注解用于权限控制,如果使用这些注解就需要使用 aop 的功能来进行判断。shiro 提供了 spring aop 集成,用于权限注解的解析和验证

(1)@RequiresAuthentication :方法在访问或调用时,当前 Subject 必须在当前 session 中已经过认证。表示当前 Subject 已经通过 login 进行了身份验证;即 Subject.isAuthenticated() 返回 true

(2)@RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的,才能访问或调用被该注解标注的类,实例,方法。

(3)@RequiresGuest :表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。使用该注解标注的类,实例,方法在访问或调用时,当前 Subject 可以是 guest 身份,不需要经过认证或者在原先的 session 中存在记录。

(4)@RequiresRoles(value={"admin", "user"}, logical= Logical.AND) 当前 Subject 必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天 Subject 不同时拥有所有指定角色,则方法不会执行还会抛出 AuthorizationException 异常。

(5)@RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR) :表示当前 Subject 需要权限 user:auser:b。当前 Subject 需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前 Subject 不具有这样的权限,则方法不会被执行。

Shiro 的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):

RequiresRoles
RequiresPermissions
RequiresAuthentication
RequiresUser
RequiresGuest

例如:你同时声明了 @RequiresRoles@RequiresPermissions,那就要求拥有此角色的同时还得拥有相应的权限。

@RequiresRoles

可以用在 Controller 或者方法上。可以多个 roles,多个roles 时默认逻辑为 AND 也就是所有具备所有 role 才能访问。

示例:

//属于user角色
@RequiresRoles("user")

//必须同时属于user和admin角色
@RequiresRoles({"user","admin"})

//属于user或者admin之一;修改logical为OR 即可
@RequiresRoles(value={"user","admin"},logical=Logical.OR)

@RequiresPermissions

@RequiresRoles 类似。示例:

//符合index:hello权限要求
@RequiresPermissions("index:hello")

//必须同时复核index:hello和index:world权限要求
@RequiresPermissions({"index:hello","index:world"})

//符合index:hello或index:world权限要求即可
@RequiresPermissions(value={"index:hello","index:world"},logical=Logical.OR)

@RequiresAuthentication,@RequiresUser,@RequiresGuest

这三个的使用方法一样

@RequiresAuthentication
@RequiresUser
@RequiresGusst

其他例子:

@RequiresPermissions({"file:read", "write:aFile.txt"} )
void someMethod();

要求subject中必须同时含有file:readwrite:aFile.txt的权限才能执行方法someMethod()。否则抛出异常AuthorizationException

@RequiresRoles({"admin"})
void method();

只有 admin 角色才能访问该方法,其他角色访问将会抛出异常

自定义 Shiro 注解

但是仅仅是拥有shiro中的这5个注解肯定是不够使用的。在实际的使用过程中,根据需求,我们会在权限认证中加入我们自己特有的业务逻辑的,我们为了便捷则可以采用自定义注解的方式进行使用。这种方法不仅仅适用于 Apache Shiro,很多其他的框架如:Hibernate Validator、SpringMVC、甚至我们可以写一套校验体系,在aop中去验证权限,这都是没问题的。所以自定义注解的作用很广。但是在这里,我仅仅基于shiro的来实现适用于它的自定义注解。

  • 定义注解类
/**
 * 用于认证的接口的注解,组合形式默认是“或”的关系
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
    /**
     * 业务模块
     * @return
     */
    String[] module();
    /**
     * 操作类型
     */
    String[] 定义注解类
}
  • 定义注解的处理类
/**
 * Auth注解的操作类
 */
public class AuthHandler extends AuthorizingAnnotationHandler {

    public AuthHandler() {
        //写入注解
        super(Auth.class);
    }

    @Override
    public void assertAuthorized(Annotation a) throws AuthorizationException {
        if (a instanceof Auth) {
            Auth annotation = (Auth) a;
            String[] module = annotation.module();
            String[] action = annotation.action();
            //1.获取当前主题
            Subject subject = this.getSubject();
            //2.验证是否包含当前接口的权限有一个通过则通过
            boolean hasAtLeastOnePermission = false;
            for(String m:module){
                for(String ac:action){
                    //使用hutool的字符串工具类
                    String permission = StrFormatter.format("{}:{}",m,ac);
                    if(subject.isPermitted(permission)){
                        hasAtLeastOnePermission=true;
                        break;
                    }
                }
            }
            if(!hasAtLeastOnePermission){
                throw new AuthorizationException("没有访问此接口的权限");
            }

        }
    }
}
  • 定义shiro拦截处理类
/**
 * 拦截器
 */
public class AuthMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {

    public AuthMethodInterceptor() {
        super(new AuthHandler());
    }

    public AuthMethodInterceptor(AnnotationResolver resolver) {
        super(new AuthHandler(), resolver);
    }

    @Override
    public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
        // 验证权限
        try {
            ((AuthHandler) this.getHandler()).assertAuthorized(getAnnotation(mi));
        } catch (AuthorizationException ae) {
            if (ae.getCause() == null) {
                ae.initCause(new AuthorizationException("当前的方法没有通过鉴权: " + mi.getMethod()));
            }
            throw ae;
        }
    }
}
  • 定义shiro的aop切面类
/**
 * shiro的aop切面
 */
public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor {
    public AuthAopInterceptor() {
        super();
        // 添加自定义的注解拦截器
        this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver()));
    }
}
  • 定义shiro的自定义注解启动类
/**
 * 启动自定义注解
 */
public class ShiroAdvisor extends AuthorizationAttributeSourceAdvisor {

    public ShiroAdvisor() {
        // 这里可以添加多个
        setAdvice(new AuthAopInterceptor());
    }

    @SuppressWarnings({"unchecked"})
    @Override
    public boolean matches(Method method, Class targetClass) {
        Method m = method;
        if (targetClass != null) {
            try {
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
                return this.isFrameAnnotation(m);
            } catch (NoSuchMethodException ignored) {

            }
        }
        return super.matches(method, targetClass);
    }

    private boolean isFrameAnnotation(Method method) {
        return null != AnnotationUtils.findAnnotation(method, Auth.class);
    }
}

总体的思路顺序:定义注解类(定义业务可使用的变量)-> 定义注解处理类(通过注解中的变量做业务逻辑处理)-> 定义注解的拦截器 -> 定义aop的切面类 -> 最后定义 shiro 的自定义注解启用类。其他的自定义的注解的编写思路和这个也是类似的。

注意事项

在日常开发时,往往会在 Service 层添加“@Transactional”注解,为的是当 Service 发送数据库异常时,所有数据库操作可以回滚。

当在 Service 层添加“@Transactional”注解后,执行 Service 方法前,会开启事务。此时的 Service 已经是一个代理对象了,此时如果我们将 Shiro 的权限注解加载 Service 层是不合适的,此时需要加到 Controller 层。这是因为不能让 Service 是“代理的代理”,如果强行注入,会发生类型转换异常。

推荐

推荐查看 SpringBoot 集成 Shiro,简明扼要

建议看完之后配合 Shiro保姆级教程 学习,他的文章中还包含有 SQL 的创建和实现步骤

参考

发表评论