参阅《跟我学shiro》—— 张开涛
身份认证就是让应用知道你是谁,是应用中的哪一个用户在访问资源,我们常用的身份认证方式包括用户名和密码、手机号、第三方登录等。在Shiro中,用户需要提供principals
身份和credentials
凭证给应用,从而使应用可以验证用户身份。
principals
:身份,即可以唯一标识一个用户,比如用户名,只要唯一即可。
principals
:凭证,主要用来验证用户的身份,只有用户知道的安全值,比如密码。
在GitHub项目的samples
包中,官方给我们提供了很多的案例
一. HelloWorld
1. 环境准备
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.5</version>
</dependency>
</dependencies>
2. 用户和凭证信息准备,创建shiro.ini
文件,内容如下。
[users]
#凭证 = 密码,所拥有的角色
user = 123456, admin
[roles]
#角色 = 所拥有的权限
admin = *
3. 进行测试
public static void main(String[] args) {
//1. 使用 shiro.ini 创建一个IniSecurityManagerFactory工厂
Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
Object instance = factory.getInstance();
//2. 将 SecurityManager 对象绑定到 SecurityUtils这是一个全局设置,设置一次即可
SecurityUtils.setSecurityManager((org.apache.shiro.mgt.SecurityManager) instance);
//3. 获得当前主体(会自动绑定到当前线程)
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("user", "123");
//4. 如果没有经过认证,则调用 login 方法进行认证
if (!currentUser.isAuthenticated()) {
try {
currentUser.login(token);
//5. 认证失败,抛出相应的异常
} catch (UnknownAccountException uae) {
log.info("用户{}不存在", token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("密码不正确");
} catch (AuthenticationException ae) {
}
}
if(currentUser.isAuthenticated()) {
log.info("用户{}登录成功", token.getPrincipal());
}
// 登出用户,其会自动委托给 SecurityManager.logout 方法退出。
currentUser.logout();
}
身份认证流程如下:
(1)如果当前用户没有经过认证,则调用login()
方法进行认证,其会自动委托给 SecurityManager。
(2)SecurityManager进行真正的认证逻辑,它会委托Authenticator认证器进行认证。
(3)Authenticator认证器才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自
定义插入自己的实现。
(4)认证器会委托给认证策略AuthenticationStrategy进行多 Realm身份认证。
(5)认证器根据用户的token获取用户的身份信息,然后根据身份信息从realm中查询对应的用户信息,在和用户token中的凭证(密码)进行比对,如果一致,表示登录成功,否则抛出对应的异常。
二. 数据源Realm
Realm:域,Shiro从域Realm中获取应用存储的用户数据(比如用户名和密码)。SecurityManager要验证用户身份,需要从 realm中获取用户数据,然后和用户输入的进行比对,可以把Realm看做数据源。如我们之前的 ini 配置就是使用IniRealm
对象获取用户数据的。
如果你配置了多个Realm
,默认会使用ModularRealmAuthenticator
调用AuthenticationStrategy
进行多 Realm 身份验证。
AuthenticationStrategy
的默认认证策略的实现如下:
(1)FirstSuccessfulStrategy
:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略。
(2)AtLeastOneSuccessfulStrategy
(默认):只要有一个Realm验证成功即可,和FirstSuccessfulStrategy 不同,它将返回所有Realm身份验证成功的认证信息。
(3)AllSuccessfulStrategy
:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。
public interface Realm {
//1. 返回唯一的 realm 名称
String getName();
//2. 判断该realm是否支持此 token
boolean supports(AuthenticationToken token);
//3. 根据 token中的身份信息,获取对应的应用用户信息
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}
Realm
接口如上所示,realm的主要作用是获取token的身份信息(比如用户名),然后从自定义的数据源中获取身份信息对应的用户信息,如果用户输入的密码正确,将会返回一个经过认证的用户身份信息。
(1)准备自定义 Realm
public class MyRealm implements Realm {
private static final Logger logger = LoggerFactory.getLogger(MyRealm.class);
public String getName() {
return "myCustomRealm";
}
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof UsernamePasswordToken;
}
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
String username = token.getUsername();
String inputPwd = new String(token.getPassword());
// 进行密码比对,如果不一致抛出异常
logger.info("使用自定义的 realm1 从数据源获取用户名{}对应的用户信息", username);
return new SimpleAuthenticationInfo("user", "123456", getName());
}
}
(2)准备 shiro.ini 文件
[main]
#指定DefaultSecurityMananger 属性 authenticator的值
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#指定认证策略
authenticationStrategy = org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authenticationStrategy
# 指定数据源
myRealm1 = com.demo.main.MyRealm
myRealm2 = com.demo.main.MyRealm1
securityManager.realms = $myRealm1, $myRealm2
测试过程和之前一致
如果想要自定义多realm的认证策略,先看一下AuthenticationStategy
接口的方法。
public interface AuthenticationStrategy {
// 在调用所有的realm之前调用该方法
//1. realms : 配置到 SecurityManager的所有数据源
//2. token:进行认证的 token
//3. return:一个空的认证信息,用来填充所有realm的认证信息
AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException;
//在调用每个realm之前调用该方法
//1. aggregate:该realm之前的认证结果
AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;
// 在调用每个realm之后调用该方法
//1. singleRealmInfo :本次调用realm的认证结果
//2. aggregateInfo : 之前的认证结果
//3. return:本次调用完成需要传递给下一个realm的认证信息
AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)
throws AuthenticationException;
//在调用所有的realm之后调用该方法
//return:返回给登录者的认证信息,是所有realm调用完成的认证信息的集合,是最终的结果
AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;
}
案例:自定义最少两个realm认证成功,否则失败。
public class RealmStrategy extends AbstractAuthenticationStrategy {
private static final Logger logger = LoggerFactory.getLogger(RealmStrategy.class);
public AuthenticationInfo afterAllAttempts(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) throws AuthenticationException {
logger.info("认证完成!");
if(authenticationInfo == null || authenticationInfo.getPrincipals().isEmpty() || authenticationInfo.getPrincipals().getRealmNames().size() < 2 ){
throw new AuthenticationException("认证过程发生异常,请确保至少包括两个realm可以认证该token");
}
return authenticationInfo;
}
}
自定义的实现一般继承AbstractAuthenticationStrategy
,然后将该类配置到SecurityManager
中即可。