上篇中,咱们开始探究了 RuoYi 项目是怎么进行登录信息传输、验证码校验、暗码校验,以及暗码存储安全性方案。咱们了解到,整个的验证完成是围绕 Shiro 结构进行的,而数据的传输安全性,RuoYi 是没有考虑的,假如咱们做的是要求安全等级比较高的项目,需求考虑选用 https 协议,并对要害数据进行加密后传输,一般会运用非对称暗码算法进行加解密。

本篇,咱们首要会针对 Shiro 结构做一个简略的扩展了解,然后再初窥 RuoYi 的菜单、权限功用。

Shiro

Shiro 的官网为 Apache Shiro,GitHub 仓库为 Shiro。

Shiro 的自我介绍为:Apache Shiro™ 是一个功用强大且易于运用的 Java 安全结构,可履行身份验证、授权、加密和会话管理。运用 Shiro 易于了解的 API,你能够快速轻松地维护任何运用程序——从最小的移动运用程序到最大的 web 和企业运用程序。

Shiro 高层架构,如下图:

最火后台管理系统 RuoYi 项目探秘,之三

Shiro 有三个首要概念:SubjectSecurityManagerRealms。这几个概念,咱们在上篇中对 RuoYi 的登录剖析中已有所接触。

三大概念,官方大体的介绍如下:

  • Subject:本质上是当前履行用户的特定于安全的“视图”,它代表的能够是人,也能够表示第三方服务、守护程序帐户、cron job 或任何类似的东西。
  • SecurityManagerSecurityManager 是 Shiro 架构的中心,它充当了一种“维护伞”目标,和谐其内部安全组件,这些组件共同构成一个目标图。但是,一旦为一个运用程序装备了 SecurityManager 及其内部目标图,它一般就不存在了,运用程序开发人员几乎一切的时间都在运用 Subject API。
  • RealmsRealms 充当 Shiro 和运用程序安全数据之间的“桥梁”或“连接器”。当需求与安全相关数据(如用户帐户)进行实际交互以履行身份验证(登录)和授权(拜访操控)时,Shiro 会从为运用程序装备的一个或多个 Realms 中查找许多这些内容。从这个意义上讲,Realm 本质上是一个特定于安全的 DAO。

从以上的概念描绘中,能够看出来,Subject 代表认证相关的人或者运用;SecurityManager 是用于认证的中心及桥梁;Realms 则代表用于认证的相关数据及认证办法的供给者。

再看一下 Shiro 的具体架构,如下图:

最火后台管理系统 RuoYi 项目探秘,之三

这张图能够看到更多关于 SecurityManager 的组成部分,以及常见的 Realms 的认证数据来历,具体咱们不再展开。

RuoYi 里的 Shiro

RuoYi 项目里对接运用 Shiro 的代码,放在项目 ruoyi-framework 中, 包为 com.ruoyi.framework.shiro。其间触及界说的 Realms 完成类 UserRealm,将类继承自 AuthorizingRealmAuthorizingRealm 是一个笼统类,其间笼统办法为:

    /**
     * Retrieves the AuthorizationInfo for the given principals from the underlying data store.  When returning
     * an instance from this method, you might want to consider using an instance of
     * {@link org.apache.shiro.authz.SimpleAuthorizationInfo SimpleAuthorizationInfo}, as it is suitable in most cases.
     *
     * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved.
     * @return the AuthorizationInfo associated with this principals.
     * @see org.apache.shiro.authz.SimpleAuthorizationInfo
     */
    protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

同时,AuthorizingRealm 还持续自 AuthenticatingRealm,它也是一个笼统类,其笼统办法为:

    /**
     * Retrieves authentication data from an implementation-specific datasource (RDBMS, LDAP, etc) for the given
     * authentication token.
     * <p/>
     * For most datasources, this means just 'pulling' authentication data for an associated subject/user and nothing
     * more and letting Shiro do the rest.  But in some systems, this method could actually perform EIS specific
     * log-in logic in addition to just retrieving data - it is up to the Realm implementation.
     * <p/>
     * A {@code null} return value means that no account could be associated with the specified token.
     *
     * @param token the authentication token containing the user's principal and credentials.
     * @return an {@link AuthenticationInfo} object containing account data resulting from the
     *         authentication ONLY if the lookup is successful (i.e. account exists and is valid, etc.)
     * @throws AuthenticationException if there is an error acquiring data or performing
     *                                 realm-specific authentication logic for the specified <tt>token</tt>
     */
    protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

