Shiro主要功能有认证,授权,加密,会话管理,与Web集成,缓存等.
新建一个简单的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); // 得到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,:
[users] credo=123 zhaoqian=123知识点:
从外部观察shiro,shiro的结构就是 外部代码--->Subject---->SecurityManager---->Realm
知识点:
从此我们就可以理解shiro的处理流程.
我们可以更进一步理解,Realm的数据是怎么来的?当然是我们自己定义的,也就是说,我们需要自己定义权限,角色,授权方面的数据资源(数据库存储或shiro.ini文件存储).
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.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 及其子类的关系
从上图可以看到整个Shiro的认证流程
1、首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。如我们之前的ini配置方式将使用org.apache.shiro.realm.text.IniRealm。
org.apache.shiro.realm.Realm接口如下:
String getName(); //返回一个唯一的Realm名字 boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throws AuthenticationException; //根据Token获取认证信息
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)
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(); }
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):
realmA=org.credo.test.realm.multi.RealmA realmB=org.credo.test.realm.multi.RealmB securityManager.realms=$realmA,$realmBJunit测试代码:
@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,但没有指定,不会执行.
以后一般继承AuthorizingRealm(授权)即可;其继承了AuthenticatingRealm(即身份验证),而且也间接继承了CachingRealm(带有缓存实现)。其中主要默认实现如下:
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实现。
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,$realmBJunit测试代码:
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(); }测试结果:
realm A pass A realm B pass B credo [email protected]
包含credo和[email protected],两个都通过了验证.都有两个信息.
学习资料参考以及部分文章的Copy: