2.1、什么是认证
认证是一个验证用户是他们本人的过程。用户需要提供系统识别并且信任的身份证或者一些和身份证相同作用的证明。
用户需要提交
principals
和credentials
给Shiro
从而使应用程序来验证身份。
-
Principals:
Subject
的身份属性,可以是任何东西如姓名,用户名等唯一的东西如电子邮箱/用户名。 -
Credentials:只有
Subject
知道的安全的值,如密码,数字凭证等。
2.2、认证流程
- 程序调用
Subject.login
方法,通过AuthenticationToken
实例提交用户的principals
和credentials
。 -
Subject
的实例(DelegatingSubject
或者子类)委托给Security Manager
,
Security Manager
调用securityManager.login(token)
方法负责真正的身份验证。 -
Security Manager
委托给Authenticator进行身份验证,Authenticator
才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现。 -
Authenticator
可能会委托给相应的AuthenticationStrategy
进行多Realm
身份验证,默认ModularRealmAuthenticator
会调用AuthenticationStrategy
进行多Realm
身份验证; -
Authenticato
r会把相应的token
传入Realm
,从Realm
获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm
,将按照相应的顺序及策略进行访问。
2.3、 Authenticator
综上所述,SecurityManager
实现默认使用 ModularRealmAuthenticator
实例,ModularRealmAuthenticator
同时支持单个和多个Realm
,如果配置了多个Realm
,将于AuthenticationStrategy
来协调工作。
如果想SecurityManager
用自定义Authenticator
实现来配置,你可以这样做,shiro.ini
例如:
authenticator = com.foo.bar.CustomAuthenticator
securityManager.authenticator = $authenticator
在实践中,ModularRealmAuthenticator
支持大多数的需要。
2.4、 AuthenticationStrategy
为应用程序配置两个或更多Realm
时,ModularRealmAuthenticator
依赖于内部AuthenticationStrategy
组件来确定认证成功或失败。AuthenticationStrategy
是一个无状态的组件,在认证尝试期间被查询4次(这4个交互所需的任何必要状态将作为方法参数给出):
- 在任何
Realm
被调用之前 - 在一个单独的
Realm getAuthenticationInfo
方法被调用之前 - 立即在一个单独的
Realm getAuthenticationInfo
方法被调用之后 - 在所有
Realm
被调用之后
//在所有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;
Shiro
有3个具体AuthenticationStrategy
实现:
AuthenticationStrategy 类 |
描述 |
---|---|
AtLeastOneSuccessfulStrategy |
如果一个(或多个)Realm 认证成功,则整体尝试被认为是成功的。如果没有任何验证成功,则尝试失败。 |
FirstSuccessfulStrategy |
仅使用从第一个成功验证的Realm 返回的信息。所有进一步的领土将被忽略。如果没有任何验证成功,则尝试失败。 |
AllSuccessfulStrategy |
所有配置的Realm 都必须成功进行身份验证才能成功进行整体尝试。如果任何一个人未成功认证,则尝试失败。 |
在ModularRealmAuthenticator
默认的AtLeastOneSuccessfulStrategy
实施,因为这是最常用的策略所需。但是,如果想要进行配置,可以配置不同的策略:
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authcStrategy
自定义实现时一般继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy
即可。
2.5、 Realm认证顺序
指出ModularRealmAuthenticator
与Realm实例交互的迭代顺序是非常重要的。
在ModularRealmAuthenticator
获取SecurityManager
所配置的Realm
实例。在尝试身份验证时,它将遍历该集合,并为每个Realm
支持提交的对象AuthenticationToken
调用Realm
的getAuthenticationInfo
方法。
1. 隐式排序
在shiro.ini
配置如下:
blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
将会和
securityManager.realms = $blahRealm, $fooRealm, $barRealm
配置(可无)顺序一致。
2. 显式排序
自定义顺序
blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
securityManager.realms = $fooRealm, $barRealm, $blahRealm
2.6、代码示例
项目地址:GitHub
- 构建项目环境,引入
shiro-core
与junit
jar包(maven项目构建请百度)。
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
- 编写测试类
package com.chenjy.shiro.authentication;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
public class AuthenticationTest {
SimpleAccountRealm realm = new SimpleAccountRealm();
@Before
public void addUser() {
realm.addAccount("Shiro", "1234");
}
@Test
public void testAuthentication() {
//1. 构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 1.1 设置realm
defaultSecurityManager.setRealm(realm);
// 1.2 设置SecurityManager
SecurityUtils.setSecurityManager(defaultSecurityManager);
//2. 主体提交认证
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = new UsernamePasswordToken("Shiro", "1234");
subject.login(token);
System.out.println("isAuthenticated:" + subject.isAuthenticated());
subject.logout();
System.out.println("isAuthenticated:" + subject.isAuthenticated());
}
}
运行结果为:
isAuthenticated:true
isAuthenticated:false
将用户名更改为shiro
,运行结果为:
org.apache.shiro.authc.UnknownAccountException: Realm [org.apache.shiro.realm.SimpleAccountRealm@61baa894] was unable to find account data for the submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - shiro, rememberMe=false].
……
抛出异常
org.apache.shiro.authc.UnknownAccountException
将密码改为123456
,运行结果为:
org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - Shiro, rememberMe=false] did not match the expected credentials.
……
抛出异常
org.apache.shiro.authc.IncorrectCredentialsException
以下是对认证相关内容补充(2018-05-26)。
跟随代码回顾Shiro
的认证过程:
通过SecurityUtils.getSubject()获取Subject实例subject,subject调用 subject.login(token)将AuthenticationToken实例提交认证。
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = new UsernamePasswordToken("Shiro", "123456");
subject.login(token);
查看subject.login(token)
的实现
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
……
}
可以看出subject.login(token)
将认证委托给SecurityManager
,由其实例调用login
方法进行认证。在进一步查看SecurityManager
认证的实现
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
……
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
不难看出SecurityManager
又将认证委托给了Authenticator
,而Authenticator
在验证时调用了ModularRealmAuthenticator
doAuthenticate(token)
方法,而doAuthenticate(token)
方法的实现便是通过遍历Realm
获取用户的认证权限信息。
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = doAuthenticate(token);
if (info == null) {
……
}
} catch (Throwable t) {
……
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
notifySuccess(token, info);
return info;
}
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
……
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
……
}
return info;
}
而Realm获取认证信息是通过AuthenticatingRealm
调用getAuthenticationInfo
方法,具体的实现便是与我们代码中通过SimpleAccountRealm
设置的用户名和密码进行对比。
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
……
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
……
return info;
}
//SimpleAccountRealm
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
……
}
return account;
}
在就再一次验证了之前提到过的Shiro
的认证流程
在此分析一下
AuthenticationToken
的结构,由下图可看出RememberMeAuthenticationToken
和HostAuthenticationToken
继承了AuthenticationToken
。UsernamePasswordToken
实现了RememberMeAuthenticationToken
和HostAuthenticationToken
。UsernamePasswordToken
之中的方法也如下图所示
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;
关于更多Realm的信息将在后面的章节详细讲解。在此就不再赘述。
参考文档: