权限管理
什么是权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证
和授权
两部分,简称认证授权
。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
什么是身份认证
身份认证
,就是判断一个用户是否合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
什么是授权
授权
,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
什么是Shrio
Shiro是一个功能强大且易于使用的Java安全框架,它执行身份认证、授权、加密和会话管理.使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的应用程序到最大的web和企业应用程序.
Shiro是apache旗下的一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权,加密,绘画管理等功能,组成一个通用的安全认证框架.
shiro的核心架构
Subject
Subject即主体
,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序.Subject在Shiro中是一个接口,接口定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权.
SecurityManager
SecurityManager即安全管理器
,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理.通过SecurityManager可以完成subject的认证、授权等.实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等.
SecurityManager是一个接口,继承了Authenticator,Authorizer,SessionManager这三个接口.
Authenticaor
Authenticaor即认证器
,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器.
Authorizer
Authorizer即授权器
,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能操作权限.
Realm
Realm即领域
,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库中,那么Realm就需要从数据库获取用户身份信息.
SessionManager
sessionManager即会话管理
,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录.
SessionDAO
SessionDAO即会话Dao
,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库中.
CacheManager
CacheManager即缓存管理
,将用户权限数据存储在缓存,这样可以提高性能.
Cryptography
Cryptography即密码管理
,shiro提供了一套加密/解密的组件,方便开发.比如提供常用的散列、加/解密等功能.
Shiro中的认证
认证
身份认证,就是判断一个用户是否为合法用户的处理过程.最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确.
shiro中认证的关键对象
Subject: 主体
访问系统的用户,主体可以是用户、程序等,进行认证的都成为主体;
Principal: 身份信息
是主体(Subject)进行身份认证的标识,标识必须具有唯一性
,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal).
Credential:凭证信息
是只有主体自己知道的安全信息,如密码、证书等.
认证的流程
认证的开发
创建项目并引入依赖
1 2 3 4 5 6
| <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency>
|
ini配置文件
ini配置文件 用来学习shiro书写我们系统中相关得权限数据
1 2 3 4
| [users] xiaochen=123 zhangsan=123456 lisi=789
|
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class TestAuthenticator { public static void main(String[] args) {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen","123");
try { System.out.println("认证状态:" + subject.isAuthenticated()); subject.login(token); System.out.println("认证状态:" + subject.isAuthenticated()); }catch (UnknownAccountException e){ e.printStackTrace(); System.out.println("认证失败: 用户名不存在" ); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("认证失败: 密码输入不正确"); } } }
|
开发认证源码
SimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class SimpleAccountRealm extends AuthorizingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken)token; SimpleAccount account = this.getUser(upToken.getUsername()); if (account != null) { if (account.isLocked()) { throw new LockedAccountException("Account [" + account + "] is locked."); }
if (account.isCredentialsExpired()) { String msg = "The credentials for account [" + account + "] are expired"; throw new ExpiredCredentialsException(msg); } }
return account; } }
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = this.getUsername(principals); this.USERS_LOCK.readLock().lock();
AuthorizationInfo var3; try { var3 = (AuthorizationInfo)this.users.get(username); } finally { this.USERS_LOCK.readLock().unlock(); }
return var3; }
|
**认证: **
最终执行用户名比较 SimpleAccountRealm
doGetAuthenticationInfo 在这个方法中完成用户名的校验
最终密码的校验是在 AuthenticaingRealm中
assertCredentialsMatch
总结:
AuthenticatingRealm 认证realm doGetAuthenticationInfo
AuthorizingRealm 授权realm doGetAuthorizationInfo
自定义Realm
自定义Realm实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
public class CustomerRealm extends AuthorizingRealm {
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String)token.getPrincipal(); System.out.println(principal); if("xiaochen".equals(principal)){ SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("xiaochen","123456",this.getName()); return simpleAuthenticationInfo; } return null; } }
|
使用自定义realm认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package com.shiro.authenticator;
import com.shiro.realm.CustomerRealm; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject;
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(new CustomerRealm());
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123456");
try { subject.login(token); System.out.println(subject.isAuthenticated()); }catch(UnknownAccountException e){ e.printStackTrace(); System.out.println("认证失败: 用户名不存在" ); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("认证失败: 密码输入不正确"); } } }
|
使用MD5和Salt
实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。
测试MD5、Salt、散列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class TestShiroMD5 { public static void main(String[] args) {
Md5Hash md5Hash = new Md5Hash("123");
System.out.println(md5Hash.toHex());
Md5Hash md5Hash1 = new Md5Hash("123","x0*7ps");
System.out.println(md5Hash1.toHex());
Md5Hash md5Hash2 = new Md5Hash("123","x0*7ps",1024); System.out.println("散列1024次: "+md5Hash2.toHex()); } }
|
创建有加密的Realm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
public class CustomerMd5Realm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal();
if ("xiaochen".equals(principal)){
return new SimpleAuthenticationInfo(principal,"44c42bc682c33a4dae2af47eba4c8011", ByteSource.Util.bytes("x0*7ps"),this.getName()); }
return null; } }
|
测试加密后的认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class TestCustomerMd5RealmAuthenticator {
public static void main(String[] args) {
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
CustomerMd5Realm realm = new CustomerMd5Realm();
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(1024); realm.setCredentialsMatcher(hashedCredentialsMatcher); defaultSecurityManager.setRealm(realm);
SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try { subject.login(token); System.out.println(subject.isAuthenticated()); }catch(UnknownAccountException e){ e.printStackTrace(); System.out.println("认证失败: 用户名不存在" ); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("认证失败: 密码输入不正确"); }
} }
|
密码重试次数限制
如在一个小时内密码最多重试5次,如果尝试次数超过了5次就锁定一个小时,1个小时后可再次重试,如果还是重试失败,可以锁定如1天,以此类推,防止密码被暴力破解。我们通过继承HashedCredentialsMatcher,且使用Ehcache记录重试次数和超时时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String)token.getPrincipal(); Element element = passwordRetryCache.get(username); if(element == null) { element = new Element(username , new AtomicInteger(0)); passwordRetryCache.put(element); } AtomicInteger retryCount = (AtomicInteger)element.getObjectValue(); if(retryCount.incrementAndGet() > 5) { throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { passwordRetryCache.remove(username); } return matches; }
|
Shiro中的授权
授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
关键对象
授权可简单理解为who对what(which)进行How操作
who,即主体(Subject)
,主体需要访问系统中的资源。
what,即资源(Resource)
,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型
和资源实例
,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
How,权限/许可(Permission)
,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
授权流程
授权方式
权限字符串
权限字符串的规则是: 资源标识符: 操作 : 资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分隔符,权限字符串也可以使用*通配符。
例子:
- 用户创建权限: user : create ,或 user :create:*
- 用户修改实例001的权限: user:update:001
- 用户实例001的所有权限: user: * : 001
shiro中授权编程实现方式
编程式
1 2 3 4 5 6
| Subject subject = SecurityUtils.getSunject(); if(subject.hasRole("admin")){ } else { }
|
注解式
1 2 3 4
| @RequiresRoles("admin") public void hello(){ }
|
标签式
1 2 3 4 5 6 7
| <!-- JSP/GSP 标签: 在JSP/GSP页面通过相应的标签完成: -->
<shiro:hasRole name="admin"> <!-- 有权限 --> </shiro:hasRole>
注意:Thymeleaf中使用shiro需要额外集成。
|
认证用户进行授权
在之前MD5的Test基础之上,增加用户授权。代码分布在com.shiro.authenticaor.TestCustomerMd5RealmAuthenticator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
if(subject.isAuthenticated()){ System.out.println(subject.hasRole("admin"));
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user")); for (int i = 0; i < booleans.length; i++) { System.out.println(booleans[i]); }
System.out.println("===================================================");
System.out.println("权限: "+subject.isPermitted("user:update:01")); System.out.println("权限: "+subject.isPermitted("product:create:02"));
boolean[] permitted = subject.isPermitted("user:*:01","order:*:10"); for (boolean b : permitted){ System.out.println(b); }
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01"); System.out.println(permittedAll); }
|