关于shiro:其实我不仅会-Spring-SecurityShiro-也略懂一二

29次阅读

共计 37265 个字符,预计需要花费 94 分钟才能阅读完成。

和大家分享一个松哥原创的 Shiro 教程吧,还没写完,先整一部分,剩下的敬请期待。

1.Shiro 简介

Apache Shiro 是一个开源平安框架,提供身份验证、受权、密码学和会话治理。Shiro 框架具备直观、易用等个性,同时也能提供强壮的安全性,尽管它的性能不如 SpringSecurity 那么弱小,然而在一般的我的项目中也够用了。

1.1 由来

Shiro 的前身是 JSecurity,2004 年,Les Hazlewood 和 Jeremy Haile 开办了 Jsecurity。过后他们找不到实用于应用程序级别的适合 Java 平安框架,同时又对 JAAS 十分悲观。2004 年到 2008 年期间,JSecurity 托管在 SourceForge 上,贡献者包含 Peter Ledbrook、Alan Ditzel 和 Tim Veil。2008 年,JSecurity 我的项目奉献给了 Apache 软件基金会(ASF),并被接收成为 Apache Incubator 我的项目,由导师治理,指标是成为一个顶级 Apache 我的项目。期间,Jsecurity 曾短暂更名为 Ki,随后因商标问题被社区更名为“Shiro”。随后我的项目继续在 Apache Incubator 中孵化,并减少了贡献者 Kalle Korhonen。2010 年 7 月,Shiro 社区公布了 1.0 版,随后社区创立了其项目管理委员会,并选举 Les Hazlewood 为主席。2010 年 9 月 22 日,Shrio 成为 Apache 软件基金会的顶级我的项目(TLP)。

1.2 有哪些性能

Apache Shiro 是一个弱小而灵便的开源平安框架,它干净利落地解决身份认证,受权,企业会话治理和加密。Apache Shiro 的首要指标是易于应用和了解。平安有时候是很简单的,甚至是苦楚的,但它没有必要这样。框架应该尽可能覆盖简单的中央,露出一个洁净而直观的 API,来简化开发人员在应用程序平安上所破费的工夫。

以下是你能够用 Apache Shiro 所做的事件:

  1. 验证用户来核实他们的身份
  2. 对用户执行访问控制,如:判断用户是否被调配了一个确定的平安角色;判断用户是否被容许做某事
  3. 在任何环境下应用 Session API,即便没有 Web 容器
  4. 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应
  5. 汇集一个或多个用户平安数据的数据源,并作为一个繁多的复合用户“视图”
  6. 单点登录(SSO)性能
  7. 为没有关联到登录的用户启用 ”Remember Me” 服务

    等等
    

Apache Shiro 是一个领有许多性能的综合性的程序平安框架。上面的图表展现了 Shiro 的重点:

Shiro 中有四大基石——身份验证,受权,会话治理和加密。

  1. Authentication:有时也简称为“登录”,这是一个证实用户是谁的行为。
  2. Authorization:访问控制的过程,也就是决定“谁”去拜访“什么”。
  3. Session Management:治理用户特定的会话,即便在非 Web 或 EJB 应用程序。
  4. Cryptography:通过应用加密算法放弃数据安全同时易于应用。

除此之外,Shiro 也提供了额定的性能来解决在不同环境下所面临的平安问题,尤其是以下这些:

  1. Web Support:Shiro 的 web 反对的 API 可能轻松地帮忙爱护 Web 应用程序。
  2. Caching:缓存是 Apache Shiro 中的第一层公民,来确保安全操作疾速而又高效。
  3. Concurrency:Apache Shiro 利用它的并发个性来反对多线程应用程序。
  4. Testing:测试反对的存在来帮忙你编写单元测试和集成测试。
  5. “Run As”:一个容许用户假如为另一个用户身份(如果容许)的性能,有时候在治理脚本很有用。
  6. “Remember Me”:在会话中记住用户的身份,这样用户只须要在强制登录时候登录。

2. 从一个简略的案例开始身份认证

2.1 shiro 下载

要学习 shiro,咱们首先需要去 shiro 官网下载 shiro,官网地址地址 https://shiro.apache.org/,截 … 在 2017-2019 已经停更了两年,我一度认为认为这个我的项目 gg 了),本文将采纳这个版本。当然,shiro 咱们也能够从 github 上下载到源码。两个源码下载地址如下:

1.apache shiro
2.github-shiro

下面我次要是和小伙伴们介绍下源码的下载,并没有波及到 jar 包的下载,jar 包咱们到时候间接应用 maven 即可。

2.2 创立演示工程

这里咱们先不急着写代码,咱们先关上刚刚下载到的源码,源码中有一个 samples 目录,如下:

这个 samples 目录是官网给咱们的一些演示案例,其中有一个 quickstart 我的项目,这个我的项目是一个 maven 我的项目,参考这个 quickstart,咱们来创立一个本人的演示工程。

1. 首先应用 maven 创立一个 JavaSE 工程
工程创立胜利后在 pom 文件中增加如下依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-all</artifactId>
    <version>RELEASE</version>
</dependency>

2. 配置用户

参考 quickstart 我的项目中的 shiro.ini 文件,咱们来配置一个用户,配置形式如下:首先在 resources 目录下创立一个 shiro.ini 文件,文件内容如下:

[users]
sang=123,admin
[roles]
admin=*

以上配置示意咱们创立了一个名为 sang 的用户,该用户的明码是 123,该用户的角色是 admin,而 admin 具备操作所有资源的权限。

3. 执行登录

OK,做完下面几步之后,咱们就能够来看看如何实现一次简略的登录操作了。这个登录操作咱们仍然是参考 quickstart 我的项目中的类来实现,首先咱们要通过 shiro.ini 创立一个 SecurityManager,再将这个 SecurityManager 设置为单例模式,如下:

Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

如此之后,咱们就配置好了一个根本的 Shiro 环境,留神此时的用户和角色信息咱们配置在 shiro.ini 这个配置文件中,接下来咱们就能够获取一个 Subject 了,这个 Subject 就是咱们以后的用户对象,获取形式如下:

Subject currentUser = SecurityUtils.getSubject();

拿到这个用户对象之后,接下来咱们能够获取一个 session 了,这个 session 和咱们 web 中的 HttpSession 的操作基本上是统一的,不同的是,这个 session 不依赖任何容器,能够随时随地获取,获取和操作形式如下:

// 获取 session
Session session = currentUser.getSession();
// 给 session 设置属性值
session.setAttribute("someKey", "aValue");
// 获取 session 中的属性值
String value = (String) session.getAttribute("someKey");

说了这么多,咱们的用户到当初还没有登录呢,Subject 中有一个 isAuthenticated 办法用来判断以后用户是否曾经登录,如果 isAuthenticated 办法返回一个 false,则示意以后用户未登录,那咱们就能够执行登陆,登录形式如下:

