Shiro主要功能有认证,授权,加密,会话管理,与Web集成,缓存等.
新建一个简单的Maven项目,我们只是使用Junit和shiro-core包.POM最后是如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<projectxmlns="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>
|
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
|
packageorg.credo.test;
importjunit.framework.Assert;
importorg.apache.shiro.SecurityUtils;
importorg.apache.shiro.subject.Subject;
importorg.apache.shiro.util.Factory;
importorg.apache.shiro.authc.UsernamePasswordToken;
importorg.apache.shiro.config.IniSecurityManagerFactory;
importorg.apache.shiro.mgt.SecurityManager;
importorg.junit.Test;
publicclassTestHelloShiro {
publicstaticfinalString DEFAULT_INI_RESOURCE_PATH ="classpath:shiro.ini";
@Test
publicvoidTestShiroFirst() {
// 使用ini文件方式实例化shiro IniSecurityManagerFactory.
IniSecurityManagerFactory securityManagerFactory =newIniSecurityManagerFactory(DEFAULT_INI_RESOURCE_PATH<span></span>);
// 得到SecurityManager实例 并绑定给SecurityUtils
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//得到Subject
Subject shiroSubject = SecurityUtils.getSubject();
//创建用户名/密码身份验证Token(即用户身份/凭证)
UsernamePasswordToken normalToken =newUsernamePasswordToken("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,shiro的结构就是 外部代码--->Subject---->SecurityManager---->Realm
知识点:
从此我们就可以理解shiro的处理流程.
我们可以更进一步理解,Realm的数据是怎么来的?当然是我们自己定义的,也就是说,我们需要自己定义权限,角色,授权方面的数据资源(数据库存储或shiro.ini文件存储).
shiro的Factory<SecurityManager>是一个工厂模式的应用.我们追溯源码可以看到其内部的实现.
Factory最底层接口:org.apache.shiro.util.Factory.class
1
2
3
4
5
6
7
|
packageorg.apache.shiro.util;
//应用工厂设计模式的泛型接口
publicinterfaceFactory<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接口如下:
1
2
3
|
String getName();//返回一个唯一的Realm名字
booleansupports(AuthenticationToken token);//判断此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException; //根据Token获取认证信息
|
1.我们先定义一个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
|
packageorg.credo.test.realm.single;
importorg.apache.shiro.authc.AuthenticationException;
importorg.apache.shiro.authc.AuthenticationInfo;
importorg.apache.shiro.authc.AuthenticationToken;
importorg.apache.shiro.authc.IncorrectCredentialsException;
importorg.apache.shiro.authc.SimpleAuthenticationInfo;
importorg.apache.shiro.authc.UnknownAccountException;
importorg.apache.shiro.authc.UsernamePasswordToken;
importorg.apache.shiro.realm.Realm;
publicclassTestMySingleRealmimplementsRealm{
@Override
publicString getName() {
return"TestMySingleReam";
}
@Override
publicbooleansupports(AuthenticationToken token) {
returntokeninstanceofUsernamePasswordToken;
}
@Override
publicAuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException {
String userName=String.valueOf(token.getPrincipal());
//注意token的Credentials是char[],z主要转换.
String passWord=String.valueOf((char[])token.getCredentials());
if(!userName.equals("credo")){
thrownewUnknownAccountException("无效的账户名!");
}
if(!passWord.equals("aaa")){
thrownewIncorrectCredentialsException("密码错误!");
}
returnnewSimpleAuthenticationInfo(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测试代码
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
|
@Test
publicvoidtestSingleMyRealm() {
IniSecurityManagerFactory securityManagerFactory =newIniSecurityManagerFactory("classpath:shiro-single-realm.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject shiroSubject = SecurityUtils.getSubject();
UsernamePasswordToken normalToken =newUsernamePasswordToken("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:
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
|
packageorg.credo.test.realm.multi;
importorg.apache.shiro.authc.AuthenticationException;
importorg.apache.shiro.authc.AuthenticationInfo;
importorg.apache.shiro.authc.AuthenticationToken;
importorg.apache.shiro.authc.IncorrectCredentialsException;
importorg.apache.shiro.authc.SimpleAuthenticationInfo;
importorg.apache.shiro.authc.UnknownAccountException;
importorg.apache.shiro.authc.UsernamePasswordToken;
importorg.apache.shiro.realm.Realm;
publicclassRealmAimplementsRealm {
@Override
publicString getName() {
return"RealmA";
}
@Override
publicbooleansupports(AuthenticationToken token) {
returntokeninstanceofUsernamePasswordToken;
}
@Override
publicAuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException {
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")){
thrownewUnknownAccountException("RealmA--无效的账户名!");
}
if(!passWord.equals("123")){
thrownewIncorrectCredentialsException("RealmA--密码错误!");
}
System.out.println("pass A");
returnnewSimpleAuthenticationInfo(userName, passWord,getName());
}
}
|
realmB:
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
|
packageorg.credo.test.realm.multi;
importorg.apache.shiro.authc.AuthenticationException;
importorg.apache.shiro.authc.AuthenticationInfo;
importorg.apache.shiro.authc.AuthenticationToken;
importorg.apache.shiro.authc.IncorrectCredentialsException;
importorg.apache.shiro.authc.SimpleAuthenticationInfo;
importorg.apache.shiro.authc.UnknownAccountException;
importorg.apache.shiro.authc.UsernamePasswordToken;
importorg.apache.shiro.realm.Realm;
publicclassRealmBimplementsRealm {
@Override
publicString getName() {
return"RealmsB";
}
@Override
publicbooleansupports(AuthenticationToken token) {
returntokeninstanceofUsernamePasswordToken;
}
@Override
publicAuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException {
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")){
thrownewUnknownAccountException("RealmB--无效的账户名!");
}
if(!passWord.equals("aaa")){
thrownewIncorrectCredentialsException("RealmB--密码错误!");
}
System.out.println("pass B");
returnnewSimpleAuthenticationInfo(userName, passWord,getName());
}
}
|
1
2
3
4
|
realmA=org.credo.test.realm.multi.RealmA
realmB=org.credo.test.realm.multi.RealmB
securityManager.realms=$realmA,$realmB
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Test
publicvoidtestMultiMyRealm() {
IniSecurityManagerFactory securityManagerFactory =newIniSecurityManagerFactory("classpath:shiro-multi-realm.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject shiroSubject = SecurityUtils.getSubject();
UsernamePasswordToken normalToken =newUsernamePasswordToken("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中身份验证核心的入口点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
packageorg.apache.shiro.authc;
publicinterfaceAuthenticator {
/**
* @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
*/
publicAuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throwsAuthenticationException;
}
|
ModularRealmAuthenticator默认使用AtLeastOneSuccessfulStrategy策略。
自定义AuthenticationStrategy实现,首先看其API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//在所有Realm验证之前调用
AuthenticationInfo beforeAllAttempts(
Collection<?extendsRealm> realms, AuthenticationToken token)
throwsAuthenticationException;
//在每个Realm之前调用
AuthenticationInfo beforeAttempt(
Realm realm, AuthenticationToken token, AuthenticationInfo aggregate)
throwsAuthenticationException;
//在每个Realm之后调用
AuthenticationInfo afterAttempt(
Realm realm, AuthenticationToken token,
AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)
throwsAuthenticationException;
//在所有Realm之后调用
AuthenticationInfo afterAllAttempts(
AuthenticationToken token, AuthenticationInfo aggregate)
throwsAuthenticationException;
|
因为每个AuthenticationStrategy实例都是无状态的,所有每次都通过接口将相应的认证信息传入下一次流程;通过如上接口可以进行如合并/返回第一个验证成功的认证信息。
自定义实现时一般继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy即可
测试案例:
修改shiro.ini
1
2
3
4
5
6
7
8
9
10
11
|
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
|
RealmB的getAuthenticationInfo方法返回值修改为:return new SimpleAuthenticationInfo(userName+"@qq.com", passWord,getName());
其他不变,RealmA也不变.但验证过程用户名和密码都写正确的"credo","123"
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
|
@Test
publicvoidtestAuthenticator() {
IniSecurityManagerFactory securityManagerFactory =newIniSecurityManagerFactory("classpath:shiro-multi-realm.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject shiroSubject = SecurityUtils.getSubject();
UsernamePasswordToken normalToken =newUsernamePasswordToken("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
publicvoidtearDown()throwsException {
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: