Shiro 登录认证源码详解

			<div class="operating">
</div>
</div>
</div>
</div>
<article class="baidu_pl">
<div id="article_content" class="article_content clearfix csdn-tracking-statistics" data-pid="blog" data-mod="popu_307" data-dsm="post">
<div id="content_views" class="markdown_views">
<!-- flowchart 箭头图标 勿删 -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path></svg>
<p>Apache Shiro 是一个强大且灵活的 Java 开源安全框架,拥有登录认证、授权管理、企业级会话管理和加密等功能,相比 Spring Security 来说要更加的简单。</p>

本文主要介绍 Shiro 的登录认证(Authentication)功能,主要从 Shiro 设计的角度去看这个登录认证的过程。

一、Shiro 总览

首先,我们思考整个认证过程的业务逻辑:

  1. 获取用户输入的用户名,密码;
  2. 从服务器数据源中获取相应的用户名和密码;
  3. 判断密码是否匹配,决定是否登录成功。

我们现在来看看 Shiro 是如何设计这个过程的:

图中包含三个重要的 Shiro 概念:SubjectSecurityManagerRealm。接下来,分别介绍这三者有何用:

  • Subject:表示“用户”,表示当前执行的用户。Subject 实例全部都绑定到了一个 SecurityManager 上,当和 Subject 交互时,它是委托给 SecurityManager 去执行的。
  • SecurityManager:Shiro 结构的心脏,协调它内部的安全组件(如登录,授权,数据源等)。当整个应用配置好了以后,大多数时候都是直接和 Subject 的 API 打交道。
  • Realm:数据源,也就是抽象意义上的 DAO 层。它负责和安全数据交互(比如存储在数据库的账号、密码,权限等信息),包括获取和验证。Shiro 支持多个 Realm,但是至少也要有一个。Shiro 自带了很多开箱即用的 Reams,比如支持 LDAP、关系数据库(JDBC)、INI 和 properties 文件等。但是很多时候我们都需要实现自己的 Ream 去完成获取数据和判断的功能。

登录验证的过程就是:Subject 执行 login 方法,传入登录的「用户名」和「密码」,然后 SecurityManager 将这个 login 操作委托给内部的登录模块,登录模块就调用 Realm 去获取安全的「用户名」和「密码」,然后对比,一致则登录,不一致则登录失败。

Shiro 详细结构

二、Shiro 登录示例

代码来自 Shiro 官网教程。Shiro 配置 INI 文件:

# ----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# ----------------------------------------------------------------------------
[users]
wang=123

测试 main 方法:

public static void main(String[] args) {

    log.info("My First Apache Shiro Application");

    //1.从 Ini 配置文件中获取 SecurityManager 工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //2.获取 SecurityManager 实例
SecurityManager securityManager = factory.getInstance(); //3.将 SecurityManager 实例绑定给 SecurityUtils
SecurityUtils.setSecurityManager(securityManager); //4.获取当前登录用户
Subject currentUser = SecurityUtils.getSubject(); //5.判断是否登录,如果未登录,则登录
if (!currentUser.isAuthenticated()) {
//6.创建用户名/密码验证Token(Web 应用中即为前台获取的用户名/密码)
UsernamePasswordToken token = new UsernamePasswordToken("wang", "123");
try {
//7.执行登录,如果登录未成功,则捕获相应的异常
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
} }

三、登录逻辑详解

Shiro 登录过程主要涉及到 Subject.login 方法,接下来我们将通过查看源码来分析整个登录过程。

  1. 创建 AuthenticationToken 接口的实例 token,比如例子中的 UsernamePasswordToken,包含了登录的用户名和密码;
  2. 获取当前用户 Subject,然后调用 Subject.login(AuthenticationToken) 方法;
  3. Subjectlogin 代理给 SecurityManagerlogin()

3.1 创建AuthenticationToken

第一步是创建 AuthenticationToken 接口的身份 token,比如例子中的 UsernamePasswordToken

package org.apache.shiro.authc;

public interface AuthenticationToken extends Serializable {
// 获取“用户名”
Object getPrincipal();
// 获取“密码”
Object getCredentials();
}

3.2 获取当前用户并执行登录

获取的 Subject 当前用户是我们平时打交道最多的接口,有很多方法,但是这里我们只分析 login 方法。

package org.apache.shiro.subject;

public interface Subject {

    void login(AuthenticationToken token) throws AuthenticationException;

}

login 方法接受一个 AuthenticationToken 参数,如果登录失败则抛出 AuthenticationException 异常,可通过判断异常类型来知悉具体的错误类型。

接下来,分析 Subject 接口的实现类 DelegatingSubject 是如何实现 login 方法的:

public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
// 代理给SecurityManager
Subject subject = securityManager.login(this, token);
...
}

3.3 SecurityManager 接口

前面说过,整个 Shiro 安全框架的心脏就是 SecurityManager,我们看这个接口都有哪些方法:

package org.apache.shiro.mgt;

public interface SecurityManager extends Authenticator, Authorizer, SessionManager {

    Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;

    void logout(Subject subject);

    Subject createSubject(SubjectContext context);
}

SecurityManager 包含很多内置的模块来完成功能,比如登录(Authenticator),权限验证(Authorizer)等。这里我们看到 SecurityManager 接口继承了 Authenticator 登录认证的接口:

package org.apache.shiro.authc;

public interface Authenticator {

    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;
}

那么,SecurityManager 的实现都是怎样来实现 Authenticator 接口的呢?答案是:使用了组合。SecurityManager其中的一个实现类AuthenticatingSecurityManager中拥有一个 Authenticator 的属性,这样调用 authenticate 的时候,是委托给内部的 Authenticator 属性去执行的。

3.4 SecurityManager.login 的实现

// DefaultSecurityManager.java
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
} Subject loggedIn = createSubject(token, info, subject); onSuccessfulLogin(token, info, loggedIn); return loggedIn;
} // AuthenticatingSecurityManager.java
/**
* Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication.
*/
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
  1. 调用AuthenticatingSecurityManager接口的 authenticate 方法执行登录;
  2. authenticate 方法中代理给 Authenticator 接口类型的属性去真正执行实现类中的 authenticate(token) 方法。

3.5 Authenticator 登录模块

Authenticator 接口如下:

package org.apache.shiro.authc;

public interface Authenticator {

    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;
}

其实现类有 AbstractAuthenticatorModularRealmAuthenticator

下面来看看如何实现的 authenticate 方法:

// AbstractAuthenticator.java
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
// 调用doAuthenticate方法
info = doAuthenticate(token);
if (info == null) {
...
}
} catch (Throwable t) {
...
}
...
} // ModularRealmAuthenticator.java
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
// Realm唯一时
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
} protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
...
}
// 调用Realm的getAuthenticationInfo方法获取AuthenticationInfo信息
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
...
}
return info;
}

从源码中可以看出,最后会调用 RealmgetAuthenticationInfo(AuthenticationToken) 方法。

3.6 Realm 接口

Realm 相当于数据源,功能是通过 AuthenticationToken 获取数据源中的安全数据,这个过程中可以抛出异常,告诉 shiro 登录失败。

package org.apache.shiro.realm;

public interface Realm {

    // 获取 shiro 唯一的 realm 名称
String getName(); // 是否支持给定的 AuthenticationToken 类型
boolean supports(AuthenticationToken token); // 获取 AuthenticationInfo
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}

Shiro 自带了很多开箱即用的 Realm 实现,具体的类图如下:

3.7 总结

到此,我们把整个 Shiro 的登录认证流程分析了一遍。

  1. 创建 AuthenticationToken,然后调用 Subject.login 方法进行登录认证;
  2. Subject 委托给 SecurityManager
  3. SecurityManager 委托给 Authenticator 接口;
  4. Authenticator 接口调用 Realm 获取登录信息。

整个过程中,如果登录失败,就抛出异常,是使用异常来进行逻辑控制的。

四、登录密码的存储

  1. 页面使用 Https 协议;
  2. 页面传送密码时要先加密后再传输,最好是不可逆的加密算法(MD5,SHA2);
  3. 后端存储时要结合盐(随机数)一起加密存储;
  4. 使用不可逆的加密算法,而且可以加密多次;
  5. 把加密后的密码和盐一起存储到数据库;

五、学习 Shiro 源码感悟

  1. 从整体去思考框架的实现,带着业务逻辑去看实现逻辑;
  2. 不要抠细节,要看抽象,学习其实现方法;
  3. 首先看官方文档,官方文档一般会从整体设计方面去说明,遇到具体的接口再去看Javadoc文档;
  4. 结合类图等工具方便理解;