if (!currentUser.isAuthenticated()) {UsernamePasswordToken token = new UsernamePasswordToken("sang", "123");
    try {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 (AuthenticationException ae) {}}

首先结构 UsernamePasswordToken,两个参数就是咱们的用户名和明码,而后调用 Subject 中的 login 办法执行登录,当用户名输错,明码输错、或者账户锁定等问题呈现时,零碎会通过抛异样告知调用者这些问题。

当登录胜利之后,咱们能够通过如下形式获取以后登陆用户的用户名:

log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

咱们也能够通过调用 Subject 中的 hasRole 和 isPermitted 办法来判断以后用户是否具备某种角色或者某种权限,如下:

if (currentUser.hasRole("admin")) {log.info("May the Schwartz be with you!");
} else {log.info("Hello, mere mortal.");
}
if (currentUser.isPermitted("lightsaber:wield")) {log.info("You may use a lightsaber ring.  Use it wisely.");
} else {log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

最初,咱们能够通过 logout 办法登记本次登录,如下:

currentUser.logout();

OK,至此,咱们通过官网案例给小伙伴们简略介绍了 Shiro 中的登录操作,残缺案例大家能够参考官网的 demo。

3. 聊一聊 Shiro 中的 Realm

3.1 登录流程是什么样的

首先咱们来看 shiro 官网文档中这样一张登录流程图:

参照此图,咱们的登录一共要通过如下几个步骤:

  1. 利用程序代码调用 Subject.login 办法,传递创立好的蕴含终端用户的 Principals(身份)和 Credentials(凭证)的 AuthenticationToken 实例(即上文例子中的 UsernamePasswordToken)。
  2. Subject 实例,通常是 DelegatingSubject(或子类)委托应用程序的 SecurityManager 通过调用 securityManager.login(token)开始真正的验证工作(在 DelegatingSubject 类的 login 办法中打断点即可看到)。
  3. SubjectManager 作为一个根本的“保护伞”的组成部分,接管 token 以及简略地委托给外部的 Authenticator 实例通过调用 authenticator.authenticate(token)。这通常是一个 ModularRealmAuthenticator 实例,反对在身份验证中协调一个或多个 Realm 实例。ModularRealmAuthenticator 实质上为 Apache Shiro 提供了 PAM-style 范式(其中在 PAM 术语中每个 Realm 都是一个 ’module’)。
  4. 如果应用程序中配置了一个以上的 Realm,ModularRealmAuthenticator 实例将利用配置好的 AuthenticationStrategy 来启动 Multi-Realm 认证尝试。在 Realms 被身份验证调用之前,期间和当前,AuthenticationStrategy 被调用使其可能对每个 Realm 的后果作出反应。如果只有一个繁多的 Realm 被配置,它将被间接调用,因为没有必要为一个繁多 Realm 的利用应用 AuthenticationStrategy。
  5. 每个配置的 Realm 用来帮忙看它是否反对提交的 AuthenticationToken。如果反对,那么反对 Realm 的 getAuthenticationInfo 办法将会随同着提交的 token 被调用。

OK,通过下面的介绍,置信小伙伴们对整个登录流程都有肯定的了解了,小伙伴能够通过打断点来验证咱们上文所说的五个步骤。那么在下面的五个步骤中,小伙伴们看到了有一个 Realm 承当了很重要的一部分工作,那么这个 Realm 到底是个什么货色,接下来咱们就来认真看一看。

3.2 什么是 Realm

依据 Realm 文档上的解释,Realms 担当 Shiro 和你的应用程序的平安数据之间的“桥梁”或“连接器”。当它实际上与平安相干的数据如用来执行身份验证(登录)及受权(访问控制)的用户帐户交互时,Shiro 从一个或多个为应用程序配置的 Realm 中寻找许多这样的货色。在这个意义上说,Realm 实质上是一个特定平安的 DAO:它封装了数据源的连贯详细信息,使 Shiro 所需的相干的数据可用。当配置 Shiro 时,你必须指定至多一个 Realm 用来进行身份验证和 / 或受权。SecurityManager 可能配置多个 Realms,但至多有一个是必须的。Shiro 提供了立刻可用的 Realms 来连贯一些平安数据源(即目录),如 LDAP,关系数据库(JDBC),文本配置源,像 INI 及属性文件,以及更多。你能够插入你本人的 Realm 实现来代表自定义的数据源,如果默认地 Realm 不合乎你的需要。

看了下面这一段解释,可能还有小伙伴云里雾里,那么接下来咱们来通过一个简略的案例来看看 Realm 到底表演了一个什么样的作用,留神,本文的案例在上文案例的根底上实现。首先自定义一个 MyRealm,内容如下:

public class MyRealm implements Realm {public String getName() {return "MyRealm";}
    public boolean supports(AuthenticationToken token) {return token instanceof UsernamePasswordToken;}
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String password = new String(((char[]) token.getCredentials()));
        String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {throw new UnknownAccountException("用户不存在");
        }
        if (!"123".equals(password)) {throw new IncorrectCredentialsException("明码不正确");
        }
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}

自定义 Realm 实现 Realm 接口,该接口中有三个办法,第一个 getName 办法用来获取以后 Realm 的名字,第二个 supports 办法用来判断这个 realm 所反对的 token,这里我假如值只反对 UsernamePasswordToken 类型的 token,第三个 getAuthenticationInfo 办法则进行了登陆逻辑判断,从 token 中取出用户的用户名明码等,进行判断,当然,我这里省略掉了数据库操作,当登录验证呈现问题时,抛异样即可,这里抛出的异样,将在执行登录那里捕捉到(留神,因为我这里定义的 MyRealm 是实现了 Realm 接口,所以这里的用户名和明码都须要我手动判断是否正确,前面的文章我会介绍其余写法)。

OK,创立好了 MyRealm 之后还不够,咱们还须要做一个简略配置,让 MyRealm 失效,将 shiro.ini 文件中的所有货色都正文掉,增加如下两行:

MyRealm= org.sang.MyRealm
securityManager.realms=$MyRealm

第一行示意定义了一个 realm,第二行将这个定义好的交给 securityManger,这里实际上会调用到 RealmSecurityManager 类的 setRealms 办法。OK,做好这些之后,小伙伴们能够在 MyRealm 类中的一些要害节点打上断点,再次执行 main 办法,看看整个的登录流程。

4. 再来聊一聊 Shiro 中的 Realm

4.1 Realm 的继承关系

通过查看类的继承关系,咱们发现 Realm 的子类实际上有很多种,这里咱们就来看看有代表性的几种:

  1. IniRealm

可能咱们并不知道,实际上这个类在咱们第二篇文章中就曾经用过了。这个类一开始就有如下两行定义:

public static final String USERS_SECTION_NAME = "users";
public static final String ROLES_SECTION_NAME = "roles";

这两行配置示意 shiro.ini 文件中,[users]上面的示意表用户名明码还有角色,[roles]上面的则是角色和权限的对应关系。

  1. PropertiesRealm

PropertiesRealm 则规定了另外一种用户、角色定义形式,如下:

user.user1=password,role1
role.role1=permission1

  1. JdbcRealm

这个顾名思义,就是从数据库中查问用户的角色、权限等信息。关上 JdbcRealm 类,咱们看到源码中有如下几行:

protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";

依据这几行预设的 SQL 咱们就能够大抵推断出数据库中表的名称以及字段了,当然,咱们也能够自定义 SQL。JdbcRealm 实际上是 AuthenticatingRealm 的子类,对于 AuthenticatingRealm 咱们在前面还会具体说到,这里先不开展。接下来咱们就来具体说说这个 JdbcRealm。

4.2 JdbcRealm

  1. 筹备工作

应用 JdbcRealm,波及到数据库操作,要用到数据库连接池,这里我应用 Druid 数据库连接池,因而首先增加如下依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>RELEASE</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>
  1. 数据库创立

想要应用 JdbcRealm,那我首先要创立数据库,依据 JdbcRealm 中预设的 SQL,我定义的数据库表构造如下:

这里为了大家可能直观的看到表的关系,我应用了外键,理论工作中,视状况而定。而后向表中增加几条测试数据。数据库脚本小伙伴能够在 github 上下载到(https://github.com/lenve/shir…

  1. 配置文件解决

而后将 shiro.ini 中的所有配置正文掉,增加如下配置:

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm

这里的配置文件都很简略,不做过多赘述,小伙伴惟一须要留神的是 permissionsLookupEnabled 须要设置为 true,否则一会 JdbcRealm 就不会去查问权限用户权限。

  1. 测试

OK,做完下面几步就能够测试了,测试形式和第二篇文章中一样,咱们能够测试下用户登录,用户角色和用户权限。

  1. 自定义查问 SQL

小伙伴们看懂了上文,对于自定义查问 SQL 就没什么问题了。我这里举一个简略的例子,比方我要自定义 authenticationQuery 对对应的 SQL,查看 JdbcRealm 源码,咱们发现 authenticationQuery 对应的 SQL 原本是select password from users where username = ?,如果须要批改的话,比如说我的表名不是 users 而是 employee,那么在 shiro.ini 中增加如下配置即可:

jdbcRealm.authenticationQuery=select password from employee where username = ?

OK, 这个小伙伴下来本人做尝试,我这里就不演示了。

5. Shiro 中多 Realm 的认证策略问题

5.1 多 Realm 认证策略

不晓得小伙伴们是否还记得这张登录流程图:

从这张图中咱们能够清晰看到 Realm 是能够有多个的,不过到目前为止,咱们所有的案例都还是单 Realm,那么咱们先来看一个简略的多 Realm 状况。

后面的文章咱们本人创立了一个 MyRealm,也用过 JdbcRealm,但都是独自应用的,当初我想将两个一起应用,只须要批改 shiro.ini 配置即可,如下:

MyRealm= org.sang.MyRealm

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm,$MyRealm

然而此时我数据库中用户的信息是 sang/123,MyRealm 中配置的信息也是 sang/123,我把 MyRealm 中的用户信息批改为 江南一点雨 /456,此时,我的 MyRealm 的 getAuthenticationInfo 办法如下:

public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String password = new String(((char[]) token.getCredentials()));
    String username = token.getPrincipal().toString();
    if (!"江南一点雨".equals(username)) {throw new UnknownAccountException("用户不存在");
    }
    if (!"456".equals(password)) {throw new IncorrectCredentialsException("明码不正确");
    }
    return new SimpleAuthenticationInfo(username, password, getName());
}

这个时候咱们就配置了两个 Realm,还是应用咱们一开始的测试代码进行登录测试,这个时候咱们发现我既能够应用 江南一点雨 /456进行登录,也能够应用 sang/123 进行登录,用 sang/123 登录胜利之后用户的角色信息和之前是一样的,而用 江南一点雨 /456登录胜利之后用户没有角色,这个也很好了解,因为咱们在 MyRealm 中没有给用户配置任何权限。总而言之,就是当我有了两个 Realm 之后,当初只须要这两个 Realm 中的任意一个认证胜利,就算我以后用户认证胜利。

5.2 原理追踪

好了,有了下面的问题后,接下来咱们在 Subject 的 login 办法上打断点,追随程序的执行步骤,咱们来到了 ModularRealmAuthenticator 类的 doMultiRealmAuthentication 办法中,如下:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {this.assertRealmsConfigured();
    Collection<Realm> realms = this.getRealms();
    return realms.size() == 1?this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken):this.doMultiRealmAuthentication(realms, authenticationToken);
}

在这个办法中,首先会获取以后一共有多少个 realm,如果只有一个则执行 doSingleRealmAuthentication 办法进行解决,如果有多个 realm,则执行 doMultiRealmAuthentication 办法进行解决。doSingleRealmAuthentication 办法局部源码如下:

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    ...
    ...
    AuthenticationInfo info = realm.getAuthenticationInfo(token);
    if(info == null) {String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "].";
        throw new UnknownAccountException(msg);
    } else {return info;}
}

小伙伴们看到这里就明确了,这里调用了 realm 的 getAuthenticationInfo 办法,这个办法实际上就是咱们本人实现的 MyRealm 中的 getAuthenticationInfo 办法。

那如果有多个 Realm 呢?咱们来看看 doMultiRealmAuthentication 办法的实现,局部源码如下:

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {AuthenticationStrategy strategy = this.getAuthenticationStrategy();
    AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
    Iterator var5 = realms.iterator();
    while(var5.hasNext()) {Realm realm = (Realm)var5.next();
        aggregate = strategy.beforeAttempt(realm, token, aggregate);
        if(realm.supports(token)) {
            AuthenticationInfo info = null;
            Throwable t = null;
            try {info = realm.getAuthenticationInfo(token);
            } catch (Throwable var11) { }
            aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
        } else {log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
        }
    }
    aggregate = strategy.afterAllAttempts(token, aggregate);
    return aggregate;
}

我这里次要来说下这个办法的实现思路:

  1. 首先获取多 Realm 认证策略
  2. 构建一个 AuthenticationInfo 用来寄存一会认证胜利之后返回的信息
  3. 遍历 Realm,调用每个 Realm 中的 getAuthenticationInfo 办法,看是否可能认证胜利
  4. 每次获取到 AuthenticationInfo 之后,都调用 afterAttempt 办法进行后果合并
  5. 遍历完所有的 Realm 之后,调用 afterAllAttempts 进行后果合并,这里次要判断下是否一个都没匹配上

5.3 自在配置认证策略

OK,通过下面的简略解析,小伙伴们对认证策略应该有一个大抵的意识了,那么在 Shiro 中,一共反对三种不同的认证策略,如下:

  1. AllSuccessfulStrategy,这个示意所有的 Realm 都认证胜利才算认证胜利
  2. AtLeastOneSuccessfulStrategy,这个示意只有有一个 Realm 认证胜利就算认证胜利,默认即此策略
  3. FirstSuccessfulStrategy,这个示意只有第一个 Realm 认证胜利,就算认证胜利

