推荐网站(不断完善中):个人博客
个人主页:个人主页
相关专栏:CSDN专栏
立志赚钱,干活想躺,瞎分享的摸鱼工程师一枚
在我们实战开发过程中,对于权限的控制是必不可少的,一个系统中常见的有
普通会员、管理员、超级管理员
等等不同的角色出现。我们如何更优雅的在Java中使用权限框架?来看看Java中比较火热的权限框架之一
shiro
吧~
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
使用shiro可以非常快速的完成认证、授权等功能的开发,降低系统成本。
官方地址
shiro中的三大核心组建:Subject、SecurityManager、Realm
其他常见功能组件
Authentication:身份认证/登录,验证用户是不是拥有相应的身份
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用 户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
SessionManager:SessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDao:SessionDAO即会话dao,是对session会话操作的一套接口,比如要将会话信息存储到redis数据库
CacheManager:CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能
Cryptography:即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能
权限管理的范围应该是怎么样的?
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源
权限管理包括身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问
关于认证:身份认证,就是判断一个用户是否为合法用户的处理过程
权限认证流程图
在权限中无非就这么几个概念内容为:主体、资源、权限(Permission)
模型图
主体(账号、密码)、 资源(资源名称、访问地址)、权限(权限名称、资源id)、 角色(角色名称)
角色和权限关系(角色id、权限id)、 主体和角色关系(主体id、角色id)
实际开发常见模型图
实际企业开发中会将资源与权限表合为一张表。
资源(资源名称、访问地址) 权限(权限名称、资源id)合并为:资源(权限名称、资源名称、资源访问地址)
对资源类型的管理称为粗颗粒度权限管理,即只控制到菜单、按钮、方法等
例如:用户具有用户管理的权限,具有导出订单明细的权限
对资源实例的控制称为细颗粒度权限管理,即控制到数据级别的权限
例如:用户只允许修改本部门的员工信息,用户只允许导出自己创建的订单明细
对于粗颗粒度的权限管理可以很容易做系统架构级别的功能,即系统功能操作使用统一的粗颗粒度的权限管理。
对于细颗粒度的权限管理不建议做成系统架构级别的功能,因为对数据级别的控制是系统的业务需求,随着业务需求的变更业务功能变化的可能性很大,建议对数据级别的权限控制在业务层个性化开发,
例如:用户只允许修改自己创建的商品信息可以在service接口添加校验实现,service接口需要传入当前操作人的标识,与商品信息创建人标识对比,不一致则不允许修改商品信息
基于url拦截是企业中常用的权限管理方法,实现思路是:将系统操作的每个url配置在权限表中,将权限对应到角色,将角色分配给用户,用户访问系统功能通过Filter进行过虑,过虑器获取到用户访问的url,只要访问的url是用户分配角色中的url则放行继续访问
pom.xml文件
<dependencies>
<dependency>
<groupId>commons-logginggroupId>
<artifactId>commons-loggingartifactId>
<version>1.2version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.3.2version>
dependency>
dependencies>
日志配置
log4j.rootLogger=debug,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c %m %n
shiro.ini配置
shiro.ini是shiro默认可以识别的配置文件,将与权限相关的内容写在shiro.ini文件中可以直接被加载
通过shiro.ini配置文件初始化SecurityManager环境
[users]
zhangsan=123456,admin
lisi=654321,superadmin
[roles]
admin=product:create,product:update,product:delete
superadmin=product:create,product:update,product:delete,user:delete
构建一个SecurityManager环境的流程
@Test
public void shiro(){
//通过shiro.ini初始化一个SecurityManager工厂
IniSecurityManagerFactory iniSecurityManagerFactory=new IniSecurityManagerFactory("classpath:shiro.ini");
//通过工厂获取一个SecurityManager管理器
SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
//使用SecurityUtils将securityManager设置到运行环境中
SecurityUtils.setSecurityManager(securityManager);
//创建一个Subject实例
Subject subject = SecurityUtils.getSubject();
//创建token令牌,记录用户登陆的身份和凭证即账号密码
UsernamePasswordToken user = new UsernamePasswordToken("zhangsan", "123456");
//登陆认证(如果认证不通过回抛异常)
subject.login(user);
//查询认证状态
System.out.println(subject.isAuthenticated());
//查询用户的授权信息
System.out.println("是否有删除用户的权限" + subject.isPermitted("user:delete"));
System.out.println("是否有添加产品的权限" + subject.isPermitted("product:create"));
//查询用户的授权角色信息
System.out.println("是否拥有admin的角色权限" + subject.hasRole("admin"));
System.out.println("是否拥有superadmin的角色权限" + subject.hasRole("superadmin"));
//退出登陆
subject.logout();
//退出登陆之后再查看权限
System.out.println(subject.isAuthenticated());
System.out.println("是否有删除用户的权限" + subject.isPermitted("user:delete"));
System.out.println("是否有添加产品的权限" + subject.isPermitted("product:create"));
}
UnknownAccountException:账号不存在
IncorrectCredentialsException:输入密码错误
DisabledAccountException:账号被禁用
LockedAccountException:登陆失败次数过多
ExpiredCredentialsException:凭证过期
上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm,实现自定义Realm一般继承 AuthorizingRealm(授权)即可;其继承了AuthenticatingRealm(即身份验证),而且也间接继承了CachingRealm(带有缓存实现)
用户
public class User {
//用户名、密码、用户状态
private String username;
private String password;
//0锁定、1正常
private String status;
}
自定义Realm认证部分代码
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从令牌中获取到用户输入的用户名
String usernmae = (String) token.getPrincipal();
//获取用户输入的密码(因为获取的是二进制需要转换)
String password = new String ((char[]) token.getCredentials());
//模拟从数据库中获取对应的用户数据
User admin = new User("admin", "123456","1");
//进行信息比对
if (!usernmae.equals(admin.getUsername())) {
throw new UnknownAccountException("用户不存在");
}
if (!password.equals(admin.getPassword())){
throw new IncorrectCredentialsException("密码错误");
}
if ("0".equals(admin.getStatus())){
throw new LockedAccountException("账户被锁定,请联系管理员");
}
//如果没有异常则表示认证通过则返回一个简单的认证数据模型
return new SimpleAuthenticationInfo(usernmae, password, getName());
}
自定义Realm授权部分代码
**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//初始化一个简单授权对象
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//给授权对象设置权限
//设置角色
Set<String> roles = new HashSet<>();
//模拟从数据库的角色表中获取角色添加信息
roles.add("admin");
roles.add("superadmin");
//设置权限
Set<String> permissions=new HashSet<>();
permissions.add("user:create");
permissions.add("user:delete");
permissions.add("user:update");
//添加到授权信息中
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
关于Shiro.ini配置
在shiro.ini配置中需要将内容进行更新,不再使用手动进行认证,标明自定义Realm的类路径,使用自定义Realm。
#[users]
#zhangsan=123456,admin
#lisi=654321,superadmin
#
#[roles]
#admin=product:create,product:update,product:delete
#superadmin=product:create,product:update,product:delete
#自定义realm
shiroRealm=com.imcode.common.shiro.ShiroRelam
#将realm设置到securityManager
securityManager.realms=$shiroRealm
关于测试
@Test
public void demo02(){
//通过shiro.ini初始化一个SecurityManager工厂
IniSecurityManagerFactory iniSecurityManagerFactory=new IniSecurityManagerFactory("classpath:shiro.ini");
//通过工厂获取一个SecurityManager管理器
SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
//使用SecurityUtils将securityManager设置到运行环境中
SecurityUtils.setSecurityManager(securityManager);
//创建一个Subject实例
Subject subject = SecurityUtils.getSubject();
//创建token令牌,记录用户登陆的身份和凭证即账号密码
UsernamePasswordToken user = new UsernamePasswordToken("admin", "123456");
//登陆
subject.login(user);
//测试
System.out.println(subject.isPermitted("user:create"));
System.out.println(subject.hasRole("admin"));
}
Shiro将Permission定义为定义显式行为或操作的语句。它是应用程序中原始功能的声明,仅此而已。权限是安全策略中最低级别的构造,它们只显式定义应用程序可以执行的操作。
假设您希望添加对公司打印机的访问权限,以限制某些人可以打印到特定的打印机,而其他人可以查询当前队列。
一种非常简单的方法是授予用户“queryPrinter”权限。然后,您可以调用查看权限的方法来判断用户是否具有queryPrinter的权限:
subject.isPermitted("queryPrinter")
资源标识符:操作:对象实例 ID 即对哪个资源的哪个实例可以进行什么操作.
其默认支持通配符权限字符串,:
表 示资源/操作/实例的分割;,
表示操作的分割,*
表示任意资源/操作/实例。
案例
例如:user:query、user:edit
– 冒号是一个特殊字符,它用来分隔权限字符串的下一部件:第一部分是权限被操作的领域,第二部分是被执行的操作
。
多个值
每个部分都可以包含多个值。因此,您不必向用户授予“printer:print”和“printer:query”权限,而只需授予他们一个权限:
printer:print,query
全部值
如果您想授予用户特定部分中的所有值,该怎么办?这样做比手动列出每个值更方便。同样,基于通配符,我们可以做到这一点。如果printer域有3个可能的行动(query,print,和manage),这一点
printer:query,print,manage
可以变成
printer:*
散列算法一般用于生成一段文本的摘要信息,散列算法不可逆,将内容可以生成摘要,无法将摘要转成原始内容。散列算法常用于对密码进行散列,常用的散列算法有MD5、SHA。
一般散列算法需要提供一个salt(盐)与原始内容生成摘要信息,这样做的目的是为了安全性,比如:111111的md5值是:96e79218965eb72c92a549dd5a330112,拿着“96e79218965eb72c92a549dd5a330112”去md5破解网站很容易进行破解,如果要是对111111和salt(盐,一个随机数)进行散列,这样虽然密码都是111111,但是加不同的盐会生成不同的散列值。
案例代码
@Test
public void test() {
//md5加密,不加盐
String password_md5 = new Md5Hash("111111").toString();
System.out.println("password_md5=" + password_md5);
//md5加密,加盐
String password_md5_salt_1 = new Md5Hash("111111", "imcode", 1).toString();
System.out.println("password_md5_salt_1=" + password_md5_salt_1);
//两次散列相当于md5(md5())
String password_md5_salt_2 = new Md5Hash("111111", "imcode", 2).toString();
System.out.println("password_md5_salt_2=" + password_md5_salt_2);
}
可以将shiro自带的加密类写成一个通用类。
public class MD5Util {
// 散列次数
private static int hashIterations = 3;
/**
* md5加密工具类
*/
public static String md5(String source, String salt) {
return new Md5Hash(source, salt, hashIterations).toString();
}
}
一般在数据库中的密码我们通常不会以明文的方式进行显示,会通过加密的方式加强密码的安全度。
@Test
public void test02(){
//对密码进行加密
ShiroUtil.login("admin",MD5Util.md5("123456","ak47"));
Subject subject = SecurityUtils.getSubject();
System.out.println(subject.hasRole("admin"));
System.out.println(subject.isPermitted("user:create"));
}
利用缓存可以提高效率,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
每次查询用户的权限的时候都会调用一次shiro的授权方法,使用缓存可以实现只有在用户认证成功的时候调用一下授权方法,后续不再调用该方法。我们使用ehcache作缓存.
pom.xml
引入shiro对ehcache的支持包和ehcache包
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-ehcacheartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>net.sf.ehcachegroupId>
<artifactId>ehcacheartifactId>
<version>2.10.5version>
dependency>
shiro.ini文件
#缓存管理器配置
cacheManager=org.apache.shiro.cache.ehcache.EhCacheManager
securityManager.cacheManager=$cacheManager
测试
@Test
public void test03(){
//对密码进行加密
ShiroUtil.login("admin",MD5Util.md5("123456","ak47"));
Subject subject = SecurityUtils.getSubject();
System.out.println(subject.hasRole("admin"));
System.out.println(subject.isPermitted("user:create"));
//退出登录
subject.logout();
ShiroUtil.login("admin",MD5Util.md5("123456","ak47"));
Subject subject2 = SecurityUtils.getSubject();
System.out.println(subject2.hasRole("admin"));
System.out.println(subject2.isPermitted("user:create"));
}
//测试发现,用户登录以后授权信息会被缓存,用户退出以后缓存的授权信息清空
以上内容为
Shiro框架
的初步入门,以及一些Shiro权限
的小Demo案例代码的学习过程中,手动实践才是最为重要的,多动手远胜多看。