• Shiro 中的 Realm


    前言

    之前写项目用了 Shiro 框架,来进行安全验证以及权限管理。当时项目赶得急,没怎么深入了解,只能说能跑能改,不过在使用的过程中发现 Shiro 确实很优秀。现在回过头来学习原理,读读源码,深入的学习下。·

    本篇博文主要写的是关于使用 Shiro 起步时最重要的一块,找了一些资料,力求写得简单明了。

    简介

    Realm:域,Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。从这个意义上讲,Realm 实质上是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro 。当配置 Shiro时,你必须至少指定一个 Realm ,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。
    Shiro 内置了可以连接大量安全数据源(又名目录)的 Realm,如 LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等。如果缺省的 Realm 不能满足需求,你还可以插入代表自定义数据源的自己的 Realm 实现。

    功能

    Realm能做的工作主要有以下几个方面:

    • 身份验证(getAuthenticationInfo 方法)验证账户和密码,并返回相关信息

    • 权限获取(getAuthorizationInfo 方法) 获取指定身份的权限,并返回相关信息

    • 令牌支持(supports方法)判断该令牌(Token)是否被支持

      令牌有很多种类型,例如:HostAuthenticationToken(主机验证令牌),UsernamePasswordToken(账户密码验证令牌)

    这里主来说明一下关于前两点验证方面的逻辑,因为令牌一般用的都是 UsernamePasswordToken,哪怕用 HostAuthenticationToken,也没必要细讲,这个函数很少用到。

    身份验证

    我们看到第一个方法就是我们上面说的“验证账户和密码,并返回相关信息”的方法。从方法的名字上看,只有取得验证信息的意思,其实这里面还包括了进行验证的逻辑。
    看Javadoc,这个方法的作用是:根据传进来的 Token,返回用户的验证信息。下面说明一下 Token 和 用户验证信息 。

    • Token:就是要拿来进行验证的信息,例如:如果是 UsernamePasswordToken 的话,这个 Token 的内容就是“用户提交的用户名和密码”。

      来看下 UsernamePasswordToken 的属性。

      public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
        private String username;
        private char[] password;
        private boolean rememberMe;
        private String host;
      ...

    • 用户验证信息:就是用户验证通过后,返回给系统的信息。例如:用户登录验证的话,一般来说,返回给系统的“用户验证信息”就应该是这个用户的“用户名和密码”。但也可以返回其它信息,例如返回用户的“邮箱地址和登录密码”信息,做为“用户验证信息”。 那么返回给谁呢,Shiro 中的三大组件之一的 Subject。

      不细谈,这么说吧,Subject:即“当前操作用户”。但是,在 Shiro 中,Subject 这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是 Shiro 的“用户”概念。

    上面说了“根据传进来的Token”和“返回用户的验证信息”,但没有说验证的过程,这个过程也是在这个方法中进行。我们看一下源码:

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        // doGetAuthenticationInfo方法的内容,由各个子类来实现。
        // 主要是用来取得我们保存的“用户验证信息”,例如DB里保存的密码(具体看JdbcRealm的方法实现)
        if (info == null) {
            info = doGetAuthenticationInfo(token);
            ...
        }
        // 在这里,把用户提交的信息(Token)和我们保存的“用户验证信息”进行比较
        // 如果不通过,直接抛出定义好的异常。
        if (info != null) {
            assertCredentialsMatch(token, info);
        } else {
    
        return info;
    }

    权限获取

    “权限验证”的处理,是由接口定义的。但“验证是否有访问权限”的逻辑,则是由类定义的。定义的类为:AuthorizingRealm ,在这个类中有个getAuthorizationInfo 方法。这个方法和getAuthenticationInfo 方法的处理流程有点像:

    • 验证是否有指定的权限

    • 返回用户的权限信息

    调用时机

    下面看一个实际登录的 Controller 的例子:

    @Controller
    public class LoginController {
    
        //登录跳转
        @RequestMapping(value = "/login", method = {RequestMethod.GET})
        public String loginUI() throws Exception {
            return "../../login";
        }
    
        //登录跳转
        @RequestMapping(value = "/sxqy", method = {RequestMethod.GET})
        public String loginUI2() throws Exception {
            return "../../login";
        }
    
        //重点!!!!!!
        //登录表单处理
        @RequestMapping(value = "/login", method = {RequestMethod.POST})
        public String login(ViewEmployeeMiPsd viewEmployeeMiPsd) throws Exception {
    
            //Shiro实现登录
            UsernamePasswordToken token = new UsernamePasswordToken(viewEmployeeMiPsd.getCode(),
                    viewEmployeeMiPsd.getPsd());
            Subject subject = SecurityUtils.getSubject();
            //如果获取不到用户名就是登录失败,但登录失败的话,会直接抛出异常
            try{
                //重点!!!!!!
                //getAuthenticationInfo 执行时机
                subject.login(token);
            }catch (Exception e){
                e.printStackTrace();
            }
    
            //重点!!!!!!
             //getAuthorizationInfo  执行时机 -- subject.hasRole()
            if (subject.hasRole("admin")) {
                return "redirect:/admin/showComputerProblems";
            } else if (!subject.hasRole("admin")) {
                return "redirect:/normal/showComputerProblems";
            }
    
            return "/login";
        }
    
    }

    不过,getAuthorizationInfo 的执行调用方式包括上面的总共有三个:

    1. subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
    2. @RequiresRoles(“admin”) :在方法上加注解的时候;
    3. [@shiro.hasPermission name = “admin”][/@shiro.hasPermission]:在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。

    实现

    需要注意的是,在 Shiro 实际使用中,我们是肯定会自定义一个 Realm 类的。

    从上面的功能说明可以看出来,在权限控制中比较重要的验证(登录或权限)逻辑,都是在Realm中做的。Realm的类继承如下:

    Realm类继承图

    不同的继承,需要实现不同的方法。继承了 AuthorizingRealm 的类,都要实现上面说的 getAuthenticationInfogetAuthorizationInfo 方法,来完成身份验证和权限获取。但如果自定义的 Realm 类只实现 Realm 接口的话,只需要 getAuthenticationInfo 方法就可以。下面看一个只实现 Realm 接口的自定义 Realm:

    public class MyRealm1 implements Realm {  
        @Override  
        public String getName() {  
            return "myrealm1";  
        }  
        @Override  
        public boolean supports(AuthenticationToken token) {  
            //仅支持UsernamePasswordToken类型的Token  
            return token instanceof UsernamePasswordToken;   
        }  
        @Override  
        public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
            String username = (String)token.getPrincipal();  //得到用户名  
            String password = new String((char[])token.getCredentials()); //得到密码  
            if(!"zhang".equals(username)) {  
                throw new UnknownAccountException(); //如果用户名错误  
            }  
            if(!"123".equals(password)) {  
                throw new IncorrectCredentialsException(); //如果密码错误  
            }  
            //如果身份认证验证成功,返回一个AuthenticationInfo实现;  
            return new SimpleAuthenticationInfo(username, password, getName());  
        }  
    }   

    但是在使用中基本上都会对账户进行权限管理,下面看一个继承 AuthorizingRealm 的自定义 Realm:

    @Component
    public class LoginRealm extends AuthorizingRealm{
    
        @SuppressWarnings("SpringJavaAutowiringInspection")//忽略警告,下同
        @Resource(name = "roleServiceImpl")
        private RoleService roleService;
    
        @SuppressWarnings("SpringJavaAutowiringInspection")//忽略警告,下同
        @Resource(name = "viewEmployeeMiPsdServiceImpl")
        private ViewEmployeeMiPsdService viewEmployeeMiPsdService;
    
    
    
        /**
         *      获取身份信息,我们可以在这个方法中,从数据库获取该用户的权限和角色信息
         *      当调用权限验证时,就会调用此方法
         */
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
            String code = (String) getAvailablePrincipal(principalCollection);
    
            Role role = null;
            ViewEmployeeMiPsd viewEmployeeMiPsd = null;
            viewEmployeeMiPsd = viewEmployeeMiPsdService.findByCode(code);
            //通过用户名从数据库获取角色权限集
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            Set<String> r = new HashSet<>();
            if (role != null) {
                String[] roles = role.getRolename().split("\+");
                for(int i = 0;i < roles.length; i++){
                    r.add(roles[i].toString());
                }
                //放入该用户权限信息
                info.setRoles(r);
            }
    
            return info;
        }
    
        /**
         * 在这个方法中,进行身份验证
         * login时调用
         */
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            //工号
            String code = (String) token.getPrincipal();
            //密码
            String password = new String((char[])token.getCredentials());
    
            ViewEmployeeMiPsd viewEmployeeMiPsd = null;
            viewEmployeeMiPsd = viewEmployeeMiPsdService.findByCode(code);
    
            if (viewEmployeeMiPsd == null) {
                //没有该用户
                throw new UnknownAccountException();
            } else if (!password.equals(viewEmployeeMiPsd.getPsd())) {
                //密码错误
                throw new IncorrectCredentialsException();
            }
    
            //身份验证通过,返回一个身份信息
            AuthenticationInfo aInfo = new SimpleAuthenticationInfo(code,password,getName());
    
            return aInfo;
        }
    }

    参考

    1. 关于Shiro中的Realmhotdust
    2. 关于何时执行shiro AuthorizingRealm 里的 doGetAuthenticationInfo与doGetAuthorizationInfofj200821
    3. shiro – 百度百科
  • 相关阅读:
    深入理解Java容器——HashMap
    深入理解Java并发容器——ConcurrentHashMap
    String、StringBuilder和StringBuffer的比较
    接口类、抽象类和普通类的区别
    跟我一起学算法——二项堆
    跟我一起学算法——分治法
    跟我一起学算法——动态规划
    跟我一起学算法——斐波那契堆
    Redis操作三部曲:SpringBoot2.0.X集成Redis + Redis分布式锁 + RedisCacheManager配置
    SpringBoot使用Redis做集中式缓存
  • 原文地址:https://www.cnblogs.com/Sherlock-J/p/12925964.html
Copyright © 2020-2023  润新知