Shiro主要功能有认证,授权,加密,会话管理,与Web集成,缓存等.
1.shiro入门测试
新建一个简单的Maven项目,我们只是使用Junit和shiro-core包.POM最后是如下代码:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.credo</groupId>
<artifactId>shiro-study</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
</project>
在src/test/java下建包,类TestHelloShiro.java .在src/test/resources下新建名为 shiro.ini 的文件.
package org.credo.test;
import junit.framework.Assert;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.junit.Test;
public class TestHelloShiro {
public static final String DEFAULT_INI_RESOURCE_PATH = "classpath:shiro.ini";
@Test
public void TestShiroFirst() {
// 使用ini文件方式实例化shiro IniSecurityManagerFactory.
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory(DEFAULT_INI_RESOURCE_PATH<span></span>);
// 得到SecurityManager实例 并绑定给SecurityUtils
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//得到Subject
Subject shiroSubject = SecurityUtils.getSubject();
//创建用户名/密码身份验证Token(即用户身份/凭证)
UsernamePasswordToken normalToken = new UsernamePasswordToken("credo", "123");
try {
//登录,进行身份验证
shiroSubject.login(normalToken);
} catch (Exception e) {
//登录失败,打印出错误信息,可自定义
System.out.println(e.getMessage());
}
//断言登录成功
Assert.assertEquals(true, shiroSubject.isAuthenticated());
//登出
shiroSubject.logout();
}
}
shiro.ini文件通过[users]指定了两个user:credo/123、zhaoqian/123,:
?1 2 3 | [users] credo=123 zhaoqian=123 |
知识点:
- shiro.ini–是shiro必须配置的一个重要文件.
- [users]:是shiro.ini配置里一个标注.作用就指定用户身份/凭证.
- IniSecurityManagerFactory就是Factory<SecurityManager>:通过new IniSecurityManagerFactory实例化的SecurityManager工厂,关于这个工厂下面有源码解释.
2.shiro处理流程的简单理解
从外部观察shiro,shiro的结构就是 外部代码—>Subject—->SecurityManager—->Realm
知识点:
- Subject:与外部代码交互的一层.应该理解为一个”用户”,但这个用户不一定是指传统意义的用户.应该理解为与我们当前系统交互的一个”对象”.
- SecurityManager:SecurityManager是整个shiro核心控制器,其控制所有的Subject,或者说所有的Subject的操作其实都是交给SecurityManager来处理.在一个应用中只有一个单例的SecurityManager实例存在,Apache Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
- Realm:Shiro需要Realm获取安全数据,如用户,角色,权限.Realm可以理解为”域”.简单的理解就是,Realm是像一个数据池,如果Shiro要验证一个当前系统对象的权限,密码,角色,那他就需要从Realm中获取对应的数据.
从此我们就可以理解shiro的处理流程.
- 1.外部代码访问shiro,通过与Subject的交互来进行安全方面的操作,如授权,认证,资源的权限等.
- 2.Subject相关的交互信息交由SecurityManager来处理.
- 3.SecurityManager从Realm中获取对应的”数据”进行处理,返回给外部代码.
我们可以更进一步理解,Realm的数据是怎么来的?当然是我们自己定义的,也就是说,我们需要自己定义权限,角色,授权方面的数据资源(数据库存储或shiro.ini文件存储).
3.IniSecurityManagerFactory就是Factory<SecurityManager>源码解析
shiro的Factory<SecurityManager>是一个工厂模式的应用.我们追溯源码可以看到其内部的实现.
Factory最底层接口:org.apache.shiro.util.Factory.class
package org.apache.shiro.util;
//应用工厂设计模式的泛型接口
public interface Factory<T> {
//返回一个实例
T getInstance();
}
Factory接口声明的getInstance()方法,由其直接子类AbstractFactory实现。
之后AbstractFactory在实现的getInstance()方法中调用了一个新声明的抽象方法,这个方法也是由其直接子类实现的。
这样,从Factory开始,每个子类都实现父类声明的抽象方法,同时又声明一个新的抽象方法并在实现父类的方法中调用。
通过源码追溯,我们可以发现有2个类是实现了Factory接口:
- org.apache.shiro.jndi.JndiObjectFactory,泛型类.用于JNDI查找.
- 抽象类,我们需要关注的org.apache.shiro.util.AbstractFactory,就是abstract class AbstractFactory<T> implements Factory<T> .
接着是org.apache.shiro.config.IniFactorySupport,抽象类public abstract class IniFactorySupport<T> extends AbstractFactory<T>
最终是package org.apache.shiro.config包下的IniSecurityManagerFactory.
IniSecurityManagerFactory类主要是用工厂模式创建基于Ini配置SecurityManager实例.
IniSecurityManagerFactory 是 Factory的子类,DefaultSecurityManager是 SecurityManager的子类。
Factory 与 SecurityManager 及其子类的关系
4.Shiro内部的认证流程
从上图可以看到整个Shiro的认证流程
1、首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;
2、SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;
3、
Authenticator
才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;
4、Authenticator可能会委托给相应的
AuthenticationStrategy
进行多Realm身份验证,默认
ModularRealmAuthenticator
会调用AuthenticationStrategy进行多Realm身份验证;
5、Authenticator会把相应的token传入
Realm
,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。
此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
5.Realm
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。如我们之前的ini配置方式将使用org.apache.shiro.realm.text.IniRealm。
org.apache.shiro.realm.Realm接口如下:
?1 2 3 | String getName();//返回一个唯一的Realm名字 booleansupports(AuthenticationToken token);//判断此Realm是否支持此Token AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException; //根据Token获取认证信息 |
A:单realm实现使用
1.我们先定义一个Realm.
package org.credo.test.realm.single;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.realm.Realm;
public class TestMySingleRealm implements Realm{
@Override
public String getName() {
return "TestMySingleReam";
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName=String.valueOf(token.getPrincipal());
//注意token的Credentials是char[],z主要转换.
String passWord=String.valueOf((char[])token.getCredentials());
if(!userName.equals("credo")){
throw new UnknownAccountException("无效的账户名!");
}
if(!passWord.equals("aaa")){
throw new IncorrectCredentialsException("密码错误!");
}
return new SimpleAuthenticationInfo(userName, passWord,getName());
}
}
2.ini配置文件指定自定义Realm实现(文件名我定义为:shiro-single-realm.ini)
?1 2 | singleRealm=org.credo.test.realm.single.TestMySingleRealm securityManager.realms=$singleRealm |
通过$name来引入之前的realm定义
3.Junit测试代码
@Test
public void testSingleMyRealm() {
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-single-realm.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject shiroSubject = SecurityUtils.getSubject();
UsernamePasswordToken normalToken = new UsernamePasswordToken("credo", "aaa");
try {
shiroSubject.login(normalToken);
} catch (UnknownAccountException e) {
System.out.println(e.getMessage());
} catch (IncorrectCredentialsException e) {
System.out.println(e.getMessage());
} catch (AuthenticationException e) {
e.printStackTrace();
}
Assert.assertEquals(true, shiroSubject.isAuthenticated());
shiroSubject.logout();
//解除绑定Subject到线程,防止对下次测试造成影响
ThreadContext.unbindSubject();
}
B:多个Realms的使用
realm A:
package org.credo.test.realm.multi;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.realm.Realm;
public class RealmA implements Realm {
@Override
public String getName() {
return "RealmA";
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName=String.valueOf(token.getPrincipal());
//注意token的Credentials是char[],z主要转换.
String passWord=String.valueOf((char[])token.getCredentials());
System.out.println("realm A");
if(!userName.equals("credo")){
throw new UnknownAccountException("RealmA--无效的账户名!");
}
if(!passWord.equals("123")){
throw new IncorrectCredentialsException("RealmA--密码错误!");
}
System.out.println("pass A");
return new SimpleAuthenticationInfo(userName, passWord,getName());
}
}
realmB:
package org.credo.test.realm.multi;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.realm.Realm;
public class RealmB implements Realm {
@Override
public String getName() {
return "RealmsB";
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName=String.valueOf(token.getPrincipal());
//注意token的Credentials是char[],z主要转换.
String passWord=String.valueOf((char[])token.getCredentials());
System.out.println("realm B");
if(!userName.equals("credo")){
throw new UnknownAccountException("RealmB--无效的账户名!");
}
if(!passWord.equals("aaa")){
throw new IncorrectCredentialsException("RealmB--密码错误!");
}
System.out.println("pass B");
return new SimpleAuthenticationInfo(userName, passWord,getName());
}
}
shiro.ini配置(文件名:shiro-multi-realm.ini):
@Test
public void testMultiMyRealm() {
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-multi-realm.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject shiroSubject = SecurityUtils.getSubject();
UsernamePasswordToken normalToken = new UsernamePasswordToken("credo", "aaa");
try {
shiroSubject.login(normalToken);
} catch (UnknownAccountException e) {
System.out.println(e.getMessage());
} catch (IncorrectCredentialsException e) {
System.out.println(e.getMessage());
} catch (AuthenticationException e) {
System.out.println(e.getMessage());
}
Assert.assertEquals(true, shiroSubject.isAuthenticated());
shiroSubject.logout();
ThreadContext.unbindSubject();
}
测试结果可以发现,只要其中一个realm通过就通过了.执行顺序是按shiro.ini中指定的顺序执行.先A后B.如果有realmC,realmD,但没有指定,不会执行.
6.Shiro默认提供的Realm
以后一般继承AuthorizingRealm(授权)即可;其继承了AuthenticatingRealm(即身份验证),而且也间接继承了CachingRealm(带有缓存实现)。其中主要默认实现如下:
- org.apache.shiro.realm.text.IniRealm:[users]部分指定用户名/密码及其角色;[roles]部分指定角色即权限信息;
- org.apache.shiro.realm.text.PropertiesRealm: user.username=password,role1,role2指定用户名/密码及其角色;role.role1=permission1,permission2指定角色及权限信息;
- org.apache.shiro.realm.jdbc.JdbcRealm:通过sql查询相应的信息,
- 如“select password from users where username = ?”获取用户密码,
- “select password, password_salt from users where username = ?”获取用户密码及盐;
- “select role_name from user_roles where username = ?”获取用户角色;
- “select permission from roles_permissions where role_name = ?”获取角色对应的权限信息;
- 也可以调用相应的api进行自定义sql;
7.Authenticator及AuthenticationStrategy
Authenticator的职责是验证用户帐号,是Shiro API中身份验证核心的入口点:
package org.apache.shiro.authc;
public interface Authenticator {
/**
* @throws AuthenticationException if there is any problem during the authentication process.
* See the specific exceptions listed below to as examples of what could happen
* in order to accurately handle these problems and to notify the user in an
* appropriate manner why the authentication attempt failed. Realize an
* implementation of this interface may or may not throw those listed or may
* throw other AuthenticationExceptions, but the list shows the most common ones.
* @see ExpiredCredentialsException
* @see IncorrectCredentialsException
* @see ExcessiveAttemptsException
* @see LockedAccountException
* @see ConcurrentAccessException
* @see UnknownAccountException
*/
public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;
}
如果验证成功,将返回AuthenticationInfo验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的AuthenticationException实现。
SecurityManager接口继承了Authenticator,另外还有一个ModularRealmAuthenticator实现,其委托给多个Realm进行验证,验证规则通过AuthenticationStrategy接口指定,默认提供的实现:
- FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
- AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,返回所有Realm身份验证成功的认证信息;
- AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。
ModularRealmAuthenticator默认使用AtLeastOneSuccessfulStrategy策略。
自定义AuthenticationStrategy实现,首先看其API:
//在所有Realm验证之前调用
AuthenticationInfo beforeAllAttempts(
Collection<? extends Realm> realms, AuthenticationToken token)
throws AuthenticationException;
//在每个Realm之前调用
AuthenticationInfo beforeAttempt(
Realm realm, AuthenticationToken token, AuthenticationInfo aggregate)
throws AuthenticationException;
//在每个Realm之后调用
AuthenticationInfo afterAttempt(
Realm realm, AuthenticationToken token,
AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)
throws AuthenticationException;
//在所有Realm之后调用
AuthenticationInfo afterAllAttempts(
AuthenticationToken token, AuthenticationInfo aggregate)
throws AuthenticationException;
因为每个AuthenticationStrategy实例都是无状态的,所有每次都通过接口将相应的认证信息传入下一次流程;通过如上接口可以进行如合并/返回第一个验证成功的认证信息。
自定义实现时一般继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy即可
测试案例:
修改shiro.ini
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
realmA=org.credo.test.realm.multi.RealmA
realmB=org.credo.test.realm.multi.RealmB
securityManager.realms=$realmA,$realmB
Junit测试代码:
RealmB的getAuthenticationInfo方法返回值修改为:return new SimpleAuthenticationInfo(userName+”@qq.com”, passWord,getName());
其他不变,RealmA也不变.但验证过程用户名和密码都写正确的”credo”,”123”
@Test
public void testAuthenticator() {
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-multi-realm.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject shiroSubject = SecurityUtils.getSubject();
UsernamePasswordToken normalToken = new UsernamePasswordToken("credo", "aaa");
try {
shiroSubject.login(normalToken);
} catch (UnknownAccountException e) {
System.out.println(e.getMessage());
} catch (IncorrectCredentialsException e) {
System.out.println(e.getMessage());
} catch (AuthenticationException e) {
System.out.println(e.getMessage());
}
// 得到一个PrincipalCollection,包含所有成功的.
PrincipalCollection principalCollection = shiroSubject.getPrincipals();
for(Object obj:principalCollection){
System.out.println(obj.toString());
}
Assert.assertEquals(2, principalCollection.asList().size());
Assert.assertEquals(true, shiroSubject.isAuthenticated());
shiroSubject.logout();
}
@After
public void tearDown() throws Exception {
ThreadContext.unbindSubject();
}
测试结果:
?1 2 3 4 5 6 | realm A pass A realm B pass B credo credo@qq.com |
包含credo和credo@qq.com,两个都通过了验证.都有两个信息.
学习资料参考以及部分文章的Copy:
- http://shiro.apache.org/
- http://jinnianshilongnian.iteye.com/blog/2018936
- http://blog.csdn.net/teamlet/article/details/7773341