配置形式也很简略,在 shiro.ini 中进行配置,在下面配置的根底上,减少如下配置:

authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy

此时,咱们再进行登录测试,则会要求每个 Realm 都认证通过才算认证通过。

6. Shiro 中明码加密

6.1 明码为什么要加密

2011 年 12 月 21 日,有人在网络上公开了一个蕴含 600 万个 CSDN 用户材料的数据库,数据全副为明文贮存,蕴含用户名、明码以及注册邮箱。事件产生后 CSDN 在微博、官方网站等渠道收回了申明,解释说此数据库系 2009 年备份所用,因不明起因泄露,曾经向警方报案。后又在官网网站收回了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最惊心动魄的莫过于 CSDN 把用户明码明文存储,因为很多用户是多个网站共用一个明码,因而一个网站明码泄露就会造成很大的安全隐患。因为有了这么多前事不忘; 后事之师,咱们当初做零碎时,明码都要加密解决。

明码加密咱们个别会用到散列函数,又称散列算法、哈希函数,是一种从任何一种数据中创立小的数字“指纹”的办法。散列函数把音讯或数据压缩成摘要,使得数据质变小,将数据的格局固定下来。该函数将数据打乱混合,从新创立一个叫做散列值的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输出域中很少呈现散列抵触。在散列表和数据处理中,不克制抵触来区别数据,会使得数据库记录更难找到。咱们罕用的散列函数有如下几种:

  1. MD5 音讯摘要算法

MD5 音讯摘要算法是一种被宽泛应用的明码散列函数,能够产生出一个 128 位(16 字节)的散列值,用于确保信息传输残缺统一。MD5 由美国明码学家罗纳德·李维斯特设计,于 1992 年公开,用以取代 MD4 算法。这套算法的程序在 RFC 1321 中被加以标准。将数据(如一段文字)运算变为另一固定长度值,是散列算法的根底原理。1996 年后被证实存在弱点,能够被加以破解,对于须要高度安全性的数据,专家个别倡议改用其余算法,如 SHA-2。2004 年,证实 MD5 算法无奈避免碰撞,因而不适用于安全性认证,如 SSL 公开密钥认证或是数字签名等用处。

  1. 平安散列算法

平安散列算法(Secure Hash Algorithm)是一个明码散列函数家族,是 FIPS 所认证的平安散列算法。能计算出一个数字音讯所对应到的,长度固定的字符串(又称音讯摘要)的算法。且若输出的音讯不同,它们对应到不同字符串的机率很高。SHA 家族的算法,由美国国家安全局所设计,并由美国国家标准与技术研究院公布,是美国的政府规范,其别离是:SHA-0:1993 年公布,是 SHA- 1 的前身;SHA-1:1995 年公布,SHA- 1 在许多平安协定中广为应用,包含 TLS 和 SSL、PGP、SSH、S/MIME 和 IPsec,曾被视为是 MD5 的后继者。但 SHA- 1 的安全性在 2000 年当前曾经不被大多数的加密场景所承受。2017 年荷兰密码学钻研小组 CWI 和 Google 正式发表攻破了 SHA-1;SHA-2:2001 年公布,包含 SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。尽管至今尚未呈现对 SHA- 2 无效的攻打,它的算法跟 SHA- 1 基本上依然类似;因而有些人开始倒退其余代替的散列算法;SHA-3:2015 年正式公布,SHA- 3 并不是要取代 SHA-2,因为 SHA- 2 目前并没有呈现显著的弱点。因为对 MD5 呈现胜利的破解,以及对 SHA- 0 和 SHA- 1 呈现实践上破解的办法,NIST 感觉须要一个与之前算法不同的,可替换的加密散列算法,也就是当初的 SHA-3。

6.2 Shiro 中如何加密

Shiro 中对以上两种散列算法都提供了反对,对于 MD5,Shiro 中生成音讯摘要的形式如下:

Md5Hash md5Hash = new Md5Hash("123", null, 1024);

第一个参数是要生成明码的明文,第二个参数明码的盐值,第三个参数是生成音讯摘要的迭代次数。

Shiro 中对于平安散列算法的反对如下(反对多种算法,这里我举一个例子):

Sha512Hash sha512Hash = new Sha512Hash("123", null, 1024);

这里三个参数含意与上文基本一致,不再赘述。shiro 中也提供了通用的算法,如下:

SimpleHash md5 = new SimpleHash("md5", "123", null, 1024);
SimpleHash sha512 = new SimpleHash("sha-512", "123", null, 1024);

当用户注册时,咱们能够通过下面的形式对明码进行加密,将加密后的字符串存入数据库中。我这里为了简略,就不写注册性能了,就把昨天数据库中用户的明码 123 改成 sha512 所对应的字符串,如下:

cb5143cfcf5791478e057be9689d2360005b3aac951f947af1e6e71e3661bf95a7d14183dadfb0967bd6338eb4eb2689e9c227761e1640e6a033b8725fabc783

同时,为了防止其余 Realm 的烦扰,数据库中我只配置一个 JdbcRealm。

此时如果我不做其余批改的话,登录必然会失败,起因很简略:我登录时输出的明码是 123,然而数据库中的明码是一个很长的字符串,所以登录必定不会胜利。通过打断点,咱们发现最终的明码比对是在 SimpleCredentialsMatcher 类中的 doCredentialsMatch 办法中进行明码比对的,比对的形式也很简略,间接应用了对用户输出的明码和数据库中的明码生成 byte 数组而后进行比拟,最终的比拟在 MessageDigest 类的 isEqual 办法中。局部逻辑如下:

protected boolean equals(Object tokenCredentials, Object accountCredentials) {
        ...
        ...
        // 获取用户输出明码的 byte 数组
        byte[] tokenBytes = this.toBytes(tokenCredentials);
        // 获取数据库中明码的 byte 数组
        byte[] accountBytes = this.toBytes(accountCredentials);
        return MessageDigest.isEqual(tokenBytes, accountBytes);
        ...
}

MessageDigest 的 isEqual 办法如下:

public static boolean isEqual(byte[] digesta, byte[] digestb) {if (digesta == digestb) return true;
    if (digesta == null || digestb == null) {return false;}
    if (digesta.length != digestb.length) {return false;}

    int result = 0;
    // time-constant comparison
    for (int i = 0; i < digesta.length; i++) {result |= digesta[i] ^ digestb[i];
    }
    return result == 0;
}

都是很容易了解的比拟代码,这里不赘述。咱们当初之所以登录失败是因为没有对用户输出的明码进行加密,通过对源代码的剖析,咱们发现是因为在 AuthenticatingRealm 类的 assertCredentialsMatch 办法中获取了一个名为 SimpleCredentialsMatcher 的明码比对器,这个明码比对器中比对的办法就是简略的比拟,因而如果咱们可能将这个明码比对器换掉就好了。咱们来看一下 CredentialsMatcher 的继承关系:

咱们发现这个刚好有一个 Sha512CredentialsMatcher 比对器,这个比对器的 doCredentialsMatch 办法在它的父类 HashedCredentialsMatcher,办法内容如下:

