开端

好的,起因是我把最近做好的项目给一部分人进行了测试,发现大部分朋友都提出了同一个问题,你的系统权限管理是如何实现的。我只能尴尬的说一句,不好意思这部分还没开发。然而我也知道,其实对于一个项目来说,权限可以说是最主要的一部分。后端除了对权限进行处理,其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去,顺便把我的学习笔记分享给大家。

而对于权限控制的框架呢,听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的,不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架,可以用在各种场合,甚至非 Web 项目中,由于它的会话独立于容器,后期学习分布式和微服务的时候也比较方便使用。最重要的是 Spring 官网用的也是 Shiro 的框架。所以还是打算学习 Shiro。

由于这部分内容比较多,我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服,所以我打算把这个内容分成几个部分:初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于初识 Shiro 的,主要是 Shiro 的一些基本特性和架构,以及官网上的小例子。

 # Shiro 简介

  • Apache 软件基金会开发的 Java 安全(权限)框架
  • 适用于 Java SE 和 Java EE 的环境。
  • 主要功能有:认证、授权、加密、会话管理、集成 Web、缓存等。
[官网下载]: shiro.apache.org

Shiro 具体功能

功能翻译解释
Authentication身份验证/登录验证用户登录时的身份
Authorization权限验证验证用户拥有哪些权限
Session Manager会话管理一次登录即一次会话
Cryptography加密保护数据安全,密码加密存储
Web SupportWeb支持支持方便的Web集成
Caching缓存提高查找效率
Run As身份替换假装成其他用户(若被允许)
Remember Me二次记忆二次登录无需验证

初次见面看不懂没有关系的,现在心中有个初步的印象,等到全部学习完整合完你就恍然大悟了。我就简单的讲两个:

Authentication

首先前两个功能比较容易混淆,到时候写程序千万看清别把方法名写错。Authentication 是身份的验证,我们对于权限的结构划分是基于 RBAC 模型来的(具体可以见我的另一篇博文),所以会有用户、角色、权限三层数据表。而这个功能就是判断用户的角色,是管理员、运维、开发还是测试等等。我们来看一段 Realm 中的方法 doGetAuthenticationInfo:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String userName = token.getUsername();
        User user = userService.selectByName(userName);
        return new SimpleAuthenticationInfo(
            user.getUsrName(),
            user.getUsrPassword(),
            getName()
        );
}

这段方法中首先获取用户身份 token 参数,然后获取用户名。使用我自定义的 Mapper 中的 selectByName( ) 在用户数据表中查询数据,最后返回一个 Simple 身份验证信息,参数为用户名、密码和 CachingRealm 的名字。当然正统的方式是把用户对象直接传给他,但是我测试的时候发现传用户名和密码出错的可能性更低,具体可以去看看 SimpleAuthenticationInfo 类的源码。

Authorization

第二个是用来验证角色所对应的权限。比如说管理员用户拥有增删改查所有权限,游客只拥有查找的权限等等。我们还是通过一段 Java 代码来理解这个功能。

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        String userName = (String)principalCollection.getPrimaryPrincipal();
        if (userName == null) {
            LOGGER.error("授权失败,用户信息为空!");
            return null;
        }
        try {
            Set<String> roles = roleService.findRoleByUserName(userName);
            simpleAuthorizationInfo.addRoles(roles);
            for (String role : roles) {
                Set<String> permissions = permissionService.findPermissionByRole(role);
                simpleAuthorizationInfo.addStringPermissions(permissions);
            }
            return simpleAuthorizationInfo;
        } catch (Exception e) {
            LOGGER.error("授权失败,系统内部错误!");
        }
        return simpleAuthorizationInfo;
    }

首先 new 一个简单权限验证信息对象(SimpleAuthorizationInfo),使用 getPrimaryPrincipal( ) 方法从函数参数中获取用户名。当用户名存在时,调用自定义的 Mapper 中的 findRoleByUserName( ) 在 sys-user-role 表中找到该用户所对应的角色,这个角色可以是一对多的(我的项目里是一对一的)。之后在简单权限验证信息对象中把角色添加进去。对于每一个角色都赋予了多个权限,所以接下来使用 foreach 循环再次调用 findPermissionByRole( ) 在 sys-role-permission 表中找到每一个角色对应的权限,然后同样加入到简单权限验证信息对象中。至此,获取系统内部角色和权限并把这些信息告诉 Shiro 的步骤就完成了。这个功能中的代码是比较重要的,涉及了用户的数据表和 Mybatis 的操作,因人而异。

其它

其他功能的话,Session Manager 和 Remember Me 还是比较常用的,但是配置比较简单,照葫芦画瓢就行,不多赘述。

Shiro 具体架构

架构翻译解释
Subject用户即与应用交互的用户
Security Manager安全管理管理所有用户(Shiro的心脏)
Authenticator用户验证自定义验证
Authorizer授权器控制用户不同权限
Realm安全实体数据源通过JDBC等实现
Cache Manager缓存控制器加速访问
Cryptography密码模块密码的加密与解密

架构与某些功能相类似,通过这些模块才能实现特定的功能。我也挑两个讲一下:

Subject

这里的 Subject 不是课程,我第一次看到也以为是课程,这个类和我项目中的 Subject 类竟然一样,import 的时候差点搞错。我们看一段登录接口的代码:

