权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户 首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问.
权限管理系统的模型 RBAC基于角色的权限访问控制 if(user.hasRole("总经理")){ 允许访问; }else{ 不允许访问; } RBAC基于资源的权限访问控制 if(user.isPermitted("user:create")){ 允许访问; }else{ 不允许访问; }
在RBAC模型中,who、what、how构成了访问权限三元组
认证流程中的关键对象
Subject主体,访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
Principal身份信息,是主体subject进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
credential凭证信息,是只有主体自己知道的安全信息,如密码、证书等
授权的关键对象
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的
Who即主体Subject,主体需要访问系统中的资源 What即资源Resource,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例 How权限/许可Permission,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可 权限分为粗颗粒和细颗粒,粗颗粒权限是指对资源类型的权限,细颗粒权限是对资源实例的权限
权限的数据模型 主体(账号、密码) 资源(资源名称、访问地址) 权限(权限名称、资源id) 角色(角色名称) 角色和权限关系(角色id、权限id) 主体和角色关系(主体id、角色id)
一般具体应用中会将资源和权限定义在一起
主体(账号、密码)
角色(角色名称)
权限(权限名称、资源名称、访问地址)
三者之间是多对多的关系
身份认证,就是判断一个用户是否为合法用户的处理过程 // 第一步:构建SecurityManager工厂,解析ini文件 Factory fac = new IniSecurityManagerFactory("classpath:test1/shiro.ini"); // 第二步:创建SecurityManager SecurityManager sm = fac.getInstance(); // 第三步:注册对应的SecurityManager SecurityUtils.setSecurityManager(sm); // 第四步:获取对应的subject Subject currentUser = SecurityUtils.getSubject(); //第五步:创建token,包含用户信息 UsernamePasswordToken token=new UsernamePasswordToken("yanjun1","1234567"); //第六步:调用subject的login方法进行登录 currentUser.login(token);//如果登录失败,则报对应的异常 //常见异常有:IncorrectCredentialsException口令错误,UnknownAccountException账户不存在 //LockedAccountException账户已锁定 if(currentUser.isAuthenticated()) {//判断subject是否登录成功 System.out.println("登录成功!"); } currentUser.logout();//退出登录
添加日志系统slf4j-log4j 1、添加日志的配置信息 log4j.properties log4j.rootLogger=debug, 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
2、添加对应的依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
认证执行流程 1、创建token令牌,token中有用户提交的认证信息即账号和密码 2、执行subject.login(token),最终由securityManager通过Authenticator 进行认证 3、Authenticator的实现ModularRealmAuthenticator调用realm从ini配置文件取用户真实的账号和密码,这里使用的是IniRealm(shiro自带) 4、IniRealm先根据token中的账号去ini中找该账号,如果找不到则给ModularRealmAuthenticator返回null,如果找到则匹配密码,匹配密码成功则认证通过
?Realm Realm域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说 SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身 份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;
Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要
从系统的数据库中读取用户信息,所以需要自定义realm
使用Shiro自带的JdbcRealm
[main]
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.mchange.v2.c3p0.ComboPooledDataSource
dataSource.driverClass=com.mysql.jdbc.Driver
dataSource.user=root
dataSource.password=123456
dataSource.jdbcUrl=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
jdbcRealm.dataSource=jdbcRealm
用户自定义Realm 1、实现Realm接口,必须编码完成用户名和口令的验证 2、继承抽象父类AuthenticatingRealm 3、建议使用AuthorizingRealm,编码只进行用户名称的验证,口令的验证是父类实现的 doGetAuthenticationInfo用于实现认证 doGetAuthorizationInfo用于实现授权
public class MyRealm extends AuthorizingRealm {
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
return null;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) arg0;
String username = token.getUsername();
// 执行按照用户名称查询对应的用户信息
AuthenticationInfo info = new SimpleAuthenticationInfo(username, "123456", this.getName());// 注意这里的123456是从数据库中查询到的数据
return info;
}
}
在shiro.ini中进行配置
[main]
myRealm=com.yan.MyRealm
securityManager.realms=$myRealm
具体的自定义realm的编程方法:
最基础的是Realm接口,CachingRealm负责缓存处理,AuthenticationRealm负责认证,
AuthorizingRealm负责认证和授权,通常自定义的realm继承AuthorizingRealm
多Realm认证策略
Shiro提供了3个具体的AuthenticationStrategy实现:
AtLeastOneSuccessfulStrategy如果一个或更多验证成功,则整体的尝试被认为是成功的。
如果没有一个验证成功,则整体失败。就是至少有一个Realm的验证是成功的算才认证通过,否则认证失败
FirstSuccessfulStrategy第一个Realm成功验证返回的信息将被使用,其他的Realm将被忽
略。如果没有一个Realm验证成功,则整体失败。
AllSuccessfulStrategy所有配置的Realm都必须验证成功才算认证通过,否则认证失败。
默认认证器ModularRealmAuthenticator的默认认证策略AtLeastOneSuccessfulStrategy
散列算法 散列算法一般用于生成一段文本的摘要信息,散列算法不可逆,将内容可以生成摘要,无法将摘要转成原始内容 。散列算法常用于对密码进行散列,常用的散列算法有MD5、SHA。
用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信
息是可以通过秘钥解密和还原。
一般散列算法需要提供一个salt盐与原始内容生成摘要信息,这样做的目的是为了安全性,如111111的md5值
是:96e79218965eb72c92a549dd5a330112,拿着96e79218965eb72c92a549dd5a330112去md5破解网站 很容易进行破解,如果要是对111111、salt(盐,一个随机数)和ID值进行散列,这样虽然密码都是111111加不同 的盐会生成不同的散列值。在用户数据库中储存的应该是已经加密后的密码,而不是明文的111111,而且这个过程是 不可逆的
md5加密: Md5Hash SimpleHash
1、使用Md5Hash进行口令加密
String pwd = "123456";
Md5Hash mh=new Md5Hash(pwd);//参数就是原始的口令值
String res=mh.toHex();//获取加密后的16进制串
System.out.println(res);
System.out.println(res.toString());
可以通过网站http://pmd5.com/解密
2、多次散列计算和加盐salt
Md5Hash mh=new Md5Hash(pwd,"111111",2); //参数1就是原始的口令值,参数2是盐值,参数3表示散列计算次数
String pwd="123456";
//参数1是所使用的散列算法名称,参数2是原始数据,参数3是盐值,参数4是散列计算次数
String res=new SimpleHash("md5",pwd,"111111",2).toHex();
System.out.println(res);
注册时的盐值: 一般使用用户名称作为盐值,所以一般不允许用户名称相同。随机盐值的生成方法: RandomNumberGenerator g = new SecureRandomNumberGenerator(); ByteSource bs=g.nextBytes(); bs.toHex()
加密的工具类
public class PasswordHelper {
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
private String algorithmName = "md5";
private final int hashIterations = 2;
public void encryptPassword(User user) {
user.setSalt(randomNumberGenerator.nextBytes().toHex());
String newPassword = new SimpleHash(algorithmName, user.getPassword(),ByteSource.Util.bytes(user.getCredentialsSalt()), hashIterations).toHex();
user.setPassword(newPassword); }
}
自定义带md5加密的口令 public class MyRealm extends AuthorizingRealm { protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { //进行授权操作 return null; } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) arg0; String username = token.getUsername(); // 按照username查询数据库,从数据库中获取盐值salt,存储的口令password【已经加密后的口令,不是用户提交的原始口令值】 // 如果不能按照username查询到数据,则报异常UnknownAccountException String password = "22b57715cc6f7d7d68b95fee85857d9d";// 这是从数据库中获取的凭证 String salt = "111111";// 盐值 AuthenticationInfo res = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes(salt), this.getName()); return res; } }
shiro.ini
[main]
myRealm=com.yan.MyRealm
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
myRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$myRealm
Shiro支持三种方式的授权 1、编程式:通过写if/else 授权代码块完成: Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }
2、注解式:通过在执行的Java方法上放置相应的注解完成:
@RequiresRoles("admin")
public void hello() {
//有权限
}
3、JSP标签:在JSP页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!— 有权限—>
</shiro:hasRole>
授权测试shiro.ini [users] yanjun=123456,role1,role2 [roles] role1=user:create,user:update role2=user:create,user:delete
在ini文件中用户、角色、权限的配置规则是:
用户名=密码,角色1,角色2...
角色=权限1,权限2...
首先根据用户名找角色,再根据角色找权限,角色是权限集合
权限字符串规则
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,:是资源/操作/实例的分割符,权限字符串也可以使用*通配符
用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户实例001的所有权限:user:*:001
基于角色的授权:前提时subject.isAuthenticated() if (currentUser.isAuthenticated()) { // 授权操作 boolean bb = currentUser.hasRole("role");// 如果没有role角色则返回false if (bb) System.out.println("当前用户具有role角色权限"); }
if (currentUser.isAuthenticated()) {
currentUser.checkRole("role");// 如果当前用户有role角色权限,则正常执行;
//如果没有则报异常UnauthorizedException
System.out.println("当前用户具有role角色权限");
}
hasXXX和checkXXX两种操作,如果has则没有对应角色时返回为false,不会异常中断;如果使用check没有对应角色则异常中断
boolean bb = currentUser.hasRole("role");// 如果没有role角色则返回false
bb = currentUser.hasAllRoles(Arrays.asList("role1", "role2"));
boolean[] arr = currentUser.hasRoles(Arrays.asList("role1", "role2", "role3"));
currentUser.checkRole("role1");
currentUser.checkRoles(Arrays.asList("role1", "role2"));
currentUser.checkRoles("role1", "role2","role3");
基于资源授权:前提时subject.isAuthenticated() boolean bb=currentUser.isPermitted("user:create"); if(bb) System.out.println("具有创建用户的权限"); bb=currentUser.isPermitted("user:create:1"); if(bb) System.out.println("具有创建1号用户的权限");
这里也有isXXX和checkXXX之分,结果类似
自定义realm类中doGetAuthorizationInfo方法:根据用户身份信息从数据库查询权限字符串,由shiro进行授权 public class MyRealm extends AuthorizingRealm { protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { // PrincipalCollection是一个身份集合,因为我们可以在Shiro中同时配置多个Realm,所以 //身份信息可能就有多个;因此其提供了PrincipalCollection用于聚合这些身份信息 //注意获取数据的类型应该和封装SimpleAuthenticationInfo对象是参数1的类型一致 String username = (String) arg0.getPrimaryPrincipal();// 获取身份信息 // 根据用户名称查询角色和权限数据,然后将对应的信息添加到SimpleAuthorizationInfo对象 SimpleAuthorizationInfo res = new SimpleAuthorizationInfo(); res.addRole("role1");//添加对应的角色名称 res.addRole("role2"); res.addStringPermission("user:create");//添加对应的资源权限 res.addStringPermission("news:select"); return res; } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) arg0; String username = token.getUsername(); // 执行按照用户名称查询对应的用户信息 AuthenticationInfo info = new SimpleAuthenticationInfo(username, "123456", this.getName());// 注意这里的123456是从数据库中查询到的数据 return info; } }
配置shiro.ini
[main]
myRealm=com.yan.MyRealm
securityManager.realms=$myRealm
测试
Factory<SecurityManager> fac = new IniSecurityManagerFactory("classpath:test7/shiro.ini");
SecurityManager sm = fac.getInstance();
SecurityUtils.setSecurityManager(sm);
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("yanjun", "123456");
currentUser.login(token);
if (currentUser.isAuthenticated()) {
if(currentUser.hasRole("role1"))
System.out.println("当前用户具备role1角色");
if(currentUser.isPermitted("news:select"))
System.out.println("当前用户具有news的查询资源权限");
}
currentUser.logout();// 退出登录
授权执行流程 1、执行subject.isPermitted("user:create")或者hasRole("role1") 2、securityManager通过ModularRealmAuthorizer进行授权 3、ModularRealmAuthorizer调用realm获取权限信息 4、ModularRealmAuthorizer再通过permissionResolver解析权限字符串,校验是否匹配
注意:每次进行权限判断时会自动回调Realm中的doGetAuthorizationInfo方法,所以需要考虑使用缓
存的方式避免频繁的数据库查询