欢迎光顾我的博客
好的,起因是我把最近做好的项目给一部分人进行了测试,发现大部分朋友都提出了同一个问题,你的系统权限管理是如何实现的。我只能尴尬的说一句,不好意思这部分还没开发。然而我也知道,其实对于一个项目来说,权限可以说是最主要的一部分。后端除了对权限进行处理,其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去,顺便把我的学习笔记分享给大家。
而对于权限控制的框架呢,听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的,不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架,可以用在各种场合,甚至非 Web 项目中,由于它的会话独立于容器,后期学习分布式和微服务的时候也比较方便使用。~~最重要的是 Spring 官网用的也是 Shiro 的框架。~~所以还是打算学习 Shiro。
由于这部分内容比较多,我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服,所以我打算把这个内容分成几个部分:初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于初识 Shiro 的,主要是 Shiro 的一些基本特性和架构,以及官网上的小例子。
Apache 软件基金会开发的 Java 安全(权限)框架。
适用于 Java SE 和 Java EE 的环境。
主要功能有:认证、授权、加密、会话管理、集成 Web、缓存等。
功能 | 翻译 | 解释 |
---|---|---|
Authentication | 身份验证/登录 | 验证用户登录时的身份 |
Authorization | 权限验证 | 验证用户拥有哪些权限 |
Session Manager | 会话管理 | 一次登录即一次会话 |
Cryptography | 加密 | 保护数据安全,密码加密存储 |
Web Support | Web支持 | 支持方便的Web集成 |
Caching | 缓存 | 提高查找效率 |
Run As | 身份替换 | 假装成其他用户(若被允许) |
Remember Me | 二次记忆 | 二次登录无需验证 |
初次见面看不懂没有关系的,现在心中有个初步的印象,等到全部学习完整合完你就恍然大悟了。我就简单的讲两个:
首先前两个功能比较容易混淆,到时候写程序千万看清别把方法名写错。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 类的源码。
第二个是用来验证角色所对应的权限。比如说管理员用户拥有增删改查所有权限,游客只拥有查找的权限等等。我们还是通过一段 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 还是比较常用的,但是配置比较简单,照葫芦画瓢就行,不多赘述。
架构 | 翻译 | 解释 |
---|---|---|
Subject | 用户 | 即与应用交互的用户 |
Security Manager | 安全管理 | 管理所有用户(Shiro的心脏) |
Authenticator | 用户验证 | 自定义验证 |
Authorizer | 授权器 | 控制用户不同权限 |
Realm | 安全实体数据源 | 通过JDBC等实现 |
Cache Manager | 缓存控制器 | 加速访问 |
Cryptography | 密码模块 | 密码的加密与解密 |
架构与某些功能相类似,通过这些模块才能实现特定的功能。我也挑两个讲一下:
这里的 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,对于用户不存在、密码错误、账户锁定、未知错误四种异常进行捕获并反馈。
刚刚讲到 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 的建立等等,都是非常重要的内容。
这个架构其实是先前 Authentication 和 Authorization 的结合,通过用户-角色-权限来管理用户所拥有的不同权限。
安全实体数据源又是一个非常重要的内容,我们之前的 doGetAuthenticationInfo( ) 和 doGetAuthorizationInfo( ) 两个自定义获取数据库中的角色和权限信息的方法就是写在这里的,主要使用 JDBC 来操作,用户的授权首先都是要经过这一块关卡的。
其它功能的话,也不能说不重要,没什么必要讲。就好比密码模块,我暂时还没有用到,涉及到盐值加密(salt)、MD5 码校验等等,比较复杂。~~其实是我不会所以不讲。~~还有缓存的话也只要模式化的设定一个缓存即可,如果是在公司一般都有企业内部的缓存方式。
我们从官网把最新的源代码 down 下来或者 clone,研究一下他最简单的实现原理。我们一共需要四部分的文件才能启动一个简单的 helloword 程序。可以在源代码中的 samples 下的 quickstart 中将这四个文件单独拷贝到你的项目中启动。
关于 jar 包如何导入请看 Shiro 整合 SSM 一文。
这是一个日志该如何生成和怎样生成的配置文件。
# 级别从低到高分别为 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 的配置文件,里面设置了用户权限密码等基本信息,只用于测试,一般项目开发不怎么用。
[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
类似于 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 时的所感所悟,以及对一些源码的解析。因为初学,可能有些地方理解没到位或者有偏差,请谅解。欢迎评论指正!求求大家别白嫖了,看过打个卡留个言吧!!!全部原创,真情实感,更新不易,需要动力 :)