文章目录
1、前言
1.1 简介
Apache Shiro 是 Java 的一个安全(权限)框架。可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在 JavaEE 环境。可以完成:认证、授权、加密、会话管理、与Web 集成、缓存等功能。
官方网站:http://shiro.apache.org/
1.2 功能
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web 支持,可以非常容易的集成到Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
1.3 架构
1.3.1 外部视角
从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作:
Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外API 核心就是 Subject。Subject 代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;与 Subject 的所有交互都会委托给 SecurityManager;Subject 其实是一个门面,SecurityManager 才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中DispatcherServlet 的角色;
Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource。
1.3.2 内部视角
Subject:任何可以与应用交互的“用户”;
SecurityManager :相当于SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证、授权、会话及缓存的管理。
Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的 Realm;
SessionManager:管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web环境,也可以用在如普通的 JavaSE 环境;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密。
2、HelloWorld
2.1 导入依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-all -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
2.2 配置文件
shiro.ini:
# -----------------------------------------------------------
# 用户及其分配的角色
# -----------------------------------------------------------
[users]
# 用户名“root”,密码为“secret”,角色为“admin”
root = secret, admin
# 用户名“guest”,密码为“guest”,角色为“guest”
guest = guest, guest
# 用户名“presidentskroob”,密码为“12345”,角色为“president”
presidentskroob = 12345, president
# 用户名“darkhelmet”,密码为“ludicrousspeed”,角色为“darklord”和”schwartz“
darkhelmet = ludicrousspeed, darklord, schwartz
# 用户名“lonestarr”,密码为“vespa”,角色为“goodguy”和”schwartz“
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------
# 具有指定权限的角色
# -----------------------------------------------------------
[roles]
# “admin”角色拥有所有权限,由通配符“*”表示
admin = *
# “schwartz”角色可以用任何lightsaber做任何事情(*):
schwartz = lightsaber:*
# “goodguy”角色允许“delete”(操作)用户(输入)
# 车牌“张三”(实例指定id)
goodguy = user:delete:zhangsan
2.3 测试代码
package pers.klb.shiro.quickstart;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple Quickstart application showing how to use Shiro's API.
*
* @since 0.9 RC2
*/
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// 创建具有已配置领域、用户、角色和权限的Shiro
// SecurityManager的最简单方法是使用简单的INI配置。
// 我们将使用一个工厂,它可以读取一个.ini文件并返回一个SecurityManager实例:
// 使用类路径根目录下的 shiro.ini 文件
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
// 对于这个简单的示例quickstart,可以将SecurityManager作为一个JVM单例进行访问。
// 大多数应用程序不会这样做,而是依赖于它们的容器配置或web.xml。
// 这超出了这个简单的快速入门的范围,所以我们只做最基本的,这样你就可以继续对事物有一个感觉。
SecurityUtils.setSecurityManager(securityManager);
// 现在,简单的Shiro环境已经设置好了,让我们看看您可以做些什么:
// 获取当前执行的用户(也就是subject):
// 调用 SecurityUtils.getSubject()获取当前的 Subject;
Subject currentUser = SecurityUtils.getSubject();
// 使用Session做一些事情(不需要web或EJB容器!!)
// 测试使用 Session
// Subject对象的getSession()方法获取 Session:
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("---> 获取到了正确的值! [" + value + "]");
}
// 让我们登录当前用户,这样我们就可以检查角色和权限:
// 测试当前的用户是否已经被认证. 即是否已经登录.
// 调动 Subject 的 isAuthenticated()
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
// rememberme
token.setRememberMe(true);
try {
// 执行登录.
currentUser.login(token);
}
// 若没有指定的账户, 则 shiro 将会抛出 UnknownAccountException 异常.
catch (UnknownAccountException uae) {
log.info("----> 没有用户名为 " + token.getPrincipal() + " 的用户 ");
return;
}
// 若账户存在, 但密码不匹配, 则 shiro 会抛出 IncorrectCredentialsException 异常。
catch (IncorrectCredentialsException ice) {
log.info("----> 用户为 " + token.getPrincipal() + " 的密码不正确!");
return;
}
// 用户被锁定的异常 LockedAccountException
catch (LockedAccountException lae) {
log.info("用户为 " + token.getPrincipal() + " 的账户已锁定. " + "请与管理员联系解锁.");
}
// ... 在这里捕获更多的异常(可能是特定于您的应用程序的自定义异常?
// 所有认证时异常的父类.
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//say who they are:
//打印它们的标识主体(在本例中为用户名):
log.info("----> 用户 [" + currentUser.getPrincipal() + "] 登陆成功!");
//test a role:
// 调用 Subject 的 hasRole 方法测试是否有某一个角色.
if (currentUser.hasRole("schwartz")) {
log.info("----> May the Schwartz be with you!");
} else {
log.info("----> Hello, mere mortal.");
return;
}
//test a typed permission (not instance-level)
// 调用 Subject 的 isPermitted() 方法测试用户是否具备某一个行为
if (currentUser.isPermitted("lightsaber:weild")) {
log.info("----> You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
//a (very powerful) Instance Level permission:
// 测试用户是否具备某一个行为.
if (currentUser.isPermitted("user:delete:zhangsan")) {
log.info("----> You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
//all done - log out!
// 执行登出. 调用 Subject 的 Logout() 方法.
System.out.println("---->" + currentUser.isAuthenticated());
currentUser.logout();
System.out.println("---->" + currentUser.isAuthenticated());
System.exit(0);
}
}
3、Shiro与web的集成
3.1 集成原理
Shiro 提供了与 Web 集成的支持,其通过一个ShiroFilter 入口来拦截需要安全控制的URL,然后进行相应的控制。
JavaWeb项目的web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<!-- 加载 spring 的配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 配置监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- springMVC 的前端控制器 -->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- Shiro Filter的实现类以bean的形式定义在spring配置文件中 -->
<!--
1. 配置 Shiro 的 shiroFilter.
2. DelegatingFilterProxy 实际上是 Filter 的一个代理对象. 默认情况下, Spring 会到 IOC 容器中查找和
<filter-name> 对应的 filter bean. 也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id.
-->
<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>
</web-app>
ShiroFilter 类似于如 Strut2/SpringMVC 这种web 框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini 配置文件),然后判断URL是否需要登录/权限等工作。
3.2 Shiro的工作原理
3.3 ShiroFilter
DelegatingFilterProxy
作用是自动到 Spring 容器查找名字为 shiroFilter
(filter-name)的 bean 并把所有 Filter的操作委托给它。
在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>
3.4 spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- =========================================================
Shiro 核心组件 - 不是 spring 必须的
========================================================= -->
<!-- Shiro的主要业务层对象,用于支持web的应用程序
(当没有web环境时,使用DefaultSecurityManager)-->
<!--
1. 配置 SecurityManager!
-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"></property>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
<property name="rememberMeManager.cookie.maxAge" value="10"></property>
</bean>
<!-- 让我们使用一些企业缓存支持来提高性能。你可以用任何你喜欢的企业缓存框架实现来代替它(Terracotta+Ehcache, Coherence, GigaSpaces,等等) -->
<!--
2. 配置 CacheManager.
2.1 需要加入 ehcache 的 jar 包及配置文件.
-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<!-- Set a net.sf.ehcache.CacheManager instance here if you already have one. If not, a new one
will be creaed with a default config:
<property name="cacheManager" ref="ehCacheManager"/> -->
<!-- If you don't have a pre-built net.sf.ehcache.CacheManager instance to inject, but you want
a specific Ehcache configuration to be used, specify that here. If you don't, a default
will be used.: -->
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<!--配置authenticator,策略为AtLeastOneSuccessfulStrategy-->
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>
<!-- SecurityManager用于访问安全数据(用户、角色等)。还可以使用许多其他域实现(PropertiesRealm, LdapRealm, etc) -->
<!--
3. 配置 Realm
3.1 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean
-->
<bean id="jdbcRealm" class="pers.klb.shiro.shirospring.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
<!-- 4. 配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法.
-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!--
5. 启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用.
-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!--
6. 配置 ShiroFilter.
6.1 id 必须和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
若不一致, 则会抛出: NoSuchBeanDefinitionException. 因为 Shiro 会来 IOC 容器中查找和 <filter-name> 名字对应的 filter bean.
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
<!--
配置哪些页面需要受保护.
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
3). logout 登出.
4). roles 角色过滤器
-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
<!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
<bean id="filterChainDefinitionMapBuilder"
class="pers.klb.shiro.shirospring.factory.FilterChainDefinitionMapBuilder"></bean>
<bean id="shiroService"
class="pers.klb.shiro.shirospring.services.ShiroService"></bean>
</beans>
部分细节: [urls] 部分的配置,其格式是: “url=拦截器[参数],拦截器[参数]
”; 如果当前请求的 url 匹配 [urls] 部分的某个 url 模式,将会执行其配置的拦截器。
anon(anonymous) 拦截器表示匿名访问(即不需要登录即可访问)
authc (authentication)拦截器表示需要身份认证通过后才能访问。
3.5 shiro中默认的过滤器
3.6 URL 匹配模式
url 模式使用 Ant 风格模式,Ant 路径通配符支持 ?
、*
、**
,注意通配符匹配不包括目录分隔符“/
”:
?
:匹配一个字符,如 /admin?
将匹配 /admin1
,但不匹配 /admin
或 /admin/
;
*
:匹配零个或多个字符串,如 /admin*
将匹配 /admin
、/admin123
,但不匹配 /admin/1
;
**
:匹配路径中的零个或多个路径,如 /admin/**
将匹配 /admin/a
或 /admin/a/b
。
3.7 URL 匹配顺序
URL 权限采取第一次匹配优先的 方式,即从头开始使用第一个匹配的 url 模式对应的拦截器链。
如:
– /bb/**=filter1
– /bb/aa=filter2
– /**=filter3
如果请求的url是“/bb/aa
”,因为按照声明顺序进行匹配,那么将使用 filter1 进行拦截。
4、认证功能
4.1 介绍
从外部视角看Shiro的架构:
认证指的的身份验证,一般需要提供如身份 ID 等一些标识信息来表明登录者的身份,如提供 email,用户名/密码来证明。
在 shiro 中,用户需要提供 principals (身份)和credentials(证明)给 shiro,从而应用能验证用户身份:
principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个
Primary principals,一般是用户名/邮箱/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的 principals 和 credentials 组合就是用户名/密码了。
4.2 基本用法
4.2.1 流程
1、收集用户身份/凭证,即如用户名/密码;
2、调用 Subject.login
进行登录,如果失败将得到相应的 AuthenticationException
异常,根据异常提示用户错误信息;否则登录成功;
3、创建自定义的 Realm 类,继承org.apache.shiro.realm.AuthorizingRealm
类,实现doGetAuthenticationInfo()
方法。
4.2.2 用法示例
收集身份信息并调用Subject.login
进行登录:
@RequestMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password){
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// rememberme
token.setRememberMe(true);
try {
System.out.println("1. " + token.hashCode());
// 执行登录.
currentUser.login(token);
}
// ... catch more exceptions here (maybe custom ones specific to your application?
// 所有认证时异常的父类.
catch (AuthenticationException ae) {
//unexpected condition? error?
System.out.println("登录失败: " + ae.getMessage());
}
}
return "redirect:/list.jsp";
}
继承org.apache.shiro.realm.AuthorizingRealm
类,实现doGetAuthenticationInfo()
方法:
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// 根据 token 获取待验证的身份信息
// 返回 AuthenticationInfo
}
spring配置文件中配置realm:
<bean id="jdbcRealm" class="pers.klb.shiro.shirospring.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
调用Subject.login
方法时,会进入配置的realm中,执行认证逻辑。
4.2.3 AuthenticationException
如果身份验证失败请捕获 AuthenticationException 或其子类,最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;
常见的子类如下:
4.4 Shiro内部认证逻辑
4.4.1 流程
完整流程图为:
1、首先调用 Subject.login(token)
进行登录,其会自动委托给SecurityManager
;
2、SecurityManager
负责真正的身份验证逻辑;它会委托给Authenticator
进行身份验证;
3、Authenticator
才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
4、Authenticator
可能会委托给相应的 AuthenticationStrategy
进行多 Realm
身份验证,默认 ModularRealmAuthenticator
会调用AuthenticationStrategy
进行多 Realm
身份验证;
5、Authenticator
会把相应的 token
(携带代验证的身份信息) 传入 Realm
,从 Realm
获取身份验证信息,如果没有返回或者抛出异常表示身份验证失败了。此处可以配置多个Realm
,将按照相应的顺序及策略进行访问。
4.4.2 Authenticator
Authenticator
的职责是验证用户帐号,是 Shiro API 中身份验
证核心的入口点:如果验证成功,将返回AuthenticationInfo
验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的 AuthenticationException
异常。
SecurityManager
接口继承了 Authenticator
,另外还有一个ModularRealmAuthenticator
实现,其委托给多个Realm
进行验证,验证规则通过 AuthenticationStrategy
接口指定。
4.4.3 AuthenticationStrategy
AuthenticationStrategy
接口的默认实现:
1、FirstSuccessfulStrategy
:只要有一个 Realm
验证成功即可,只返回第一个 Realm
身份验证成功的认证信息,其他的忽略;
2、AtLeastOneSuccessfulStrategy
:只要有一个Realm
验证成功即可,和FirstSuccessfulStrategy
不同,将返回所有Realm
身份验证成功的认证信息;
3、AllSuccessfulStrategy
:所有Realm
验证成功才算成功,且返回所有Realm
身份验证成功的认证信息,如果有一个失败就失败了。
ModularRealmAuthenticator
默认是AtLeastOneSuccessfulStrategy
策略
4.4.4 Realm
org.apache.shiro.realm.Realm
是Shiro的一个接口,Shiro 从 Realm
获取安全数据(如用户、角色、权限),即 SecurityManager
要验证用户身份,那么它需要从 Realm
获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm
得到用户相应的角色/权限进行验证用户是否能进行操作
该接口源码如下:
package org.apache.shiro.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
public interface Realm {
// 返回 Realm 唯一的名字
String getName();
// 判断此 Realm 是否支持此 Token
boolean supports(AuthenticationToken var1);
// 根据 Token 获取待认证的信息
AuthenticationInfo getAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;
}
它的实现类如下:
其中,org.apache.shiro.realm.AuthenticatingRealm
继承了AuthenticatingRealm
(即身份验证),而且也间接继承了CachingRealm
(带有缓存实现)。我们在自定义Realm
时继承它即可,无需实现原始Realm
。
4.5 源码解析(选看)
4.5.1 自定义Realm
自定义的Realm:
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("[FirstRealm] doGetAuthenticationInfo");
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息.");
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if("unknown".equals(username)){
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常.
if("monster".equals(username)){
throw new LockedAccountException("用户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//以下信息是从数据库中获取的.
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
Object principal = username;
//2). credentials: 密码.
Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4";
if("admin".equals(username)){
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
}else if("user".equals(username)){
credentials = "098d2c478e9c11555ce2823231e02ec1";
}
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值.
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName);
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}
spring配置文件:
<bean id="securityManager"
class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"></property>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
</bean>
<bean id="cacheManager"
class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>
<bean id="jdbcRealm"
class="pers.klb.shiro.shirospring.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
其中,HashedCredentialsMatcher
对象可以设置两个属性:hashAlgorithmName
和hashIterations
,分别表示密码加密的算法和迭代次数。
4.5.2 开始解析
根据上面小结的介绍流程,来看Shiro具体是如何走这个流程的,我们全程跟踪token对象,该对象保存了待验证的身份信息。
就以Controller为入口打个断点:
前端过来username和password后,进入Controller,首先调用 Subject.login(token)
进行登录,触发断点:
我们传入的token对象,里面保存了待验证的用户身份信息,进入login
方法:
可以看到,会通过WebDelegatingSubject
自动委托给DefaultWebSecurityManager
,这个SecurityManager
负责真正的身份验证逻辑,继续看securityManager
的login
方法:
SecurityManager
会委托给Authenticator
进行身份验证,并返回一个AuthenticationInfo
对象,进入这个authenticate
方法:
继续点进去:
继续点进去:
Authenticator
先从容器中获取所有的Realm
,可以看到当前只有一个Realm
,就是我们自定义的ShiroRealm
。
然后,Authenticator
会把相应的 token
传入 Realm
,也就是doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken)
。
进入这个doSingleRealmAuthentication
:
调用了realm.getAuthenticationInfo(token)
,这个realm
就是我们自定义的ShiroRealm
,token
携带着待认证的用户信息,点进去:
这里调用了this.doGetAuthenticationInfo
,这个方法就是我们所自定义的,继续在这个函数里面走几步:
这个principal
变量是从token
对象获取的,而这个credentials
在实际开发中应该从数据库获取,这一串长字符串其实是“123456”的MD5值。
继续往下走:
我们自定义的Realm
创建了SimpleAuthenticationInfo
对象,传入构造器的参数有principal
、credentials
、credentialsSalt
和realmName
,这四个对象的取值分别为:
principal = "admin"
;
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e"
;
credentialsSalt = "YWRtaW4="
;
realmName = "pers.klb.shiro.shirospring.realms.ShiroRealm_0"
。
至此,我们自定义的Realm已经工作完毕,就是给框架返回一个SimpleAuthenticationInfo
对象:
那么框架拿到这个对象接着做了什么事情呢?我们继续往下走:
调用了this.cacheAuthenticationInfoIfPossible(token, info);
方法,token
就是一开始我们的追踪对象,现在多了一个info
,这个info
就是我们自定义的Realm
返回的,里面包含了从数据库提取出来的用户密码等信息,进入这个方法看看:
纯粹输出一些日志信息,跳出这个方法:
这个方法貌似是对比账号密码,我们点进去看看:
这里框架创建了一个CredentialsMatcher
,看名字叫做凭证匹配器,输入的正是token
和info
,一个是待认证的信息,一个是数据库的信息。我们点进去这个方法:
因为用户传进来的“123456”,而我们开启了MD5加密模式,因此,这里首先把“123456”经过MD5加密,然后把机密后的字符串跟info里面从数据库提取出来的密码比较是否一致。(分析到这一步其实已经很明确整个过程了)
继续往下走:
凭证匹配器无论是否匹配成功,都返回info给调用者,我们继续往下看:
可以看到,如果我们没有自定义Realm来处理token,也就不会有info,而这里我们是有info的,它还是把info往上返回给上一级调用者:
继续回调:
这些全是开始走过的,现在只是拿到info后不断回调而已:
又调用了一个方法this.notifySuccess(token, info)
,这个this指的是ModularRealmAuthenticator
,点进去看看:
var3的size为0,继续往下走,继续回调:
根据token
、info
和subject
创建一个Subject
对象,
createSubject
是根据token
、info
和subject
创建一个新的Subject
,并设置authenticated
属性为true。
接着调用onSuccessfulLogin
方法:
继续点进去rememberMeSuccessfulLogin
方法:
这个rmm.onSuccessfulLogin(subject, token, info)
里面的subject是上面新创建的loggedIn
,返回回去的loggendIn
为:
往下走,看loggendIn
返回给了谁:
往下走就是对this
的赋值,而this
就是和loggendIn
相同类型的WebDelegatingSubject
。
至此,整个login
逻辑执行完毕。如果subject.login(token)
执行过程没有任何异常抛出,就验证通过了。
4.5.3 小结
以token为追踪对象,我们经历了SecurityManager
、Authenticator
、Realm
,完全符合官方提供的流程图:
5、授权功能
5.1 相关概念
授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
5.1.1 基本概念
主体(Subject):访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。
资源(Resource):在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许。Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的)
角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。
5.1.2 Permissions
语法:资源标识符:操作:对象实例ID
,即对哪个资源的哪个实例可以进行什么操作。
其默认支持通配符权限字符串:
1、:
表示资源/操作/实例的分割;
2、,
表示操作的分割;
3、*
表示任意资源/操作/实例
多层次管理:
例如:user:query
、user:edit
冒号 是一个特殊字符,它用来分隔权限字符串的下一 部件:第一部分是权限被操作的领域(打印机),第二部分是被执行的操作。
多个值: 每个 部件能够保护多个值。因此,除了授予用户 user:query
和 user:edit
权限外,也可以简单地授予他们一个:user:query,edit
还可以用 *
代替所有的值,如:user:*
,也可以写:*:query
表示某个用户在所有的领域都有 query 的权限。
5.1.3 Shiro 的 Permissions
实例级访问控制:
这种情况通常会使用三个部件: 域、操作、被付诸实施的实例。如:user:edit:manager
;
也 可以使用通配符来定义,如:user:edit:*
、user:*:*
、user:*:manager
;
部分 省略 通配符:缺少的部件意味着用户可以访问所有与之匹配的值,比如:user:edit
等价于 user:edit :*
、user
等价于 user:*:*
;
注意: 通配符只能 从字符串的结尾处省略部件,也就是说 user:edit
并不等价于 user:*:edit
。
5.2 授权方式
5.2.1 编程式
通过写if/else 授权代码块完成。
继承 AuthorizingRealm
类, 并实现其 doGetAuthorizationInfo
方法:
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
Object principal = principals.getPrimaryPrincipal();
//2. 利用登录的用户的信息来用户当前用户的角色或权限(可能需要查询数据库)
Set<String> roles = new HashSet<>();
roles.add("user");
if("admin".equals(principal)){
roles.add("admin");
}
//3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//4. 返回 SimpleAuthorizationInfo 对象.
return info;
}
}
5.2.2 注解式
通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相应的异常。
public class ShiroService {
@RequiresRoles({"admin"})
public void testMethod(){
System.out.println("testMethod, time: " + new Date());
Session session = SecurityUtils.getSubject().getSession();
Object val = session.getAttribute("key");
System.out.println("Service SessionVal: " + val);
}
}
5.2.3 JSP/GSP 标签
在JSP/GSP 页面通过相应的标签完成。
比如,在JSP中,导入JSP库:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
就可以在页面上使用了:
<shiro:hasRole name="admin">
<br><br>
<a href="admin.jsp">Admin Page</a>
</shiro:hasRole>
<shiro:hasRole name="user">
<br><br>
<a href="user.jsp">User Page</a>
</shiro:hasRole>
5.2 流程图
5.3 授权的内部流程
和前面的认证流程相似。
1、首先调用 Subject.hasRole
或者Subject.isPermitted
接口,其会委托给SecurityManager
,而 SecurityManager
接着会委托给 Authorizer
;
2、Authorizer
是真正的授权者,如果调用如isPermitted(“user:view”)
,其首先会通过PermissionResolver
把字符串转换成相应的 Permission
实例;
3、在进行授权之前,其会调用相应的 Realm
获取 Subject
相应的角色/权限用于匹配传入的角色/权限;
4、Authorizer
会判断 Realm
的角色/权限是否和传入的匹配,如果有多个Realm
,会委托给 ModularRealmAuthorizer
进行循环判断,如果匹配如 isPermitted
或者hasRole
会返回true,否则返回false表示授权失败。
5.4 权限注解
5.4.1 @RequiresAuthentication
表示当前Subject
已经通过login
进行了身份验证;即 Subject. isAuthenticated()
返回 true。
5.4.2 @RequiresUser
表示当前 Subject
已经身份验证或者通过记住我登录的。
5.4.3 @RequiresGuest
表示当前Subject
没有身份验证或通过记住我登录过,即是游客身份。
5.4.4 @RequiresRoles(value={“admin”, “user”}, logical=Logical.AND)
表示当前 Subject
需要角色 admin
和user
;
5.4.5 @RequiresPermissions (value={“user:a”, “user:b”},logical= Logical.OR)
表示当前 Subject 需要权限 user:a
或user:b
。
5.5 默认拦截器
Shiro 内置了很多默认的拦截器,比如身份验证、授权等
相关的。默认拦截器可以参考org.apache.shiro.web.filter.mgt.DefaultFilter
中的枚举拦截器:
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
// ...
}
5.5.1 身份验证相关
5.5.2 授权相关
5.5.3 会话相关
5.6 授权开发
5.6.1 定义一个权限过滤规则的类
package pers.klb.shiro.shirospring.factory;
import java.util.LinkedHashMap;
public class FilterChainDefinitionMapBuilder {
public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/login.jsp", "anon");
map.put("/shiro/login", "anon");
map.put("/shiro/logout", "logout");
map.put("/user.jsp", "authc,roles[user]");
map.put("/admin.jsp", "authc,roles[admin]");
map.put("/list.jsp", "user");
map.put("/**", "authc");
return map;
}
}
5.6.2 配置文件
<!--ShiroFilter-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<!--配置权限规则-->
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
</bean>
<!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder"
factory-method="buildFilterChainDefinitionMap"></bean>
<bean id="filterChainDefinitionMapBuilder"
class="pers.klb.shiro.shirospring.factory.FilterChainDefinitionMapBuilder"></bean>
6、会话管理
6.1 概述
Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如web容器tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对Web 的透明支持、SSO 单点登录的支持等特性。
6.2 会话相关API
1、Subject.getSession()
:即可获取会话;其等价于Subject.getSession(true)
,即如果当前没有创建 Session
对象会创建一个;Subject.getSession(false)
,如果当前没有创建 Session
则返回null。
2、session.getId()
:获取当前会话的唯一标识;
3、session.getHost()
:获取当前Subject的主机地址;
4、session.getTimeout()
& session.setTimeout(毫秒)
:获取/设置当前Session的过期时间;
5、session.getStartTimestamp()
&session.getLastAccessTime()
:
获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定
期调用 session.touch()
去更新最后访问时间;如果是 Web 应用,每
次进入 ShiroFilter 都会自动调用 session.touch()
来更新最后访问时间。
6、 session.touch()
& session.stop()
::更新会话最后访问时间及销毁会话;当Subject.logout()
时会自动调用 stop
方法来销毁会话。如果在web中,调用 HttpSession. invalidate()
也会自动调用Shiro Session.stop
方法进行销毁Shiro 的会话;
7、session.setAttribute(key, val)
&session.getAttribute(key)
&
session.removeAttribute(key)
:设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作。
6.3 会话监听器
会话监听器用于监听会话创建、过期及停止事件:
package org.apache.shiro.session;
public interface SessionListener {
void onStart(Session session);
void onExpiration(Session session);
}
6.4 SessionDao
AbstractSessionDAO
提供了 SessionDAO
的基础实现,如生成会话ID等。
CachingSessionDAO
提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager
。
MemorySessionDAO
直接在内存中进行会话维护。
EnterpriseCacheSessionDAO
提供了缓存功能的会话维护,默认情况下使用 MapCache
实现,内部使用ConcurrentHashMap
保存缓存的会话。
配置示例:
使用方法:
6.5 SerializableUtils
6.6 会话验证
Shiro 提供了会话验证调度器,用于定期的验证会话是否
已过期,如果过期将停止会话。
出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器SessionValidationScheduler
。
Shiro 也提供了使用Quartz会话验证调度器:QuartzSessionValidationScheduler
7、缓存
7.1 CacheManagerAware
Shiro 内部相应的组件(DefaultSecurityManager
)会自动检测相应的对象(如Realm
)是否实现了CacheManagerAware
并自动注入相应的CacheManager
。
7.2 Realm 缓存
Shiro 提供了 CachingRealm
,其实现了CacheManagerAware
接口,提供了缓存的一些基础实现;
AuthenticatingRealm
及 AuthorizingRealm
也分别提供了对AuthenticationInfo
和 AuthorizationInfo
信息的缓存。
7.3 Session 缓存
如 SecurityManager
实现了 SessionSecurityManager
,其会判断 SessionManager
是否实现了CacheManagerAware
接口,如果实现了会把CacheManager
设置给它。
SessionManager
也会判断相应的 SessionDAO
(如继承自CachingSessionDAO
)是否实现了CacheManagerAware
,如果实现了会把 CacheManager
设置给它。
设置了缓存的 SessionManager
,查询时会先查缓存,如果找不到才查数据库。
8、RememberMe
8.1 概述
Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
1、首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会把 RememberMe 的Cookie 写到客户端并保存下来;
2、关闭浏览器再重新打开;会发现浏览器还是记住你的;
3、访问一般的网页服务器端还是知道你是谁,且能正常访问;
4、但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。
8.2 认证和记住我
subject.isAuthenticated()
表示用户进行了身份验证登录的,即使有 Subject.login
进行了登录;
subject.isRemembered()
:表示用户是通过记住我登录的,此时可能并不是真正的你(如你的朋友使用你的电脑,或者你的cookie 被窃取)在访问的。
两者二选一,即 subject.isAuthenticated()==true
,则subject.isRemembered()==false
;反之一样。
建议:
访问一般网页:如个人在主页之类的,我们使用user 拦截
器即可,user 拦截器只要用户登录(isRemembered() || isAuthenticated())过即可访问成功;
访问特殊网页:如我的订单,提交订单页面,我们使用authc
拦截器即可,authc
拦截器会判断用户是否是通过Subject.login
(isAuthenticated()==true)登录的,如果是才放行,否则会跳转到登录页面叫你重新登录。
8.3 身份验证相关
8.4 自定义RememeberMe
需要在登录之前这样创建Token:UsernamePasswordToken(用户名,密码,是否记住我)
,且调用UsernamePasswordToken
的token.setRememberMe(true)
方法。
配置文件: