权限(适用于初学者)

452 阅读5分钟

权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户 首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问.

权限管理系统的模型 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=dataSource
	securityManager.realms=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方法,所以需要考虑使用缓
存的方式避免频繁的数据库查询