Shiro 关于认证与授权功能的实现

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/Geffin/article/details/100518764

1 什么是 Shiro?

在学习 Shiro 之前,我们得先了解下什么是 Shiro 。

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

通俗来说,Shiro 就是一个安全认证框架。

2 为什么要学习 Shiro

  1. 使用 Shiro 可以非常快速的完成认证、授权等功能的开发,降低系统成本。
  2. Shiro 使用广泛,不仅可以使用在 web 应用中,而且在非 web 应用,甚至集群分布式应用中都可以见到它的身影。
  3. 与同为安全框架的 spring security 相比,Shiro 更为简单灵活。

3 Shiro 概念介绍

  1. Subject:主体。主体记录了当前操作用户(当前操作的主体)。Subject 在 Shiro 中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过 Subject 进行认证授权,而 Subject 则是通过 SecurityManager 安全管理器进行认证授权。
  2. SecurityManager:安全管理器。实际上 SecurityManager 是一个接口,它负责对所有的 Subject 进行安全管理,也是 Shiro 的核心。从深层次来说,SecurityManager 通过 Authenticator 进行安全认证,通过 Authorizer 进行授权,通过 SessionManager 进行会话管理。事实上,SecurityManager 也继承了上述三个接口。
  3. Authenticator:认证器。Authenticator 用于对用户身份进行认证,Shiro 为我们提供了 ModularRealmAuthenticator 实现类。
  4. Authorizer:授权器。用户通过认证器认证通过后,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
  5. realm:领域。securityManager 进行安全认证时需要通过 Realm 获取用户权限数据(如果用户数据在数据库中则 Realm 需要去数据库查询用户信息 )。
  6. sessionManager:会话管理。事实上 Shiro 自定义了一套会话管理,并不依赖于 web 容器的 session。
  7. CacheManager:缓存管理。用于将用户权限数据存储在缓存之中。
  8. Cryptography:密码管理。Shiro 为我们提供了一套加密/解密的组件,方便我们的开发。

4 在非 web 工程中 Shiro 的使用

4.1 一个最简单的登录退出程序