@PostMapping(value = "/login")
@ResponseBody
public Response<User> userLogin(@RequestBody User sysUser) {
        User result;
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(
          sysUser.getUsrName(), sysUser.getUsrPassword()
        );
        token.setRememberMe(true);
        try {
            subject.login(token);
            result = userService.userLogin(sysUser.getUsrName(), sysUser.getUsrPassword());
            session.setAttribute("user", subject);
        } catch (UnknownAccountException e) {
            return getFailResult(404, "Message not found");
        } catch (IncorrectCredentialsException e) {
            return getFailResult(412, "Incorrect message");
        } catch (LockedAccountException e) {
            return getFailResult(401, "Account locked");
        } catch (Exception e) {
            e.printStackTrace();
            return getFailResult(408, "Unknown error");
        }
        return getSuccessResult(result);
    }

这里我们定义了一个 Subject 类的对象,然后使用 SecurityUtils.getSubject( ) 获取当前登录用户的信息。可以 conmand + 单击这个类看一下源码,原来 SecurityUtils 中放了一个 SecurityManager 对象,也就是第二部分的架构,这部分下面再讲。然后我们通过前端获取到的用户名和密码 new 一个 UsernamePasswordToken 类的对象,生成一个 token。最后使用这个唯一的 token 进行 login,对于用户不存在、密码错误、账户锁定、未知错误四种异常进行捕获并反馈。

Security Manager

刚刚讲到 SecurityUtils 中放了一个 SecurityManager 对象,而 SecurityManager 对象非常的简单,就只包含登录、退出、创建用户三个方法。

public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
    Subject login(Subject var1, AuthenticationToken var2) throws AuthenticationException;
    void logout(Subject var1);
    Subject createSubject(SubjectContext var1);
}

这个架构是 Shiro 的心脏,可以用来管理所有用户。在第三个方法中提供了用户信息上下文的建立与保存,session 和 principals 的建立等等,都是非常重要的内容。

Authorizer

这个架构其实是先前 Authentication 和 Authorization 的结合,通过用户-角色-权限来管理用户所拥有的不同权限。

Realm

安全实体数据源又是一个非常重要的内容,我们之前的 doGetAuthenticationInfo( ) 和 doGetAuthorizationInfo( ) 两个自定义获取数据库中的角色和权限信息的方法就是写在这里的,主要使用 JDBC 来操作,用户的授权首先都是要经过这一块关卡的。

其它

其它功能的话,也不能说不重要,没什么必要讲。就好比密码模块,我暂时还没有用到,涉及到盐值加密(salt)、MD5 码校验等等,比较复杂。其实是我不会所以不讲。还有缓存的话也只要模式化的设定一个缓存即可,如果是在公司一般都有企业内部的缓存方式。

官网示例

我们从官网把最新的源代码 down 下来或者 clone,研究一下他最简单的实现原理。我们一共需要四部分的文件才能启动一个简单的 helloword 程序。可以在源代码中的 samples 下的 quickstart 中将这四个文件单独拷贝到你的项目中启动。

jar 包

关于 jar 包如何导入请看 Shiro 整合 SSM 一文。

log4j.properties

这是一个日志该如何生成和怎样生成的配置文件。

# 级别从低到高分别为 DEBUG INFO WARN ERROR FATAL,INFO 代表重要信息
log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries WARN 代表警告信息
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

我们可以看到,对于不同的文件产生的输出。

shiro.ini

这是一个 Shiro 的配置文件,里面设置了用户权限密码等基本信息,只用于测试,一般项目开发不怎么用。

[users]
# 以下示例配置了5位用户,等号后分别是密码和用户对应的角色,比如管理员、访客等等
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

[roles]
# 以下示例配置了3种觉得,等号后是他们所拥有的权限,*代表全部,:代表内含权限,而第三种的三个参数分别代表用户角色、用户权限、允许操作的实例号
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

Quickstart.java

类似于 Spring 的 Application,这是一个启动类。在启动类中我们也可以看到 IDEA 报了个错,说 Shiro 的工厂类已经过时,不推荐使用 ini 的形式配置。看一下主函数中一些重要的代码吧:

// 通过工厂导入ini配置
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
// 创建单例
SecurityUtils.setSecurityManager(securityManager);
// 获取当前的角色
Subject currentUser = SecurityUtils.getSubject();
// 测试session
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
// 验证用户是否登录
if (!currentUser.isAuthenticated()) {
      // 封装用户名密码
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
    token.setRememberMe(true);
    // 登录成功
    try {
        currentUser.login(token);
    } 
    // 用户名不存在异常
    catch (UnknownAccountException uae) {
        log.info("There is no user with username of " + token.getPrincipal());
    } 
    // 用户名存在,密码错误异常
    catch (IncorrectCredentialsException ice) {
        log.info("Password for account " + token.getPrincipal() + " was incorrect!");
    } 
    // 用户锁定异常
    catch (LockedAccountException lae) {
        log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                "Please contact your administrator to unlock it.");
    }
    // 其他异常... catch more exceptions here (maybe custom ones specific to your application?
    catch (AuthenticationException ae) {
        //unexpected condition?  error?
    }
}
// 测试是否有该角色 test a role:
if (currentUser.hasRole("schwartz")) {
    log.info("May the Schwartz be with you!");
} else {
    log.info("Hello, mere mortal.");
}
// 测试是否有该权限 test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {
    log.info("You may use a lightsaber ring.  Use it wisely.");
} else {
    log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

好啦,大家可以自行到官网下载实例本地测试,看源码是一个能够最快理解程序的方式。

总结

这篇博文从我的项目需要整合 Shiro 开始讲起,主要是写了我在初学 Shiro 时的所感所悟,以及对一些源码的解析。因为初学,可能有些地方理解没到位或者有偏差,请谅解。欢迎评论指正!求求大家别白嫖了,看过打个卡留个言吧!!!全部原创,真情实感,更新不易,需要动力 :)

最后修改:2022 年 05 月 27 日
随意