Shrio + Spring Boot配置与使用

一、概述

1. Shiro介绍

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

2. 基本功能

在这里插入图片描述

  • Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;

  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;

  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

  • 支持的特性:

    • Web Support:Web 支持,可以非常容易的集成到 Web 环境;

    • Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;

      扫描二维码关注公众号,回复: 13748455 查看本文章
    • Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

    • Testing:提供测试支持;

    • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

    • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

注意:Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。

3. 架构

在这里插入图片描述

  • Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;

  • SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

  • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

  • Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

  • Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;

  • SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);

  • SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;

  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

  • Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的。

对于我们而言,最简单的一个 Shiro 应用:

  1. 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager

  2. 我们需要给 ShiroSecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
    在这里插入图片描述

二、认证(登录)

1. 引入依赖

maven 仓库地址:https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.6.0</version>
</dependency>

在这里插入图片描述

2. Shiro 中常见类

(1)常见类

Shiro 中常用的类或者接口:

名字 类型 说明
SecurityManager 接口 安全管理器,对主体进行认证授权,还额外提供登录,退出和创建用户的功能
DefaultWebSecurityManager 对象 默认的 Web SecurityManager,会提供一些默认值
ShiroFilterFactoryBean 对象 Shiro 的过滤器对象,这个对象将配置 Shiro 相关的一个规则的拦截
Realm 接口 数据源,从数据源存取认证和授权相关的数据
AuthenticatingRealm 抽象类 只负责认证(登录),通过实现该类来指定认证方法
AuthorizingRealm 抽象类 负责认证(登录)和授权,通过实现该类来指定认证和授权方法
AuthenticationToken 接口 登录未验证的数据
AuthenticationInfo 接口 身份验证/登录过程相关的帐户信息。
Subject 接口 程序中用这个对象来操作 shiro

(2)常见异常

异常类 说明
AuthenticationException Shiro 中所有异常的父类
AccountException 账号异常 可以我们自己抛出
UnknownAccountException 账号不存在的异常 可以我们自己抛出
LockedAccountException 账号异常锁定异常 可以我们自己抛出
IncorrectCredentialsException 密码错误异常 这个异常会在Shiro进行密码验证是抛出

3. 通过Shiro认证账号

1. Realm 接口

ShiroRealm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源

public interface Realm {
    
    
	// 返回一个唯一的 Realm 名字
    String getName();

	// 多 Realm 中,判断此 Realm 是否支持此 Token
    boolean supports(AuthenticationToken token);

	// 依据未认证的 AuthenticationToken,返回认证后的 AuthenticationInfo
    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

}

2. 自定义 Realm 类

常用的两个已经实现了 Realm 接口的抽象类

  • AuthenticatingRealm:只负责认证(登录)
  • AuthorizingRealm:负责认证(登录)和授权,是 AuthenticatingRealm 的子类

这里先使用 AuthenticatingRealm,后面认证再使用 AuthorizingRealm

public class MyRealm extends AuthenticatingRealm {
    
    
    /**
     * Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
     * @param authenticationToken   用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
     * @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证,密码是由 Shiro 进行验证是否合法
     * @throws AuthenticationException 如果认证失败Shiro就会抛出AuthenticationException
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        //将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //获取用户在浏览器中输入的账号
        String username = token.getUsername();
        
        //认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
        String dbusername = username;
        if (!"admin".equals(dbusername) && !"zhangsan".equals(dbusername)) {
    
    //判断用户账号是否正确
            throw new UnknownAccountException("账号错误");
        }
        if ("zhangsan".equals(username)) {
    
    
            throw new LockedAccountException("账号被锁定");
        }
        
        //定义一个密码这个密码应该来自数据库中
        //如果要从数据库中取,就定义一下 UserMapper,然后从数据库中获取即可
        String dbpassword = "123456";
        
        //认证密码是否正确
        return new SimpleAuthenticationInfo(dbusername, dbpassword, getName());
    }
}

3. ShiroConfig

定义 ShiroConfig

@Configuration
public class ShiroConfig {
    
    
    //配置Shiro的安全管理器
    @Bean
    public SecurityManager securityManager(Realm myRealm) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    //配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
    @Bean
    public Realm myRealm() {
    
    
        MyRealm realm = new MyRealm();
        return realm;
    }

    //配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    
    
        //创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        //设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
        shiroFilter.setSecurityManager(securityManager);

        //用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
        //作用是用于通知Shiro我们可以使用这里路径转向到登录页面,当Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
        shiroFilter.setLoginUrl("/");
        //登录成功后转向页面
        shiroFilter.setSuccessUrl("/success");
        //用于指定没有权限转向页面
        shiroFilter.setUnauthorizedUrl("/noPermission");
        
        //定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问什么样的请求不可以访问
        /*
         * anon:无需认证就可以访问
         * authc:必须认证了才能访问
         * user:必须用有了 记住我 功能才能用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有某个角色权限才能访问
         * logout:登出后会释放当前用户的内存
         */
        Map<String, String> map = new LinkedHashMap<String, String>();
        // '/login' 表示某个请求的名字;'anon'表示权限
        map.put("/login", "anon");
        map.put("/logout", "logout");
        // 注意: ** 表示任意子孙路径
        //       *  表示任意的一个路径
        //       ?  表示任意的一个字符
        map.put("/admin/**", "authc");
        map.put("/user/**", "authc");

        /**
         * 表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
         * 如果没有指定 /** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
         */
        map.put("/**","authc");
        shiroFilter.setFilterChainDefinitionMap(map);
        return shiroFilter;
    }
}

4. UserController

@Controller
public class TestController {
    
    
    @RequestMapping("/login")
    public String login(String username, String password, Model model) {
    
    
        
        //创建一个shiro的Subject对象,利用这个对象来完成用户的登录认证
        //Subject可以理解为当前用户,shiro 提供了自己的 Session
        Subject subject = SecurityUtils.getSubject();

        //判断用户是否认证过(是否登录过),进入 if 说明需要认证
        if (!subject.isAuthenticated()) {
    
    

            //创建一个用户账号和密码的Token对象,并设置用户输入的账号和密码。这个对象将在Shiro中被获取
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            try {
    
    

                //调用login后Shiro就会自动执行我们自定义的Realm中的认证方法
                subject.login(token);

            } catch (UnknownAccountException e) {
    
    
                //用户的账号错误异常,可以看上面的 MyRealm类,是我自己在后台抛出的
                model.addAttribute("errorMessage", "账号不存在");
                return "login";
            } catch (LockedAccountException e) {
    
    
                //账号被锁定异常,可以看上面的 MyRealm类,是我自己在后台抛出的
                model.addAttribute("errorMessage", "账号被冻结");
                return "login";
            } catch (IncorrectCredentialsException e) {
    
    
                //用户密码错误异常,是 shiro 在认证密码时抛出的
                model.addAttribute("errorMessage", "密码错误");
                return "login";
            }
        }
        //登录成功,跳转到登录成功页面
        return "success";
    }

	/** 登出有两种方式
	 * 1. 在 ShiroConfig 中直接配置登出即可:map.put("/logout", "logout"); 
	 * 2. 使用 Subject 的 logout() 来登出,如下:
	 */
    @RequestMapping("/logout")
    public String logout() {
    
    
        Subject subject = SecurityUtils.getSubject();
        
		//退出登录
        subject.logout();

        return "login";
    }
}

5. 密码加密(了解)