4.1.1 引入依赖
<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    
    <dependency>
    	<groupId>org.apache.shiro</groupId>
    	<artifactId>shiro-core</artifactId>
    	<version>1.4.0</version>
	</dependency>
	
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-log4j12</artifactId>
		<version>1.6.4</version>
	</dependency>
	
	<dependency>
        `<groupId>commons-logging</groupId>
      	  <artifactId>commons-logging</artifactId>
        <version>1.2</version>
	</dependency>
    
  </dependencies>

我们需要引入 shiro-core 的 jar 包

4.1.2 创建日志配置文件 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
4.1.3 创建 Shiro 配置文件 shiro.ini

我们可以通过 shiro.ini 配置文件来初始化 SecurityManager 环境。同时为了方便测试我们也将用户名和密码放在 shiro.ini 中。

[users]
Ling=123
4.1.4 代码
package edu.szu.ShiroTest;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

public class Test {

	//模拟用户登录
	public void login(String username,String password) {
		//创建SecurityManager工厂
		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
		//创建SecurityManager
		SecurityManager securityManager = factory.getInstance();
		//将securityManager设置到运行环境中
		SecurityUtils.setSecurityManager(securityManager);
		//创建一个Subject实例
		Subject subject = SecurityUtils.getSubject();
		//创建token令牌,记录用户认证的身份与凭证
		UsernamePasswordToken token = new UsernamePasswordToken(username,password);
		//用户登录
		try {
			subject.login(token);
		}catch (Exception e) {
			System.out.println("登录失败!");
		}
		//查询用户认证状态
		boolean is = subject.isAuthenticated();
		System.out.println("用户认证状态: " + is);
		//模拟用户退出
		subject.logout();
		//查询用户认证状态
		is = subject.isAuthenticated();
		System.out.println("用户认证状态: " + is);
	}
	
	public static void main(String[] args) {
		Test test = new Test();
		test.login("Ling", "123");
	}
}

4.1.5 测试

运行程序,我们的控制台显示如下:

用户认证状态: true
用户认证状态: false

我们修改一下传入的账号密码,故意把它改错,再次运行程序,输出如下:

登录失败!
用户认证状态: false
用户认证状态: false
4.1.6 认证执行流程

subject.login(token) 将由 securityManager 通过 Authenticator 进行认证,而 Authenticator 的实现 ModularRealmAuthenticator 调用 realm 从 ini 配置文件读取用户真实的账号密码,这里使用的是 Shiro 自带的 IniRealm(IniRealm 会根据 token 中的账号密码去 ini 配置文件中寻找)。

4.2 自定义 Realm

我们之前使用的是 Shiro 自带的 IniRealm,但是 IniRealm 有一个问题,它会从 ini 配置文件中读取用户的信息,但大部分情况下用户的信息是存储在数据库中的,所以我们需要自定义 realm。

一般来说,我们自定义的 Realm 继承自 AuthorizingRealm。

4.2.1 修改 Shiro 配置文件

把文件名修改为 shiro-realm.ini,同时内容修改为

[main]
#自定义 realm
customRealm=edu.szu.ShiroTest.MyRealm
#将realm设置到securityManager
securityManager.realms=$customRealm
4.2.2 代码
package edu.szu.ShiroTest;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MyRealm extends AuthorizingRealm{

	//授权服务
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		// TODO Auto-generated method stub
		return null;
	}

	//认证服务
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		//从token中获取用户信息
		String username = (String) token.getPrincipal();
		//此处应该使用username到数据库查询密码,我偷懒就省略了
		//模拟查询成功
		String password = "123";
		//模拟查询失败
		/**
		if(!username.equals("Ling")) {
			return null;
		}
		**/
		//将认证信息交由父类进行认证
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,getName());
		return simpleAuthenticationInfo;
	}

	@Override
	public String getName() {
		return "MyRealm";
	}
	
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof UsernamePasswordToken;
	}
	
}

正常情况:

用户认证状态: true
用户认证状态: false

账号密码错误情况:

登录失败!
用户认证状态: false
用户认证状态: false

4.3 在 Shiro 中使用散列算法

散列算法是不可逆的,一般用于生成一段文本的摘要信息,但无法将摘要转化为原始内容,可用于对密码进行散列,常用的散列算法有MD5、SHA。

为了安全性,一般散列算法需要提供一个 salt 与原始内容生成摘要信息。若无 salt,有 md5 值很容易破解出原始内容,但若将原始值与 salt 一起进行散列,虽然原始内容相同,但加上不同的 salt 会生成不同的散列值。

4.3.1 使用示例
String password = "123456";
String salt = "asdfgh";
//使用md5进行加密
String passwordWithMD5 = new Md5Hash(password).toString();
System.out.println(passwordWithMD5);
//使用md5进行加密,加salt,进行一次散列,注意执行两次散列相当于md5(md5())
String passwordWithMD5Sale = new Md5Hash(password,salt,1).toString();
System.out.println(passwordWithMD5Sale);

结果:

e10adc3949ba59abbe56e057f20f883e
003dc55c5d91addfead4a4fa347c4f2d
4.3.2 在我们的 Realm 中添加散列算法

先将 salt 和散列后的值存在数据库中,Realm 从数据库取出 salt 和加密后的值由 Shiro 完成密码校验。

修改我们的 Realm

package edu.szu.ShiroTest;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class MyRealm extends AuthorizingRealm{

	//授权服务
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		// TODO Auto-generated method stub
		return null;
	}

	//认证服务
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		//从token中获取用户信息
		String username = (String) token.getPrincipal();
		//salt 的信息应该从数据库中取得,我这里模拟数据库操作
		String salt = "asdfgh";
		//此处应该使用username到数据库查询,且查询到的值为加密后的值
		//模拟查询成功
		String password = "003dc55c5d91addfead4a4fa347c4f2d";
		//模拟查询失败
		/**
		if(!username.equals("Ling")) {
			return null;
		}
		**/
		//将认证信息交由父类进行认证
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,ByteSource.Util.bytes(salt),getName());
		return simpleAuthenticationInfo;
	}

	@Override
	public String getName() {
		return "MyRealm";
	}
	
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof UsernamePasswordToken;
	}
	
}
4.3.3 修改 Shiro 配置文件

把 Shiro 配置文件的文件名改为 shiro-cryptography.ini

[main]
#定义凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列算法
credentialsMatcher.hashAlgorithmName=md5
#散列次数
credentialsMatcher.hashIterations=1
#自定义 realm
customRealm=edu.szu.ShiroTest.MyRealm
#将realm设置到securityManager
securityManager.realms=$customRealm
#将凭证匹配器设置到 realm
customRealm.credentialsMatcher=$credentialsMatcher

4.4 授权

4.4.1 授权代码块格式
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
4.4.2 权限配置文件

创建权限配置文件 shiro-permission.ini

[users]
#用户Ling密码为123456,具有role1和role2两个角色
Ling=123456,role1,role2

[roles]
#role1具有对资源user的create和update权限
role1=user:create,user:update
#role2具有对资源user的create权限
role2=user:create

user:create 表示对资源 user 具有创建权限

4.4.3 代码测试
package edu.szu.ShiroTest;

import java.util.Arrays;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

public class Test {

	//模拟用户登录,授权
	public void test(String username,String password) {
		//创建SecurityManager工厂
		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini");
		//创建SecurityManager
		SecurityManager securityManager = factory.getInstance();
		//将securityManager设置到运行环境中
		SecurityUtils.setSecurityManager(securityManager);
		//创建一个Subject实例
		Subject subject = SecurityUtils.getSubject();
		//创建token令牌,记录用户认证的身份与凭证
		UsernamePasswordToken token = new UsernamePasswordToken(username,password);
		//用户登录
		try {
			subject.login(token);
		}catch (Exception e) {
			System.out.println("登录失败!");
		}
		//查询用户认证状态
		boolean is = subject.isAuthenticated();
		System.out.println("用户认证状态: " + is);
		
		//授权检测:基于角色授权
		System.out.println("用户是否具有一个角色" + subject.hasRole("role1"));
		System.out.println("用户是否具有一个角色" + subject.hasRole("role3"));
		System.out.println("用户是否具有多个角色" + subject.hasAllRoles(Arrays.asList("role1","role2")));
		
		//授权检测:基于资源授权
		System.out.println("用户是否具有一个权限" + subject.isPermitted("user:create"));
		System.out.println("用户是否具有一个权限" + subject.isPermitted("user:delete"));
		System.out.println("用户是否具有多个权限" + subject.isPermittedAll("user:create","user:update:1"));
		
		//模拟用户退出
		subject.logout();
		//查询用户认证状态
		is = subject.isAuthenticated();
		System.out.println("用户认证状态: " + is);
	}
	
	public static void main(String[] args) {
		Test test = new Test();
		test.test("Ling", "123456");
	}
}

测试结果如下

用户认证状态: true
用户是否具有一个角色true
用户是否具有一个角色false
用户是否具有多个角色true
用户是否具有一个权限true
用户是否具有一个权限false
用户是否具有多个权限true
用户认证状态: false
4.4.4 自定义 Realm

与认证相似,授权功能的实现大部分情况需要从数据库中获取数据,故我们需要自定义 Realm 。

代码如下:

package edu.szu.ShiroTest;

import java.util.ArrayList;
import java.util.List;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class MyRealm extends AuthorizingRealm{

	//授权服务
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		//获取身份信息
		String username = (String) principals.getPrimaryPrincipal();
		//这里根据身份信息到数据库中查询权限信息
		//这里直接使用静态数据进行模拟
		List<String> list = new ArrayList<String>();
		list.add("user:create");
		list.add("user:update");

		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		
		for(String permission:list) {
			simpleAuthorizationInfo.addStringPermission(permission);
		}
		
		return simpleAuthorizationInfo;
	}

	//认证服务
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		//从token中获取用户信息
		String username = (String) token.getPrincipal();
		//salt 的信息应该从数据库中取得,我这里模拟数据库操作
		String salt = "asdfgh";
		//此处应该使用username到数据库查询,且查询到的值为加密后的值
		//模拟查询成功
		String password = "003dc55c5d91addfead4a4fa347c4f2d";
		//模拟查询失败
		/**
		if(!username.equals("Ling")) {
			return null;
		}
		**/
		//将认证信息交由父类进行认证
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,ByteSource.Util.bytes(salt),getName());
		return simpleAuthenticationInfo;
	}

	@Override
	public String getName() {
		return "MyRealm";
	}
	
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof UsernamePasswordToken;
	}
	
}

配置文件使用认证阶段的即可。

参考:Shiro权限管理框架详解

猜你喜欢

转载自blog.csdn.net/Geffin/article/details/100518764
今日推荐