UserRealm 对两个笼统办法的完成别离如下:

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0)
    {
        SysUser user = ShiroUtils.getSysUser();
        // 人物列表
        Set<String> roles = new HashSet<String>();
        // 功用列表
        Set<String> menus = new HashSet<String>();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 管理员拥有一切权限
        if (user.isAdmin())
        {
            info.addRole("admin");
            info.addStringPermission("*:*:*");
        }
        else
        {
            roles = roleService.selectRoleKeys(user.getUserId());
            menus = menuService.selectPermsByUserId(user.getUserId());
            // 人物参加AuthorizationInfo认证目标
            info.setRoles(roles);
            // 权限参加AuthorizationInfo认证目标
            info.setStringPermissions(menus);
        }
        return info;
    }
    /**
     * 登录认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
    {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        String password = "";
        if (upToken.getPassword() != null)
        {
            password = new String(upToken.getPassword());
        }
        SysUser user = null;
        try
        {
            user = loginService.login(username, password);
        }
        catch (CaptchaException e)
        {
            throw new AuthenticationException(e.getMessage(), e);
        }
        catch (UserNotExistsException e)
        {
            throw new UnknownAccountException(e.getMessage(), e);
        }
        catch (UserPasswordNotMatchException e)
        {
            throw new IncorrectCredentialsException(e.getMessage(), e);
        }
        catch (UserPasswordRetryLimitExceedException e)
        {
            throw new ExcessiveAttemptsException(e.getMessage(), e);
        }
        catch (UserBlockedException e)
        {
            throw new LockedAccountException(e.getMessage(), e);
        }
        catch (RoleBlockedException e)
        {
            throw new LockedAccountException(e.getMessage(), e);
        }
        catch (Exception e)
        {
            log.info("对用户[" + username + "]进行登录验证..验证未经过{}", e.getMessage());
            throw new AuthenticationException(e.getMessage(), e);
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }

能够看到,doGetAuthorizationInfo(PrincipalCollection arg0) 代码便是取得一个 SysUser 目标,并给定相应的菜单和用户人物。也便是说用户和权限和菜单绑定都是在这儿完成的,而其间最中心的用户数据是从从而来呢?持续看以下中心的一句代码,并盯梢进去:

SysUser user = ShiroUtils.getSysUser();
public static SysUser getSysUser()
{
    SysUser user = null;
    Object obj = getSubject().getPrincipal();
    if (StringUtils.isNotNull(obj))
    {
        user = new SysUser();
        BeanUtils.copyBeanProp(user, obj);
    }
    return user;
}

能够看到,用户信息来历于 getPrincipal(),而它来自于 getProject(),持续跟进,能够找到:

public class ShiroUtils
{
    public static Subject getSubject()
    {
        return SecurityUtils.getSubject();
    }
...

跟进,找到 SecurityUtils 类里的代码完成:

public static Subject getSubject() {
    Subject subject = ThreadContext.getSubject();
    if (subject == null) {
        subject = (new Subject.Builder()).buildSubject();
        ThreadContext.bind(subject);
    }
    return subject;
}

此处逻辑为:从 ThreadContext.getSubject() 中获取 Subject,假如其为 null 则直接构建一个目标,并存入 ThreadContext 中。进入 buildSubject() 的逻辑,代码如下:

public Subject buildSubject() {
  return this.securityManager.createSubject(this.subjectContext);
}

咱们再跟进 UserRealm 另一个完成的办法 doGetAuthenticationInfo(AuthenticationToken token),能够看到此办法完成了真正的登录认证,将待认证信息与认证源的数据进行认证对比,承认用户是否能够认证经过。

这个时候咱们重新回到 RuoYi 事务中的登录办法 ajaxLogin,调查登录事务到底是怎么经过 Shiro 结构完成的:

UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
try
{
    subject.login(token);
    return success();
}

这儿能够很明晰的看到,此处运用了上文提到的办法 SecurityUtils.getSubject() 来生成 Subject,然后对生成的 Subject 目标履行登录操作。那此处逻辑就很明晰了:在项目初始化装备时,SecurityManager 就已经与 UserRealm 提前绑定,当用户触发到登录时,代码中运用 SecurityManager 生成了一个 Subject 目标,再经过 Subject 界说的 login 办法进行了登录操作,而 login 办法的调用,终究会运转到咱们上面剖析到的 UserRealm 中的 doGetAuthenticationInfo(AuthenticationToken token) 办法,终究登录成功的话,Shiro 结构会为 Subject 目标增加认证用户的相关信息。

经过以上的代码整理和剖析,咱们能比较明晰地体会到 Shiro 三大中心概念的用途。Realm 便是用来比较认证信息是否合法的,中心便是供给认证源用于对比;SecurityManagerRealm 提前绑定,供给认证 API 给事务运用,事务 Subjectlogin 办法,完成终究经过 SecurityManger 调用 Realm 中的认证办法进行登录,并赋予 Subject 目标相关数据,终究可经过 SecurityManager 取得用户相关数据目标 Subject,并能从 Subject 中获取自己需求的各种用户信息。

初见 RBAC

在上面登录逻辑中,咱们看到在 UserRealm 中登录成功后,对用户进行菜单和权限的绑定操作。但比较奇怪的是这段代码,让人感觉疑惑:

roles = roleService.selectRoleKeys(user.getUserId());
menus = menuService.selectPermsByUserId(user.getUserId());
// 人物参加AuthorizationInfo认证目标
info.setRoles(roles);
// 权限参加AuthorizationInfo认证目标
info.setStringPermissions(menus);

咱们很明显的能够看到 info 目标的两个办法为 setRoles()(设置人物)、SetStringPermissions()(设置权限),但作者在这处对设置权限函数增加的数据,命名为 menus(菜单)。咱们只能有一个开始猜测:RuoYi 体系中,是否没有针对 API 级别的权限操控,而只是针对菜单级进行了操控呢?

别的,SysRole 都有些什么特点,又是怎么对用户操作进行权限操控的呢?菜单是什么样的数据结构,又是怎么进行拜访操控的呢?这些 RBAC 相关疑问的解答,咱们将在下篇展开。

小结

(读太多文字人会太累,所以本篇在 RBAC 预备展开时戛但是止)本篇咱们简略的扩展了一下 Shiro 结构的相关知识,并结合 Shiro 结构简略剖析了 RuoYi 项目怎么对接运用的 Shiro 结构。

别的,咱们还在对接代码中发现了 RBAC 比较中心的权限、菜单等数据。RBAC 神秘的面纱才刚揭开,咱们下一篇持续。本篇就到这儿,比心,❤。