public class MyRealm extends AuthenticatingRealm {
    
    
    /**
     * Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
     * @param authenticationToken   用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
     * @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证,密码是由 Shiro 进行验证是否合法
     * @throws AuthenticationException 如果认证失败Shiro就会抛出AuthenticationException
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        //将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //获取用户在浏览器中输入的账号
        String username = token.getUsername();
        
        //认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
        String dbusername = username;
        if (!"admin".equals(dbusername) && !"zhangsan".equals(dbusername)) {
    
    //判断用户账号是否正确
            throw new UnknownAccountException("账号错误");
        }
        if ("zhangsan".equals(username)) {
    
    
            throw new LockedAccountException("账号被锁定");
        }
        //定义一个密码这个密码应该来自数据库中
        String dbpassword = "123456";

		//参数1:为加密算法,这里选择MD5加密
        //参数2:为被加密的数据的数据
        //参数3:为加密时的盐值,用于改变加密后数据结果,通常这个盐值需要选择一个表中唯一的数据例如表中的账号
        //参数4:为需要对数据使用指定的算法加密多少次
        Object obj = new SimpleHash("MD5", dbpassword, "", 1);
        //认证密码是否正确 使用加密后的密码登录
        return new SimpleAuthenticationInfo(dbusername, obj.toString(), getName());
    }
}

三、授权

1. 在 Realm 中配置权限

1. 自定义 Realm 类

自定义 Realm 类,继承 AuthorizingRealm 抽象类, AuthorizingRealm 类在 AuthenticatingRealm 类的基础上,多了一个授权的抽象方法。

public class MyRealm extends AuthorizingRealm {
    
    
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        // 在这里进行认证,详见认证中的自定义 Realm 类(二.3.2)
    }

    @Override
    //Shiro用户授权的回调方法
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        //从Shiro中获取用户名
        Object username = principalCollection.getPrimaryPrincipal();
        
        //创建一个SimpleAuthorizationInfo类的对象,利用这个对象需要设置当前用户的权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        
        //用户对应的角色信息集合(从数据库中取)
        Set<String> roles = new HashSet<>();
        if ("admin".equals(username)) {
    
    
        	//表明该用户有 admin 和 user 角色
            roles.add("admin");
            roles.add("user");
        } else if ("zhangsan".equals(username)) {
    
    
        	//表明该用户只有 user 角色
            roles.add("user");
        }

        //用户对应的权限信息(从数据库取)
        Set<String> psermission = new HashSet<>();
        if ("admin".equals(username)) {
    
    
        	//表明该用户具有 admin 角色的 add 权限
            psermission.add("admin:add");
        }
        
        //设置角色和权限信息
        simpleAuthorizationInfo.setRoles(roles);
        simpleAuthorizationInfo.setStringPermissions(psermission);
        return simpleAuthorizationInfo;
    }

}

2. 在 ShiroConfig 中配置路径的权限规则

@Configuration
public class ShiroConfig {
    
    
    //配置Shiro的安全管理器
    @Bean
    public SecurityManager securityManager(Realm myRealm) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    //配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
    @Bean
    public Realm myRealm() {
    
    
        MyRealm realm = new MyRealm();
        return realm;
    }

    //配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    
    
        //创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        //设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
        shiroFilter.setSecurityManager(securityManager);

        //用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
        //作用是用于通知Shiro我们可以使用这里路径转向到登录页面,当Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
        shiroFilter.setLoginUrl("/");
        //登录成功后转向页面
        shiroFilter.setSuccessUrl("/success");
        //用于指定没有权限转向页面
        shiroFilter.setUnauthorizedUrl("/noPermission");
        
        //定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问什么样的请求不可以访问
        /*
         * anon:无需认证就可以访问
         * authc:必须认证了才能访问
         * user:必须用有了 记住我 功能才能用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有某个角色权限才能访问
         * logout:登出后会释放当前用户的内存
         */
        Map<String, String> map = new LinkedHashMap<String, String>();
        // '/login' 表示某个请求的名字;'anon'表示权限
        map.put("/login", "anon");
        map.put("/logout", "logout");
        //roles[admin] 表示 以/admin/**开头的请求需要拥有admin角色才可以访问
        //perms[admin:add] 表示 /admin/test 的请求需要拥有 admin:add权限才可访问
        //注意:权限匹配是从上到下进行匹配的,因此 /admin/test 要写在 /admin/** 前面
        map.put("/admin/test", "authc,perms[admin:add]");
        map.put("/admin/**", "authc,roles[admin]");
        map.put("/user/**", "authc,roles[user]");
        map.put("/**","authc");

        shiroFilter.setFilterChainDefinitionMap(map);
        return shiroFilter;
    }
}

2. 基于注解的权限控制

1. ShiroConfig 中开启注解支持

注意:启动注解的权限控制以后需要删除在 Shiro 配置类中的权限拦截的配置规则

@Configuration
public class ShiroConfig {
    
    

    @Bean
    public MyRealm myRealm() {
    
    
        return new MyRealm();
    }

    @Bean
    public SecurityManager securityManager(MyRealm myRealm) {
    
    
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager(myRealm);

        return manager;
    }

    //配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
    //例如什么样的请求可以访问什么样的请求不可以访问等等
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
        shiroFilterFactory.setSecurityManager(securityManager);

        // 登录页面
        shiroFilterFactory.setLoginUrl("/");
        // 登录成功后访问的地址
        shiroFilterFactory.setSuccessUrl("/success");
        // 登录失败时访问的地址
        shiroFilterFactory.setUnauthorizedUrl("/nopermission");

        /**
         * 配置权限拦截规则
         */
        Map<String, String> map = new LinkedHashMap<>();

        map.put("/login", "anon");
        map.put("/logout", "logout");