Shiro 登录认证源码详解的更多相关文章

  1. 源码详解系列(六) ------ 全面讲解druid的使用和源码

    简介 druid是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,druid还扩展 ...

  2. Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解

    Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解 今天主要理一下StreamingContext的启动过程,其中最为重要的就是Jo ...

  3. spring事务详解(三)源码详解

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

  4. 条件随机场之CRF++源码详解-预测

    这篇文章主要讲解CRF++实现预测的过程,预测的算法以及代码实现相对来说比较简单,所以这篇文章理解起来也会比上一篇条件随机场训练的内容要容易. 预测 上一篇条件随机场训练的源码详解中,有一个地方并没有 ...

  5. [转]Linux内核源码详解--iostat

    Linux内核源码详解——命令篇之iostat 转自:http://www.cnblogs.com/york-hust/p/4846497.html 本文主要分析了Linux的iostat命令的源码, ...

  6. saltstack源码详解一

    目录 初识源码流程 入口 1.grains.items 2.pillar.items 2/3: 是否可以用python脚本实现 总结pillar源码分析: @(python之路)[saltstack源 ...

  7. udhcp源码详解(五) 之DHCP包--options字段

    中间有很长一段时间没有更新udhcp源码详解的博客,主要是源码里的函数太多,不知道要不要一个一个讲下去,要知道讲DHCP的实现理论的话一篇博文也就可以大致的讲完,但实现的源码却要关心很多的问题,比如说 ...

  8. Activiti架构分析及源码详解

    目录 Activiti架构分析及源码详解 引言 一.Activiti设计解析-架构&领域模型 1.1 架构 1.2 领域模型 二.Activiti设计解析-PVM执行树 2.1 核心理念 2. ...

  9. 源码详解系列(七) ------ 全面讲解logback的使用和源码

    什么是logback logback 用于日志记录,可以将日志输出到控制台.文件.数据库和邮件等,相比其它所有的日志系统,logback 更快并且更小,包含了许多独特并且有用的特性. logback ...

随机推荐

  1. PHP配置错误信息回报的等级

    Error_reporting:配置错误信息回报的等级  1       E_ERROR              致命的运行错误  2      E_WARNING           运行时警告( ...

  2. Nginx反向代理、负载均衡功能

    环境: [root@db02 ~]# uname -a Linux db02 -.el6.x86_64 # SMP Tue Mar :: UTC x86_64 x86_64 x86_64 GNU/Li ...

  3. 【读书笔记】C#高级编程(一).NET体系结构

    写在前面:从业两年来,一直停留在会用的阶段,而没有去仔细思考过为什么这么用,之前也大致扫过<c#高级编程>一书,这次想借一袭脑海中的冲动,再次好好仔细过过这本书,夯实基础,温故知新. 一. ...

  4. hibernate的查询 (比较get 与load)

    hibernate的查询的比较hibernate的查询有很多,Query,find,Criteria,get,load query使用hsql语句,可以设置参数是常用的一种方式 criteria的方式 ...

  5. MySQL中在原表中做数据去重(按日期去重,保留id最小的记录)

    表名称 code600300 delete from code600300 where id not in (select minid from (select min(id) as minid fr ...

  6. 关于Activity

    Activity与界面 1.Activity相当于浏览器的标签.相当于空白的网页,界面相当于浏览器内的网页. 2.将Activity与界面绑定就相当于在浏览器内填写了相应的网页. 3.Activity ...

  7. es6新语法的使用

    1.声明变量: let 声明变量 作用域代码块作用域{} 尽在模块 先使用后声明 会报错 { let a= 12; alert(a) } let 不允许重复声明同一个变量 const 声明是一个常量, ...

  8. iOS中使用RNCryptor对资源文件加密

    原文:http://blog.csdn.net/chenpolu/article/details/46277587 RNCryptor源码https://github.com/RNCryptor/RN ...

  9. 缓存溢出Buffer Overflow

    缓存溢出(Buffer overflow),是指在存在缓存溢出安全漏洞的计算机中,攻击者可以用超出常规长度的字符数来填满一个域,通常是内存区地址.在某些情况下,这些过量的字符能够作为“可执行”代码来运 ...

  10. php文件编程

    一:文件常见操作 流的概念:当数据从程序(内存)->文件(磁盘),我们称为输出流,当数据从文件(磁盘)->程序(内存),我们称为输入流 1,获取文件信息 <?php //打开文件 f ...