public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {Object tokenHashedCredentials = hashProvidedCredentials(token, info);
    Object accountCredentials = getCredentials(info);
    return equals(tokenHashedCredentials, accountCredentials);
}

这时咱们发现获取 tokenHashedCredentials 的形式不像以前那样简略粗犷了,而是调用了 hashProvidedCredentials 办法,而 hashProvidedCredentials 办法最终会来到上面这个重载办法中:

protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {String hashAlgorithmName = assertHashAlgorithmName();
    return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
}

这几行代码似曾相识,很显著,是零碎帮咱们对用户输出的明码进行了转换。理解了这些之后,那我只须要将 shiro.ini 批改成如下样子即可实现登录了:

sha512=org.apache.shiro.authc.credential.Sha512CredentialsMatcher
# 迭代次数
sha512.hashIterations=1024
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
# 批改 JdbcRealm 中的 credentialsMatcher 属性
jdbcRealm.credentialsMatcher=$sha512
securityManager.realms=$jdbcRealm

如此之后,咱们再进行登录测试,就能够登录胜利了。

本大节案例下载:https://github.com/lenve/shir…

7. Shiro 中明码加盐

7.1 明码为什么要加盐

不论是音讯摘要算法还是平安散列算法,如果原文一样,生成密文也是一样的,这样的话,如果两个用户的明码原文一样,存到数据库中密文也就一样了,还是不平安,咱们须要做进一步解决,常见解决方案就是加盐。盐从那里来呢?咱们能够应用用户 id(因为个别状况下,用户 id 是惟一的),也能够应用一个随机字符,我这里采纳第一种计划。

7.2 Shiro 中如何实现加盐

shiro 中加盐的形式很简略,在用户注册时生成明码密文时,就要退出盐,如下几种形式:

Md5Hash md5Hash = new Md5Hash("123", "sang", 1024);
Sha512Hash sha512Hash = new Sha512Hash("123", "sang", 1024);
SimpleHash md5 = new SimpleHash("md5", "123", "sang", 1024);
SimpleHash sha512 = new SimpleHash("sha-512", "123", "sang", 1024)

而后咱们首先将 sha512 生成的字符串放入数据库中,接下来我要配置一下我的 jdbcRealm,因为我要指定我的盐是什么。在这里我的盐就是我的用户名,每个用户的用户名是不一样的,因而这里没法写死,在 JdbcRealm 中,零碎提供了四种不同的 SaltStyle,如下:

SaltStyle 含意
NO_SALT 默认,明码不加盐
CRYPT 明码是以 Unix 加密形式贮存的
COLUMN salt 是独自的一列贮存在数据库中
EXTERNAL salt 没有贮存在数据库中,须要通过 JdbcRealm.getSaltForUser(String)函数获取

四种不同的 SaltStyle 对应了四种不同的明码解决形式,局部源码如下:

switch (saltStyle) {
case NO_SALT:
    password = getPasswordForUser(conn, username)[0];
    break;
case CRYPT:
    // TODO: separate password and hash from getPasswordForUser[0]
    throw new ConfigurationException("Not implemented yet");
    //break;
case COLUMN:
    String[] queryResults = getPasswordForUser(conn, username);
    password = queryResults[0];
    salt = queryResults[1];
    break;
case EXTERNAL:
    password = getPasswordForUser(conn, username)[0];
    salt = getSaltForUser(username);
}

在 COLUMN 这种状况下,SQL 查问后果应该蕴含两列,第一列是明码,第二列是盐,这里默认执行的 SQL 在 JdbcRealm 一结尾就定义好了,如下:

protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";

即零碎默认的盐是数据表中的 password_salt 提供的,然而我这里是 username 字段提供的,所以这里我一会要自定义这条 SQL。自定义形式很简略,批改 shiro.ini 文件,增加如下两行:

jdbcRealm.saltStyle=COLUMN
jdbcRealm.authenticationQuery=select password,username from users where username=?

首先设置 saltStyle 为 COLUMN,而后从新定义 authenticationQuery 对应的 SQL。留神返回列的程序很重要,不能随便调整。如此之后,零碎就会主动把 username 字段作为盐了。

不过,因为 ini 文件中不反对枚举,saltStyle 的值实际上是一个枚举类型,所以咱们在测试的时候,须要减少一个枚举转换器在咱们的 main 办法中,如下:

BeanUtilsBean.getInstance().getConvertUtils().register(new AbstractConverter() {
    @Override
    protected String convertToString(Object value) throws Throwable {return ((Enum) value).name();}

    @Override
    protected Object convertToType(Class type, Object value) throws Throwable {return Enum.valueOf(type, value.toString());
    }

    @Override
    protected Class getDefaultType() {return null;}
}, JdbcRealm.SaltStyle.class);

当然,当前当咱们将 shiro 和 web 我的项目整合之后,就不须要这个转换器了。

如此之后,咱们就能够再次进行登录测试了,会发现没什么问题了。

7.3 非 JdbcRealm 如何配置盐

OK,刚刚是在 JdbcRealm 中配置了盐,如果没用 JdbcRealm,而是本人定义的一般 Realm,要怎么解决配置盐的问题?

首先要阐明一点是,咱们后面的文章在自定义 Realm 时都是通过实现 Realm 接口实现的,这种形式有一个缺点,就是明码比对须要咱们本人实现,个别在我的项目中,咱们自定义 Realm 都是通过继承 AuthenticatingRealm 或者 AuthorizingRealm,因为这两个办法中都重写了 getAuthenticationInfo 办法,而在 getAuthenticationInfo 办法中,调用 doGetAuthenticationInfo 办法获取登录用户,获取到之后,会调用 assertCredentialsMatch 办法进行明码比对,而咱们间接实现 Realm 接口则没有这一步,局部源码如下:

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {AuthenticationInfo info = getCachedAuthenticationInfo(token);
    if (info == null) {
        // 调用 doGetAuthenticationInfo 获取 info,这个 doGetAuthenticationInfo 是咱们在自定义 Realm 中本人实现的
        info = doGetAuthenticationInfo(token);
        log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
        if (token != null && info != null) {cacheAuthenticationInfoIfPossible(token, info);
        }
    } else {log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }
    if (info != null) {
        // 获取到 info 之后,进行明码比对
        assertCredentialsMatch(token, info);
    } else {log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
    }

    return info;
}

基于下面所述的起因,这里我先继承 AuthenticatingRealm,如下:

public class MyRealm extends AuthenticatingRealm {public String getName() {return "MyRealm";}
    public boolean supports(AuthenticationToken token) {return token instanceof UsernamePasswordToken;}
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {throw new UnknownAccountException("用户不存在");
        }
        String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58";
        return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
    }
}

对于这个类,我说如下几点:

  1. 用户名我这里还是手动判断了下,实际上这个中央要从数据库查问用户信息,如果查不到用户信息,则间接抛 UnknownAccountException
  2. 返回的 SimpleAuthenticationInfo 中,第二个参数是明码,失常状况下,这个明码是从数据库中查问进去的,我这里间接写死了
  3. 第三个参数是盐值,这样结构好 SimpleAuthenticationInfo 之后返回,shiro 会去判断用户输出的明码是否正确