        //配置注解,需要将 ShrioConfig 中所有的角色、权限配置删掉
        //map.put("/admin/add", "authc,perms[admin:add]");
        //map.put("/admin/**", "authc,roles[admin]");
        //map.put("/user/**", "authc,roles[user]");

        shiroFilterFactory.setFilterChainDefinitionMap(map);

        return shiroFilterFactory;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
    
    
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    /**
     * 开启aop支持
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

2. 使用注解

  • @RequiresAuthentication:表示当前 Subject 已经通过 login 进行了身份验证;即 Subject.isAuthenticated() 返回 true

  • @RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的。

  • @RequiresGuest:表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。

  • @RequiresRoles(String[] value, Logical logical)

    • value:角色列表。例如:value={“admin”, “user”}
    • logical:多个角色之间要满足的关系(Logical.ANDLogical.OR),默认是 Logical.AND
  • @RequiresPermissions (String[] value, Logical logical)

    • value:角色权限列表。例如:value={“user:a”, “user:b”}
    • logical:多个角色之间要满足的关系(Logical.ANDLogical.OR),默认是 Logical.AND
@Controller
public class TestController {
    
    

    @RequestMapping("/nopermission")
    public String noPermission() {
    
    
        return "noPermission";
    }

    //RequiresRoles  Shiro的注解 表示访问这功能必须要拥有 admin角色
    //注意使用了注解以后需要配置Spring声明式异常捕获,否则将在浏览器中直接看到Shiro的错误信息而不是友好的信息提示
    @RequiresRoles(value = {
    
    "admin"})
    @RequestMapping("/admin/test")
    public @ResponseBody String adminTest() {
    
    
        return "这个adminTest请求";
    }

    @RequiresPermissions(value = {
    
    "admin:add"})
    @RequestMapping("/admin/add")
    public @ResponseBody String adminAdd() {
    
    
        Subject subject = SecurityUtils.getSubject();
        //验证当前用户是否拥有这个权限
        //subject.checkPermission();
        //验证当前用户是否拥有这个角色
        subject.checkRole("admin:add");
        return "这个adminAdd请求";
    }

    //配置一个Spring的异常监控,当工程抛出了value所指定的所以异常类型以后将直接进入到当前方法中
    @ExceptionHandler(value = {
    
    Exception.class})
    public String myError(Throwable throwable) {
    
    
        //获取异常的类型,应该根据不同的异常类型进入到不通的页面显示不同提示信息
        System.out.println(throwable.getClass());
        return "noPermission";
    }
}

四、会话管理(Redis)

1. 整合 Redis

  1. 添加 pom 文件
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 配置 redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123

2. 重写 SessionDAO

public class MyRedisSessionDAO extends AbstractSessionDAO {
    
    

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    public static final int DEFAULT_TIME_OUT = 30;
    public static final String SESSION_PREFIX = "shiro_session";

    @Override
    protected Serializable doCreate(Session session) {
    
    
        // 生成 SessionId
        Serializable sessionId = this.generateSessionId(session);
        // 必须要将生成的id设置到session实力当中
        this.assignSessionId(session, sessionId);

        // 将 session 保存到 redis 中
        this.saveSession(session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
    
    
        if (sessionId != null) {
    
    
            Session session = (Session) redisTemplate.opsForValue().get(SESSION_PREFIX + sessionId);
            return session;
        }
        return null;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
    
    
        this.saveSession(session);
    }

    @Override
    public void delete(Session session) {
    
    
        if (session != null) {
    
    
            redisTemplate.delete(SESSION_PREFIX + session.getId());
        }
    }

    // 返回所有的 session
    @Override
    public Collection<Session> getActiveSessions() {
    
    
        Set<Object> keys = redisTemplate.keys(SESSION_PREFIX + "*");

        Set<Session> set = keys.parallelStream()
                .map(key -> (Session) redisTemplate.opsForValue().get(key))
                .collect(Collectors.toSet());

        return set;
    }

    // 将 session 保存到 redis 中
    private void saveSession(Session session) {
    
    
        if (session != null && session.getId() != null) {
    
    
            redisTemplate.opsForValue().set(SESSION_PREFIX + session.getId(), session, 30, TimeUnit.MINUTES);
        }
    }
}

3. 将重写的 SessionDAO 配置到 SecurityManager

@Configuration
public class ShiroConfig {
    
    

    @Bean
    public MyRealm myRealm() {
    
    
        return new MyRealm();
    }

    // 创建自己的 SessionDAO 实例
    @Bean
    public SessionDAO myRedisSessionDAO() {
    
    
        return new MyRedisSessionDAO();
    }

    //创建 Session 管理对象,使用自定义的 SessionDao
    @Bean
    public SessionManager sessionManager(SessionDAO myRedisSessionDAO) {
    
    
        DefaultWebSessionManager manager = new DefaultWebSessionManager();
        manager.setSessionDAO(myRedisSessionDAO);
        return manager;
    }

    @Bean
    public SecurityManager securityManager(MyRealm myRealm,
                                           SessionManager sessionManager) {
    
    
        // 配置 Realm
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager(myRealm);
        // 配置 SessioonManager
        manager.setSessionManager(sessionManager);

        return manager;
    }

    //配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
    //例如什么样的请求可以访问什么样的请求不可以访问等等
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
        shiroFilterFactory.setSecurityManager(securityManager);
        // 登录页面
        shiroFilterFactory.setLoginUrl("/");
        // 登录成功后访问的地址
        shiroFilterFactory.setSuccessUrl("/success");
        // 登录失败时访问的地址
        shiroFilterFactory.setUnauthorizedUrl("/nopermission");

        /**
         * 配置权限拦截规则
         */
        Map<String, String> map = new LinkedHashMap<>();

