Shiro源码分析笔记-简单的登陆流程分析

摘要:本文通过源码追踪,介绍 Shiro 如何实现简单的登陆控制。本文的源码追踪十分详细,基本所有的调用以及接口对象关系都包含其中。

首先我们看一张图,这张图简要介绍了Shiro登陆说需要的主要组件,大家可以查看官方的登陆步骤参考

Shiro Authentication Sequence

我们接下来更加详细介绍一下在登陆过程中源码的调用流程,前6步为生产环境的代码,也就是我们自己需要实现的最简单的登陆逻辑代码。从第7步开始为 Shiro 源码的调用。

1. SecurityManager 的工厂类

创建一个 SecurityManager 的工厂类,配置权限,这里我们可以使用最简单的 ini 配置文件来导入账号和权限。

1
2
Factory<org.apache.shiro.mgt.SecurityManager> factory =
new IniSecurityManagerFactory("classpath:shiro-realm.ini");

shiro-realm.ini 文件

1
2
3
4
5
[main]
#define realms
myRealm1=MyRealm1
#specified realms implementation of securityManager
securityManager.realms=$myRealm1

我们在下面的步骤中我们会详细介绍自定义的 myRealm1

2. SecurityManager 实例

使用工厂模式获取一个 SecurityManager 的实例。

1
2
org.apache.shiro.mgt.SecurityManager securityManager 
= factory.getInstance();

3. 配置当前 SecurityManager

使用 SecurityUtils 设置该实例为当前的 SecurityManager

1
SecurityUtils.setSecurityManager(securityManager);

4. 获取 Subject

使用 SecurityUtils 获取当前的 Subject

1
Subject subject = SecurityUtils.getSubject();

5. 准备登陆的数据,

简单的登陆数据为:用户名和密码。

1
UsernamePasswordToken token = new UsernamePasswordToken("wang", "123");

这组数据应该是放到 myRealm1 中进行匹配的。所以后面我们会看到这个用户名和密码在 myRealm1 是如何进行验证的。

6. 开始登陆

使用 Subjectlogin 方法传入准备的登陆数据来进行验证。

1
2
3
4
5
6
try {
subject.login(token);
} catch (AuthenticationException e) {
//If authentication failed will get exception.
e.printStackTrace();
}

7. Subjectlogin 方法

loginSubject 的接口,实际调用的是 DelegationgSubjectlogin 的实现。

1
public void login(AuthenticationToken token) throws AuthenticationException

8. SecurityManagerlogin

DelegationSubject 只是一个代理,在 login 方法中调用 securityManager.login(this, token), this 指的是 Subject 对象。

1
Subject subject = securityManager.login(this, token);

9. DefaultSecurityManagerlogin

这个 login 还是一个接口,这回是 SecurityManager 的接口,实际调用 DefaultSecurityManagerlogin 实现。

1
public Subject login(Subject subject, AuthenticationToken token)

10. login 的具体实现

在该login 实现中,定义了一个 AuthenticationInfoinfo 对象,这个 info 变量将是我们授权过程重要对象。然后调用父类的 authenticate(token) 方法,该方法为 AuthenticatingSecurityManager 抽象类中的方法,该方法实际是调用了成员变量 private Authenticator authenticatorauthenticate(token) 方法。该方法是 Authenticator 的接口,这时候调用抽象类 AbstractAuthenticator 的实现。这个 authencitcatorAuthenticatingSecurityManager 的构造函数中实际上是实例了 ModularRealmAuthenticator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
AuthenticationInfo info;
try {
info = authenticate(token);
}

///AuthenticatingSecurityManager.java
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}

//AuthenticatingSecurityManager.java 构造函数
public AuthenticatingSecurityManager() {
super();
this.authenticator = new ModularRealmAuthenticator();
}

//AbstractAuthenticator.java
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = doAuthenticate(token);

11. doAuthenticate 开始授权

上一步的 authenticate 方法实现中,调用了 doAuthenticate(token) 来开始授权。这个 doAuthenticate 实际上是调用的 ModularRealmAuthenticatordoAuthenticate。该方法首先获取 realm 的数量,如果是一个 realm 则进入 doSingleRealmAuthentication ,否则进入 doMultiRealmAuthentication

1
2
3
4
5
6
7
8
9
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);
}
}

12. Realm

简单案例是单 realm,所以进入 doSingleRealmAuthentication ,函数中调用 AuthenticationInfo info = realm.getAuthenticationInfo(token); 来获取 info。这个 realm 实际上是我们在配置 ini 文件中的 MyRealm1 类。

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
public class MyRealm1 implements Realm {

@Override
public String getName() {
return "myrealm1";
}

@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken; //仅支持UsernamePasswordToken类型的Token
}

@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

String username = (String)token.getPrincipal(); //得到用户名
String password = new String((char[])token.getCredentials()); //得到密码
if(!"zhang".equals(username)) {
throw new UnknownAccountException(); //如果用户名错误
}
if(!"123".equals(password)) {
throw new IncorrectCredentialsException(); //如果密码错误
}
//如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}

13. Realm 授权逻辑

如果不自己实现 Realm 将会调用了抽象 realm 实现 AuthenticatingRealmgetAuthenticationInfo 方法。而这里将会调用我们自定义的 RealmgetAuthenticationInfo,自定义getAuthenticationInfo十分简单就是匹配用户名和密码成功就返回SimpleAuthenticationInfo

这里我们介绍一下抽象的 AuthenticatingRealm 的实现方法,该方法分两步,第一步是拿到 info,第二步是验证 credential 信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
}
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
  1. info = doGetAuthenticationInfo(token); 这个方法直接把传入的 token 强制转化为 UsernamePasswordToken 父类。然后使用 getUser(token) 获取账号信息 SimpleAccount account = getUser(upToken.getUsername()); 实际上这个 User 是在配置 realm 的时候能够获取的,可以是最简单的 user/password 配置。
  2. assertCredentialsMatch(token, info); 这个方法是使用 token 来匹配 info 信息,如果没有异常表示匹配成功,如果有异常则表示匹配失败。 该方法是匹配逻辑的关键,首先获取一个 matcher:cm,本例最简单的 SimpleCredentialsMatcher。 然后 cm.doCredentialsMatch(token, info) 来进行匹配,该函数就是获取 token 中的 credential,然后获取 AuthenticationInfo infocredential 然后判断是否相等,具体如何相等,这里就是简单的匹配。

如果匹配成功无异常,返回知道返回 info 为止。

1
2
3
4
5
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials);
}

至此一个完整的简单的 Shiro 登陆的源码的详细流程就走完了。