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 – 百度百科

猜你喜欢

转载自blog.csdn.net/zx48822821/article/details/80503742