        map.put("/login", "anon");
        map.put("/logout", "logout");

        //配置注解,需要将 ShrioConfig 中所有的角色、权限配置删掉
        map.put("/admin/add", "authc,perms[admin:add]");
        map.put("/admin/**", "authc,roles[admin]");
        map.put("/user/**", "authc,roles[user]");

        shiroFilterFactory.setFilterChainDefinitionMap(map);

        return shiroFilterFactory;
    }
}

4. 查看 Redis

在这里插入图片描述

五、缓存(Redis)

1. 创建 CacheManager

CacheManager 中需要提供一个 Cache 对象。Shiro 提供了类似于 SpringCache 抽象,即 Shiro 本身不实现 Cache,但是对 Cache 进行了又抽象,方便更换不同的底层 Cache 实现。

public class MyRedisCacheManager extends AbstractCacheManager {
    
    

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    protected Cache createCache(String s) throws CacheException {
    
    
        return new RedisCache();
    }

	// 创建 RedisCache 对象实现 Cache 接口
    class RedisCache<K, V> implements Cache<K, V> {
    
    

        public static final String CACHE_KEY = "redis_cache";

        @Override
        public V get(K k) throws CacheException {
    
    
            return (V) redisTemplate.boundHashOps(CACHE_KEY).get(k);
        }

        @Override
        public V put(K k, V v) throws CacheException {
    
    
            redisTemplate.boundHashOps(CACHE_KEY).put(k, v);
            return v;
        }

        @Override
        public V remove(K k) throws CacheException {
    
    
            V v = get(k);
            redisTemplate.boundHashOps(CACHE_KEY).delete(k);
            return v;
        }

        @Override
        public void clear() throws CacheException {
    
    
            redisTemplate.delete(CACHE_KEY);
        }

        @Override
        public int size() {
    
    
            return (int) (long) redisTemplate.boundHashOps(CACHE_KEY).size();
        }

        @Override
        public Set<K> keys() {
    
    
            return redisTemplate.boundHashOps(CACHE_KEY).keys();
        }

        @Override
        public Collection<V> values() {
    
    
            return redisTemplate.boundHashOps(CACHE_KEY).values();
        }
    }
}

2. 配置 ShiroConfig

@Configuration
public class ShiroConfig {
    
    
	
	// 创建自定义的 CacheManager 对象
    @Bean
    public CacheManager cacheManager() {
    
    
        return new MyRedisCacheManager();
    }
    
    // 配置 SecurityManager 的 CacheManager 为自定义的 MyRedisCacheManager
    @Bean
    public SecurityManager securityManager(MyRealm myRealm,
                                           CacheManager cacheManager) {
    
    
        // 配置 Realm
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager(myRealm);
        manager.setCacheManager(cacheManager);

        return manager;
    }
    
	@Bean
    public MyRealm myRealm() {
    
    
        return new MyRealm();
    }

3. 查看 Redis

查看 Redis ,大致可以看出 Shiro 将权限信息缓存了起来

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/zyx1260168395/article/details/109289933