下面的外围步骤是第三步,零碎去主动比拟明码输出是否正确,在比对的过程中,须要首先对用户输出的明码进行加盐加密,既然加盐加密,就会波及到 credentialsMatcher,这里咱们要用的 credentialsMatcher 实际上和在 JdbcRealm 中用的 credentialsMatcher 一样,只须要在配置文件中减少如下一行即可:

MyRealm.credentialsMatcher=$sha512

sha512 和咱们上文定义的统一,这里就不再反复说了。

本大节案例下载:https://github.com/lenve/shir…

8. Shiro 中自定义带角色和权限的 Realm

明码加密加盐小伙伴们应该没有问题了,然而后面几篇文章又给咱们带来了一个新的问题:咱们后面 IniRealm、JdbcRealm 以及自定义的 MyRealm,其中前两个咱们都能实现用户认证以及受权,即既能治理用户登录,又能治理用户角色,而咱们自定义的 MyRealm,目前还只能实现登录,不能实现受权,本文咱们就来看看自定义 Realm 如何实现受权。

8.1 问题追踪

上篇文章咱们没有实现自定义 Realm 的受权操作,然而这个并不影响咱们调用 hasRole 办法去获取用户的权限,我在上文测试代码上的 currentUser.hasRole 下面打断点,通过层层追踪,咱们发现最终来到了 ModularRealmAuthorizer 类的 hasRole 办法中,局部源码如下:

public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {assertRealmsConfigured();
    for (Realm realm : getRealms()) {if (!(realm instanceof Authorizer)) continue;
        if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {return true;}
    }
    return false;
}

咱们看到在这里会遍历所有的 realm,如果这个 realm 是 Authorizer 的实例,则会进行进一步的受权操作,如果不是 Authorizer 的实例,则间接跳过,而咱们只有一个自定义的 MyRealm 继承自 AuthenticatingRealm,很显著不是 Authorizer 的实例,所以这里必然返回 false,受权失败,所以要解决受权问题,第一步,得先让咱们的 MyRealm 成为 Authorizer 的实例。

8.2 解决方案

如下图是 Authorizer 的继承关系:

小伙伴们看到,在 Authorizer 的实现类中有一个 AuthorizingRealm,关上这个类,咱们发现它的继承关系如下:

public abstract class AuthorizingRealm extends AuthenticatingRealm
        implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {...}

咱们发现,这个 AuthorizingRealm 不仅是 Authorizer 的实现类,同时也是咱们上文所用的 AuthenticatingRealm 的实现类,既然 AuthorizingRealm 同时是这两个类的实现类,那么我把 MyRealm 的继承关系由 AuthenticatingRealm 改为 AuthorizingRealm,必定不会影响我上文的性能,批改之后的 MyRealm 如下(局部要害代码):

public class MyRealm extends AuthorizingRealm {protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {throw new UnknownAccountException("用户不存在");
        }
        String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58";
        return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
    }

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {Set<String> roles = new HashSet<String>();
        if ("sang".equals(principals.getPrimaryPrincipal().toString())) {roles.add("普通用户");
        }
        return new SimpleAuthorizationInfo(roles);
    }
}

继承了 AuthorizingRealm 之后,须要咱们实现 doGetAuthorizationInfo 办法。在这个办法中,咱们配置用户的权限。这里我为了不便,间接增加了普通用户这个权限,实际上,这里应该依据用户名去数据库里查问权限,查问形式不赘述。

通过源码追踪,咱们发现最终受权会来到 AuthorizingRealm 类的如下两个办法中:

public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {AuthorizationInfo info = getAuthorizationInfo(principal);
    return hasRole(roleIdentifier, info);
}

protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
}

这两个办法的逻辑很简略,第一个办法中调用的 getAuthorizationInfo 办法会最终调用到咱们自定义的 doGetAuthorizationInfo 办法,第二个 hasRole 办法接管的两个参数,第一个是用户申请的角色,第二个是用户具备的角色集,一个简略的 contains 函数就判断出用户是否具备某个角色了。

然而这个时候,用户只有角色,没有权限,咱们能够对 doGetAuthorizationInfo 办法做进一步的欠缺,如下:

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {Set<String> roles = new HashSet<String>();
    Set<String> permiss = new HashSet<String>();
    if ("sang".equals(principals.getPrimaryPrincipal().toString())) {roles.add("普通用户");
        permiss.add("book:update");
    }
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
    info.setStringPermissions(permiss);
    return info;
}

当然,失常状况下,权限也该当是从数据库中查问失去的,我这里简化下。

那么这个角色是怎么验证的呢?追踪源码咱们来到了 AuthorizingRealm 类的如下两个办法中:

public boolean isPermitted(PrincipalCollection principals, Permission permission) {AuthorizationInfo info = getAuthorizationInfo(principals);
    return isPermitted(permission, info);
}

//visibility changed from private to protected per SHIRO-332
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {Collection<Permission> perms = getPermissions(info);
    if (perms != null && !perms.isEmpty()) {for (Permission perm : perms) {if (perm.implies(permission)) {return true;}
        }
    }
    return false;
}

第一个 isPermitted 办法中调用了 getAuthorizationInfo 办法,而 getAuthorizationInfo 办法最终会调用到咱们本人定义的 doGetAuthorizationInfo 办法,即获取到用户的角色权限信息,而后在第二个办法中进行遍历判断,查看是否具备相应的权限,第二个 isPermitted 办法的第一个参数就是用户要申请的权限。

本大节案例下载:https://github.com/lenve/shir…

9. Shiro 整合 Spring

9.1 Spring&SpringMVC 环境搭建

Spring 和 SpringMVC 环境的搭建,整体上来说,还是比拟容易的,因为这个不是本文的重点,因而这里我不做具体介绍,小伙伴能够在文末下载源码查看 Spring+SpringMVC 环境的搭建。同时,因为 MyBatis 的整合绝对要容易很多,这里为了升高我的项目复杂度,我也就先不引入 MyBatis。

对于我的项目依赖,除了 Spring、SpringMVC、Shiro 相干的依赖,还须要退出 Shiro 和 Spring 整合的 jar,如下:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>RELEASE</version>
</dependency>

9.2 整合 Shiro

搭建好 Spring+SpringMVC 环境之后,整合 Shiro 咱们次要配置两个中央:

  1. web.xml 中配置代理过滤器,如下:
<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

这样之后,当 DelegatingFilterProxy 拦挡到所有申请之后,都会委托给 shiroFilter 来解决,shiroFilter 是咱们第二步在 Spring 容器中配置的一个实例。

  1. 配置 Spring 容器

在 Spring 容器中至多有两个 Bean 须要咱们配置,一个就是第一步中的 shiroFilter,还有一个就是 SecurityManager,残缺配置如下:

<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
</bean>
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"></property>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authc
        </value>
    </property>
</bean>

这是一个非常简单的配置,咱们在当前的文章中还会持续欠缺它,对于这个配置我说如下几点:

  1. 首先咱们须要配置一个 securityManager,到时候咱们的 realm 要配置在这里。
  2. 还要配置一个名为 shiroFilter 的 bean,这个名字要和 web.xml 中代理过滤器的名字统一。
  3. shiroFilter 中,loginUrl 示意登录页面地址。
  4. successUrl 示意登录胜利地址。
  5. unauthorizedUrl 示意受权失败地址。
  6. filterChainDefinitions 中配置的 /**=authc 示意所有的页面都须要认证 (登录) 之后能力拜访。
  7. authc 实际上是一个过滤器,这个咱们在后文还会再具体说到。
  8. 匹配符遵循 Ant 格调门路表达式, 这里能够配置多个,匹配程序从上往下匹配到了就不再匹配了。比方上面这个写法:
/a/b/*=anon
/a/**=authc

假如我的门路是 /a/b/ c 那么就会匹配到第一个过滤器 anon,而不会匹配到 authc,所以这里的程序很重要。

OK,这些配置写完后,在 webpap 目录下创立对应的 jsp 文件,如下:

此时,启动我的项目去浏览器中拜访,无论咱们拜访什么地址,最初都会回到 login.jsp 页面,因为所有的页面(即便不存在的地址)都须要认证后才能够拜访。

本大节案例:https://github.com/lenve/shir…

10. Shiro 解决登录的三种形式

10.1 筹备工作

很显著,不论是那种登录,都离不开数据库,这里数据库我采纳咱们后面的数据库,这里不做赘述(文末能够下载数据库脚本),然而我这里须要首先配置 JdbcRealm,在 applicationContext.xml 中首先配置数据源,如下:

<context:property-placeholder location="classpath:db.properties"/>
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
    <property name="username" value="${db.username}"/>
    <property name="password" value="${db.password}"/>
    <property name="url" value="${db.url}"/>
</bean>

有了数据源之后,接下来配置 JdbcRealm,如下:

<bean class="org.apache.shiro.realm.jdbc.JdbcRealm" id="jdbcRealm">
    <property name="dataSource" ref="dataSource"/>
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="sha-512"/>
            <property name="hashIterations" value="1024"/>
        </bean>
    </property>
    <property name="saltStyle" value="COLUMN"/>
    <property name="authenticationQuery" value="select password, username from users where username = ?"/>
</bean>

JdbcRealm 中这几个属性和咱们本系列第七篇文章根本是统一的,首先咱们配置了明码比对器为 HashedCredentialsMatcher,相应的算法为 sha512,明码加密迭代次数为 1024 次,而后咱们配置了明码的盐从数据表的列中来,username 列就是咱们的盐,这些配置和前文都是统一的,不分明的小伙伴能够参考咱们本系列第七篇文章。

10.2 自定义登录逻辑

自定义登录逻辑比较简单,首先咱们把 login.jsp 页面进行简略革新:

<form action="/login" method="post">
    <table>
        <tr>
            <td> 用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td> 明码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2"><input type="submit" value="登录"></td>
        </tr>
    </table>
</form>

而后创立咱们的登录解决 Controller,如下:

@PostMapping("/login")
public String login(String username, String password) {Subject currentUser = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {currentUser.login(token);
        return "success";
    } catch (AuthenticationException e) { }
    return "login";
}

登录胜利咱们就去 success 页面,登录失败就回到登录页面。做完这两步之后,咱们还要批改 shiroFilter 中的 filterChainDefinitions 属性,要设置 /login 接口能够匿名拜访,如下:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"></property>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /login=anon
            /**=authc
        </value>
    </property>
</bean>

做完这些之后,就能够去 login.jsp 页面测试登录了。

下面中形式是咱们本人写登录逻辑,shiro 也给咱们提供了两种不必本人写登录逻辑的登录形式,请持续往下看。

10.3 基于 HTTP 的认证

shiro 中也提供了基于 http 协定的认证,当然,这种认证也得有数据库的辅助,数据配置和前文一样,咱们只须要批改一个配置即可,如下:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authcBasic
        </value>
    </property>
</bean>

这个示意所有的页面都要通过基于 http 的认证。此时咱们关上任意一个页面,认证形式如下:

10.4 表单登录

表单登录和基于 HTTP 的登录相似,都是不须要咱们本人写登录逻辑的登录,然而出错的逻辑还是要略微解决下,首先批改 shiroFilter:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="successUrl" value="/success.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authc
        </value>
    </property>
</bean>

配置登录页面,也配置登录胜利后的跳转页面,同时设置所有页面都要登录后能力拜访。

配置登录页面申请,如下:

@RequestMapping("/login")
public String login(HttpServletRequest req, Model model) {String shiroLoginFailure = (String) req.getAttribute("shiroLoginFailure");
    if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {model.addAttribute("error", "账户不存在!");
    }
    if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {model.addAttribute("error", "明码不正确!");
    }
    return "login";
}

如果登录失败,那么在 request 中会有一个 shiroLoginFailure 的属性中保留了登录失败的异样类名,通过判断这个类名,咱们就能够晓得是什么起因导致了登录失败。

OK,配置好这两步之后,就能够去登录页面测试了。

10.5 登记登录

登记登录比较简单,就一个过滤器,按如下形式配置:

<property name="filterChainDefinitions">
    <value>
        /logout=logout
        /**=authc
    </value>
</property>

通过 get 申请拜访 /logout 即可登记登录。

本大节有三个案例,下载地址如下:

  • https://github.com/lenve/shir…
  • https://github.com/lenve/shir…
  • https://github.com/lenve/shir…

11. Shiro 中的受权问题

11.1 配置角色

本文的案例在上文的根底上实现,因而 Realm 这一块我仍然采纳 JdbcRealm,相干的受权就不用配置了。然而这里的数据库脚本有更新,小伙伴须要下载从新执行(https://github.com/lenve/shir…

先来介绍下目前数据库中用户的状况,数据库中有两个用户,sang 具备 admin 的角色,同时具备 book:*author:create两个权限,lisi 具备 user 的角色,同时具备 user:infouser:delete两个权限。批改 shiroFilter,如下:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /admin.jsp=authc,roles[admin]
            /user.jsp=authc,roles[user]
            /logout=logout
            /**=authc
        </value>
    </property>
</bean>

对于这里的配置,我说如下几点:

  1. unauthorizedUrl 示意受权失败时展现的页面
  2. filterChainDefinitions 中咱们配置了 admin.jsp 页面必须登录后能力拜访,同时登录的用户必须具备 admin 角色,user.jsp 也是必须登录后能力拜访,同时登录的用户必须具备 user 角色

11.2 测试

测试时咱们别离用 sang/123 和 lisi/123 进行登录,登录胜利后别离拜访 user.jsp 和 admin.jsp 就能看到成果。

11.3 配置权限

下面的形式是配置角色,然而还没有配置权限,要配置权限,首先要在 jdbcRealm 中增加容许权限信息的查问:

<property name="permissionsLookupEnabled" value="true"/>

而后配置下 shiroFilter:

<property name="filterChainDefinitions">
    <value>
        /admin.jsp=authc,roles[admin]
        /user.jsp=authc,roles[user]
        /userinfo.jsp=authc,perms[user:info]
        /bookinfo.jsp=authc,perms[book:info]
        /logout=logout
        /**=authc
    </value>
</property>

这里假如拜访 userinfo.jsp 须要 user:info 权限,拜访 bookinfo.jsp 须要 book:info 权限。

OK,做完这些之后就能够测试了,别离用 sang/123 和 lisi/123 进行登录,登录胜利后别离拜访 bookinfo.jsp 和 userinfo.jsp 就能够看到不同成果了。

本大节案例下载:https://github.com/lenve/shir…

12. Shiro 中的 JSP 标签

12.1 缘起

上篇文章中,咱们在 success.jsp 中写了很多像上面这种超链接:

<h1> 登录胜利!</h1>
<h3><a href="/logout"> 登记 </a></h3>
<h3><a href="/admin.jsp">admin.jsp</a></h3>
<h3><a href="/user.jsp">user.jsp</a></h3>
<h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
<h3><a href="/userinfo.jsp">userinfo.jsp</a></h3>

然而对于不同身份的用户,并不是每一个链接都是无效的,点击有效的链接会进入到未受权的页面,这样用户体验并不好,最好可能把不可达的链接自动隐藏起来,同时,我也心愿可能不便获取以后登录用户的信息等,思考到这些需要,咱们来聊聊 shiro 中的 jsp 标签。

12.2 标签介绍

shiro 中的标签并不多,次要有如下几种:

  1. shiro:guest

shiro:guest 标签只有在以后未登录时显示里边的内容,如下:

<shiro:guest>
    欢送【游客】拜访!
</shiro:guest>
  1. shiro:user

shiro:user 是在用户登录之后显示该标签中的内容,无论是通过失常的登录还是通过 Remember Me 登录,如下:

<shiro:user>
    欢送【<shiro:principal/>】拜访!
</shiro:user>
  1. shiro:principal

shiro:principal 用来获取以后登录用户的信息,显示成果如下:

4.shiro:authenticated

和 shiro:user 相比,shiro:authenticated 的范畴变小,当用户认证胜利且不是通过 Remember Me 认证胜利,这个标签中的内容才会显示进去:

<shiro:authenticated>
    用户【<shiro:principal/>】身份认证通过,不是通过 Remember Me 认证!
</shiro:authenticated>
  1. shiro:notAuthenticated

shiro:notAuthenticated 也是在用户未认证的状况下显示内容,和 shiro:guest 不同的是,对于通过 Remember Me 形式进行的认证,shiro:guest 不会显示内容,而 shiro:notAuthenticated 会显示内容(因为此时并不是游客,然而又的确未认证),如下:

<shiro:notAuthenticated>
    用户未进行身份认证
</shiro:notAuthenticated>
  1. shiro:lacksRole

当用户不具备某个角色时候,显示内容,如下:

<shiro:lacksRole name="admin">
    用户不具备 admin 角色
</shiro:lacksRole>
  1. shiro:lacksPermission

当用户不具备某个权限时显示内容:

<shiro:lacksPermission name="book:info">
    用户不具备 book:info 权限
</shiro:lacksPermission>
  1. shiro:hasRole

当用户具备某个角色时显示的内容:

<shiro:hasRole name="admin">
    <h3><a href="/admin.jsp">admin.jsp</a></h3>
</shiro:hasRole>
  1. shiro:hasAnyRoles

当用户具备多个角色中的某一个时显示的内容:

<shiro:hasAnyRoles name="user,aaa">
    <h3><a href="/user.jsp">user.jsp</a></h3>
</shiro:hasAnyRoles>
  1. shiro:hasPermission

当用户具备某一个权限时显示的内容:

<shiro:hasPermission name="book:info">
    <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
</shiro:hasPermission>

本大节案例下载:https://github.com/lenve/shir…

13.Shiro 中的缓存机制

13.1 增加依赖

应用缓存,首先须要增加相干依赖,如下:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

13.2 增加配置文件

ehcache 的配置文件次要参考官网的配置,在 resources 目录下创立 ehcache.xml 文件,内容如下:

<ehcache>
    <diskStore path="java.io.tmpdir/shiro-spring-sample"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />
    <cache name="shiro-activeSessionCache"
           maxElementsInMemory="10000"
           eternal="true"
           overflowToDisk="true"
           diskPersistent="true"
           diskExpiryThreadIntervalSeconds="600"/>
    <cache name="org.apache.shiro.realm.SimpleAccountRealm.authorization"
           maxElementsInMemory="100"
           eternal="false"
           timeToLiveSeconds="600"
           overflowToDisk="false"/>
</ehcache>

这些都是 ehcache 缓存中惯例的配置,含意我就不一一解释了,文末下载源码有正文。

13.3 缓存配置

接下来咱们只须要在 applicationContext 中简略配置下缓存即可,配置形式如下:

<bean class="org.apache.shiro.cache.ehcache.EhCacheManager" id="cacheManager">
    <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
    <property name="realm" ref="jdbcRealm"/>
    <property name="cacheManager" ref="cacheManager"/>
</bean>

首先配置 EhCacheManager 类,指定缓存地位,而后在 DefaultWebSecurityManager 中引入 cacheManager 即可,如此之后,咱们的缓存就利用上了。

13.4 测试

因为我这里应用了 JdbcRealm,如果应用了自定义 Realm 那么能够通过打日志看是否应用了缓存,应用了 JdbcRealm 之后,咱们能够通过打断点来查看是否利用了缓存,比方我执行如下代码:

subject.checkRole("admin");
subject.checkPermission("book:info");

通过断点跟踪,发现最终会来到 AuthorizingRealm 的 getAuthorizationInfo 办法中,在该办法中,首先会去缓存中检查数据,如果缓存中有数据,则不会执行 doGetAuthorizationInfo 办法(数据库操作就在 doGetAuthorizationInfo 办法中进行),如果缓存中没有数据,则会执行 doGetAuthorizationInfo 办法,并且在执行胜利后将数据保留到缓存中(前提是配置了缓存,cache 不为 null),此时咱们通过断点,发现执行了缓存而没有查询数据库中的数据,局部源码如下:

protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
    AuthorizationInfo info = null;
    Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
    if (cache != null) {Object key = getAuthorizationCacheKey(principals);
        info = cache.get(key);
    }
    if (info == null) {info = doGetAuthorizationInfo(principals);
        if (info != null && cache != null) {Object key = getAuthorizationCacheKey(principals);
            cache.put(key, info);
        }
    }
    return info;
}

OK,整体来说 shiro 中的缓存配置还是非常简单的。

That’s all.

本大节案例下载地址:https://github.com/lenve/shir…

待续。。。

正文完
 0