Shiro入门

Shiro入门

1、前提

本篇内容算是记录,是在观看了其他一些视频教程的基础上写的,我们每个人都可以是学习者,看到好的东西,跟着学习,然后记录,再进行归纳总结,最终成为自己的一部分。

2、Shiro简介

Apache Shiro是Java的一个安全(权限)框架。Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以完成:认证、授权、加密、会话管理、与Web集成、缓存等。

3、Shiro的功能

Shiro的功能点如下图所示:
Shiro入门
下面对每个模块进行简单的说明:

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用
    户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户
    对某个资源是否具有某个权限。
  • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有
    信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的。
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
  • Web Support:Web 支持,可以非常容易的集成到Web环境。
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可
    以提高效率。
  • Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能
    把权限自动传播过去。
  • Testing:提供测试支持。
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登
    录了。

4、Shiro架构

4.1、从外部看

从外部来看Shiro ,即从应用程序的角度来观察如何使用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。

4.2、从内部看

Shiro入门
以下对上面各部分进行简要说明:

  • 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提高了一些常见的加密组件用于如密码加密/解密。

5、快速开始demo

通过官方为我们提供的简单例子来体验shiro的使用,先建一个普通的项目,如下:
Shiro入门
然后加入必要的jar包:
Shiro入门
上面的jar是shiro核心jar包以及日志相关的jar,日志可以帮助我们更好的监控程序的运行。

然后是配置文件,如下:
Shiro入门
直接放在了classpath路径下。

log4j.properties日志配置具体内容如下:

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=TRACE

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

然后shiro.ini是shiro的配置,里面存放的是用户相关的信息,比较简单,如下:

## 用户相关
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

## 用户角色相关
[roles]
admin = *
schwartz = lightsaber:*
goodguy = user:delete:zhangsan

然后在包下创建一个Quickstart.java文件:
Shiro入门
内容直接从官网上复制下来的,修改后如下:

package com.ycz.demo;

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 {

    // 获取log对象
	private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);

	public static void main(String[] args) {

		// 获取配置文件,进行初始化
		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
		// 获取SecurityManager对象
		SecurityManager securityManager = factory.getInstance();
		// 设置SecurityManager对象
		SecurityUtils.setSecurityManager(securityManager);

		// 获取当前的Subject
		Subject currentUser = SecurityUtils.getSubject();

		// 获取Session对象
		Session session = currentUser.getSession();
		session.setAttribute("someKey", "yanchengzhi");
		String value = (String) session.getAttribute("someKey");
		if (value.equals("yanchengzhi")) {
			log.info("---> 当前值:[" + value + "]");
		}
		// 测试当前的用户是否已经被认证. 即是否已经登录.调用isAuthenticated()方法
		if (!currentUser.isAuthenticated()) {
			// 用户名和密码封装为UsernamePasswordToken对象
			UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
			// 记住我
			token.setRememberMe(true);
			try {
				// 执行登录
				currentUser.login(token);
			} catch (UnknownAccountException uae) {// 没有账号
				log.info("----> 该账号不存在! " + token.getPrincipal());
				return;
			} catch (IncorrectCredentialsException ice) {// 账户存在, 密码不对
				log.info("----> 账户的密码 " + token.getPrincipal() + " 不正确!");
				return;
			} catch (LockedAccountException lae) {// 用户锁定异常LockedAccountException
				log.info("The account for username " + token.getPrincipal() + " is locked.  "
						+ "Please contact your administrator to unlock it.");
			} catch (AuthenticationException ae) {// 所有认证时异常的父类
				// unexpected condition? error?
			}
		}
		log.info("----> 用户 【" + currentUser.getPrincipal() + "】 登录成功!");

		// 测试用户是否拥有某角色
		if (currentUser.hasRole("schwartz")) {
			log.info("----> 拥有该角色!");
		} else {
			log.info("----> 不拥有该角色!");
			return;
		}
		// 测试用户是否拥有某权限
		if (currentUser.isPermitted("lightsaber:weild")) {
			log.info("----> 具备该行为!");
		} else {
			log.info("不具备该行为!");
		}

		// 测试用户是否具备某一个行为.
		if (currentUser.isPermitted("user:delete:lisi")) {
			log.info("----> 具备该行为!");
		} else {
			log.info("不具备该行为!");
		}
		System.out.println("用户是否已认证?" + currentUser.isAuthenticated());
		// 用户登出
		currentUser.logout();
		System.out.println("用户是否已认证?" + currentUser.isAuthenticated());
		System.exit(0);
	}
}

执行该类的main方法后,控制台输出如下:
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
通过这个简单的demo示例,我们可以大概的了解shiro的一个运行过程。

6、Spring集成Shiro

6.1、启动demo

上面的测试demo是在非web环境下的测试,以下将会在web环境上集成Spring进行测试,先建立一个动态的web项目:
Shiro入门

(1)引入jar依赖包

首先引入Spring和shiro的依赖包到lib目录下,如下:
Shiro入门
(2)配置文件

这里有3个配置文件:spring-context.xml、spring-mvc.xml、ehcache.xml,以及web.xml总配置。

先来看web.xml内容,如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
	http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	id="WebApp_ID" version="3.1">
	
	<display-name>shiro-1</display-name>
	
	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>
	
	<!-- 加载spring上下文配置 -->
	<context-param>
	    <param-name>contextConfigLocation</param-name>
	    <param-value>classpath:spring-context.xml</param-value>
	</context-param>
	<!-- 上下文监听 -->
	<listener>
	    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	
	<!-- 加载springmvc配置 -->
	<servlet>
	   <servlet-name>springMVC</servlet-name>
	   <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	   <init-param>
	      <param-name>contextConfigLocation</param-name>
	      <param-value>classpath:spring-mvc.xml</param-value>
	   </init-param>
	   <load-on-startup>1</load-on-startup>
	</servlet>
	<!-- 映射 -->
	<servlet-mapping>
	   <servlet-name>springMVC</servlet-name>
	   <url-pattern>/</url-pattern>
	</servlet-mapping>
	
	<!-- 配置shiro的过滤器 -->
	<!-- DelegatingFilterProxy是一个代理对象,默认情况下,Spring会到IOC容器中查找和filtername对应的bean对象,
	     也可以通过targetBeanName的初始化参数来配置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>

Spring的核心配置spring-context.xml内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans.xsd 
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context.xsd">
	
	<!-- 配置securityManager -->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
	    <!--  引入缓存管理器 -->
	    <property name="cacheManager" ref="cacheManager"></property>
	    <property name="realm" ref="jdbcRealm"></property>
	</bean>
	
	<!-- 配置cacheManager缓存管理器 -->
	<!-- 使用ehcache,需要引入jar包和配置文件 -->
	<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
	   <!-- 载入ehcache配置文件 -->
	   <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property>
	</bean>
	
	<!-- 配置realm管理器,这里用的是自定义的,对Realm接口的空实现 -->
	<bean id="jdbcRealm" class="com.ycz.shiro.realm.ShiroRealm">
	</bean>
	
	<!-- 配置LifecycleBeanPostProcessor,可以在springIOC中使用shirobean对象的生命周期方法 -->
	<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>
	
	<!-- IOC中启用shiro注解,依赖上面的bean对象,所以必须先配置上面的LifecycleBeanPostProcessor -->
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
	   depends-on="lifecycleBeanPostProcessor"></bean>
	<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor ">
	    <property name="securityManager" ref="securityManager"></property>
	</bean>
	
	<!-- 配置shiroFilter,id必须和web.xml中配置的一致,在加载web.xml进行初始化的时候,已经创建了一个名为shiroFilter的对象,
	     所以这里如果要使用的话,那么名称必须一致,如果在IOC容器中找不到这个名称的bean,会抛出NoSuchBeanDefinitionException异常-->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
	    <property name="securityManager" ref="securityManager"></property>
	    <!-- 登录页面 -->
	    <property name="loginUrl" value="/login.jsp"></property>
	    <!-- 登录成功页面 -->
	    <property name="successUrl" value="/index.jsp"></property>
	    <!-- 未认证页面 -->
	    <property name="unauthorizedUrl" value="/unauthorized.jsp"></property>
	    <!-- 配置受保护的页面,以及访问所需要的权限 -->
	    <!-- anno允许匿名访问
	         authc必须经过认证后才能访问,访问未认证的页面会重定向到loginUrl登录页面
	         未配置的默认允许访问
	         多个url配置有交叠的部分,以第一次配置的优先
	          -->
	    <property name="filterChainDefinitions">
	        <value>
	           /login.jsp = anon
	           /** = authc 
	        </value> 
	    </property>
	</bean>
	
</beans>

SpringMVC的配置文件spring-mvc.xml内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc 
    http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
    http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
    <!-- 包扫描 -->
    <context:component-scan base-package="com.ycz.shiro.*"></context:component-scan>
    
    <!-- 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
       <property name="prefix" value="/"></property>
       <property name="suffix" value=".jsp"></property>
    </bean>
    
    <!-- 注解驱动 -->
    <mvc:annotation-driven></mvc:annotation-driven>
    
    <!-- 静态资源放行 -->
    <mvc:default-servlet-handler/>
    
</beans>

缓存的配置文件ehcache.xml内容如下:

<ehcache>

    <!-- Sets the path to the directory where cache .data files are created.

         If the path is a Java System Property it is replaced by
         its value in the running VM.

         The following properties are translated:
         user.home - User's home directory
         user.dir - User's current working directory
         java.io.tmpdir - Default temp file path -->
    <diskStore path="java.io.tmpdir"/>
    
    <cache name="authorizationCache"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <cache name="authenticationCache"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <cache name="shiro-activeSessionCache"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <!--Default Cache configuration. These will applied to caches programmatically created through
        the CacheManager.

        The following attributes are required for defaultCache:

        maxInMemory       - Sets the maximum number of objects that will be created in memory
        eternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the element
                            is never expired.
        timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
                            if the element is not eternal. Idle time is now - last accessed time
        timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
                            if the element is not eternal. TTL is now - creation time
        overflowToDisk    - Sets whether elements can overflow to disk when the in-memory cache
                            has reached the maxInMemory limit.

        -->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />

    <!--Predefined caches.  Add your cache configuration settings here.
        If you do not have a configuration for your cache a WARNING will be issued when the
        CacheManager starts

        The following attributes are required for defaultCache:

        name              - Sets the name of the cache. This is used to identify the cache. It must be unique.
        maxInMemory       - Sets the maximum number of objects that will be created in memory
        eternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the element
                            is never expired.
        timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
                            if the element is not eternal. Idle time is now - last accessed time
        timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
                            if the element is not eternal. TTL is now - creation time
        overflowToDisk    - Sets whether elements can overflow to disk when the in-memory cache
                            has reached the maxInMemory limit.

        -->

    <!-- Sample cache named sampleCache1
        This cache contains a maximum in memory of 10000 elements, and will expire
        an element if it is idle for more than 5 minutes and lives for more than
        10 minutes.

        If there are more than 10000 elements it will overflow to the
        disk cache, which in this configuration will go to wherever java.io.tmp is
        defined on your system. On a standard Linux system this will be /tmp"
        -->
    <cache name="sampleCache1"
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="true"
        />

    <!-- Sample cache named sampleCache2
        This cache contains 1000 elements. Elements will always be held in memory.
        They are not expired. -->
    <cache name="sampleCache2"
        maxElementsInMemory="1000"
        eternal="true"
        timeToIdleSeconds="0"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        /> -->

    <!-- Place configuration for your caches following -->

</ehcache>

(3)核心Realm编写
在spring-context.xml中,关于Shiro有这样的配置:
Shiro入门
这里面配置了Realm的管理器,而这个管理器是由用户提供的。在realm包下创建新的类ShiroRealm.java,具体信息如下:

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.realm.Realm;

/*
 * 该类作为Realm组件使用
 */
public class ShiroRealm implements Realm {

	@Override
	public AuthenticationInfo getAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public String getName() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public boolean supports(AuthenticationToken arg0) {
		// TODO Auto-generated method stub
		return false;
	}

}

这个Realm管理器实现了org.apache.shiro.realm.Realm接口,该接口的源码如下:
Shiro入门
Shiro入门
这个接口只定义了3个抽象方法:getName()、supports(AuthenticationToken token)、getAuthenticationInfo(AuthenticationToken token),具体实现由用户自定义。

(4)jsp页面
在webapp目录下创建两个jsp页面:登录的login.jsp和登录成功后的跳转页面index.jsp。

login.jsp内容如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
     <form action="shiro/login" method="post">
        用户名:<input type="text" name="username" />
        <br><br>
        密码:<input type="password" name="password" />
        <br><br>
        <input type="submit" value="提交" />
     </form>
</body>
</html>

index.jsp内容如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录成功页面</title>
</head>
<body>
    登录成功!!!<br><br>
    <a href="shiro/logout">退出</a>
</body>
</html>

(5)启动测试

将此项目加到web容器中,然后启动:
Shiro入门
观察控制台,项目是启动成功的,也有可能启动阶段报错,如果报错的话,需要检查一下配置文件,极有可能是spring-context.xml配置的问题。

启动成功后访问项目:
Shiro入门
发现重定向到了login.jsp页面,web.xml中配置的默认页面如下:
Shiro入门
按照一般情况,访问项目应该是默认跳转到index.jsp页面,现在却重定向到了login.jsp页面,原因是在spring-context.xml中有这样的配置:
Shiro入门
在ShiroFilter里面配置了shiro的默认登录页面login.jsp,然后在filterChainDefinitions配置了页面的访问权限,只有login.jsp是允许匿名访问的,除此之外的其他资源都需要进行验证才能访问,因为没有验证,所以访问项目自动重定向到了shiro的登录loginUrl指向的页面。这里修改一下:
Shiro入门
再访问项目:
Shiro入门
现在是直接来到了默认的访问页,而没有重定向到shiro的登录页面。

6.2、ShiroFilter的原理

Shiro入门
说明如下:

  • Shiro提供了与Web集成的支持,其通过一个ShiroFilter入口来拦截需要安全控制的URL,然后
    进行相应的控制。
  • ShiroFilter类似于如Strut2/SpringMVC这种web框架的前端控制器,是安全控制的入口点,其
    负责读取配置(如ini配置文件),然后判断URL是否需要登录/权限等工作。

在项目的webapp目录下加一个新的页面user.jsp,如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户界面</title>
</head>
<body>
   欢迎来到用户界面!!!
</body>
</html>

然后修改spring-context.xml中的filterChainDefinitions配置项,如下:
Shiro入门
这里配置了login.jsp和user.jsp页面无需验证,然后其他资源页面都需要验证,启动项目访问:
Shiro入门
访问http://localhost:8081/shiro-1,shiro帮我们重定向到了login.jsp,因为默认index.jsp无权访问,所以重定向到了shiro指定loginUrl的页面。按照配置,user.jsp页面是可以匿名访问无需认证的,直接输入访问:
Shiro入门
事实上也是可以的。

6.2.1、DelegatingFilterProxy代理

DelegatingFilterProxy作用是自动到Spring容器查找名字为shiroFilter(filter-name)的bean并把所有 Filter的操作委托给它。

在web.xml中有这样的配置:
Shiro入门
所有的请求url都由DelegatingFilterProxy这个类进行拦截,然后DelegatingFilterProxy去Spring的容器中寻找一个名为shiroFilter的bean对象,把接下来的所有过滤操作交给shiroFilter处理。

在spring-context.xml中我们有这样的配置:
Shiro入门
项目加载时向Spring容器中注入了一个shiroFilter的bean对象,类型是ShiroFilterFactoryBean,因为有这个bean对象的存在,所以DelegatingFilterProxy才会将请求委托给该bean对象来进行处理,如果这个bean对象不存在,应该会报错,修改如下:
Shiro入门
将委托对象名称改为shiroFilter2,这个bean对象在Spring容器中是不存在的,然后启动项目:
Shiro入门
启动阶段抛出了异常,原因是找不到shiroFilter2这个bean对象,确实该对象在Spring容器中是不存在的。定位到具体的位置:
Shiro入门
Shiro入门
异常定位到了DelegatingFilterProxy类的initDelegate方法,打上断点,点击getTargetBeanName()方法:
Shiro入门
Shiro入门
找到set方法:
Shiro入门
根据说明,在没有指定targetBeanName的情况下,会默认的使用web.xml中的filter-name作为targetBeanName的默认值,web.xml中配置如下:
Shiro入门
刚才已经打了断点,现在以debug的形式运行项目:
Shiro入门
找到属性targetBeanName的值,确实是shiroFilter2,因为在Spring容器中找不到这个名称的bean对象,所以启动时就抛出异常了,现在修改spring-context.xml中bean对象的名称为shiroFilter2:
Shiro入门
重新启动项目:
Shiro入门
正常启动,没有抛异常,再访问项目:
Shiro入门
可以正常访问,是OK的。所以得出一个结论:在不指定targetBeanName的情况下,web.xml中DelegatingFilterProxy类的filter-name的值必须和spring-context.xml中向Spring容器中注入的ShiroFilterFactoryBean类的bean对象名称保持一致,否则会报错。

可以指定targetBeanName的参数值,这时filter-name值可以随意,做如下修改:
Shiro入门
现在filter-name的值为shiroFilter,而指定targetBeanName参数值为shiroFilter2,然后spring-context.xml中相关配置如下:
Shiro入门
这个bean对象的名称还是shiroFilter2,重新启动项目:
Shiro入门
正常启动,然后访问项目:
Shiro入门
可以正常访问。将web.xml改回来:
Shiro入门
filter-name的值与bean对象名称一致,那么可以省去配置targetBeanName参数的步骤,简单一点。

6.2.2、filterChainDefinitions配置

Shiro入门
我们一般在filterChainDefinitions中来指定过滤规则,一般公共配置使用配置文件,例如js、css、img这些静态资源文件无需拦截,这里不做考虑。过滤规则类似url的形式,比如上面的/login.jsp = anon,说明如下:

  • 过滤规则的格式是: “url=拦截器[参数],拦截器[参数]”。
  • 如果当前请求的url匹配 [urls] 部分的某个url模式,将会执行其配置的拦截器。
  • anon(anonymous)拦截器表示匿名访问(即不需要登录即可访问)。
  • authc (authentication)拦截器表示需要身份认证通过后才能访问。

6.2.2.1、过滤器种类

shiro中使用过滤器对配置的url来进行拦截或放行处理,shiro中默认的过滤器如下图所示:
Shiro入门

6.2.2.2、URL匹配模式

url模式是使用Ant风格模式进行配置的。Ant路径通配符支持 ?、*、**,注意通配符匹配不包括目录分隔符“/”。说明如下:

  • ?:匹配一个字符,如/admin?将匹配 /admin1,但不匹配/admin或/admin/。
  • *:匹配零个或多个字符串,如/admin将匹配/admin、/admin123,但不匹配/admin/1。
  • ** :匹配路径中的零个或多个路径,如 /admin/** 将匹配/admin/a或/admin/a/b。

6.2.2.3、URL匹配顺序

URL权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的url模式对应的拦截器链。例子如下:
/bb/** = filter1
/bb/aa = filter2
/** = filter3

如果请求的url是“/bb/aa”,那么会按照声明顺序进行匹配,将使用filter1进行拦截而不是filter2,配置url路径有交叠的部分将以先配置的优先,前面的会覆盖后面的。
Shiro入门
比如上面的配置,user.jsp就是匿名无需认证即可访问的:
Shiro入门
如果改成这样:
Shiro入门
这时user.jsp需要认证才能访问,被上面的/** = authc覆盖了。
Shiro入门
访问user.jsp,shiro重定向到了登录页面,说明user.jsp是受保护的,需要认证才能访问。

6.3、Shiro认证

认证就是用户登录的过程,也就是验证用户身份的合法性。从外部来看,即从应用程序的角度来看Shiro,流程如下:
Shiro入门
身份验证有关的几条说明如下:

  • 身份验证:一般需要提供如身份ID等一些标识信息来表明登录者的身份,如提供email,用户名/密码来证明。
  • 在shiro中,用户需要提供principals(身份)和credentials(证明给shiro,从而应用能验证用户身份。
  • principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/邮箱/手机号。
  • credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。最常见的principals和 credentials组合就是用户名/密码。

身份验证的基本流程如下图:
Shiro入门
可以从最开始的Quickstart例子来看shiro认证的简单流程,如下:
Shiro入门
先是将用户名和密码封装成一个UsernamePasswordToken类型对象,然后以此对象为参数,调用login方法执行登录验证,可以看login方法的相关源码:
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
Shiro入门
doGetAuthenticationInfo方法就到了最底层的具体实现。所以从以上可以得出一个结论:
抛开继承关系,最终继承的是org.apache.shiro.realm.AuthenticatingRealm类,需要实现的是doGetAuthenticationInfo(AuthenticationToken token)方法。

6.3.1、环境搭建

经过以上分析,会通过一个demo来更好的认识shiro认证的过程。

创建一个登录页面login.jsp,实际上已经有了,内容如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
     <form action="shiro/login" method="post">
        用户名:<input type="text" name="username" />
        <br><br>
        密码:<input type="password" name="password" />
        <br><br>
        <input type="submit" value="提交" />
     </form>
</body>
</html>

然后创建一个handlers包,包下创建一个控制器ShiroHandler:
Shiro入门
内容如下:

@Controller
@RequestMapping("/shiro")
public class ShiroHandler {
	
	@RequestMapping("/login")
	public String login(@RequestParam("username") String username,
			@RequestParam("password") String password) {
		// 获取主体
		Subject currentUser = SecurityUtils.getSubject();
		if(!currentUser.isAuthenticated()) {
			// 用户名密码封装为Token对象
			UsernamePasswordToken token = new UsernamePasswordToken(username, password);
			System.out.println("token的哈希码值:" + token.hashCode());
			token.setRememberMe(true); // 记住我
			try {
				currentUser.login(token);// 执行登录
			} catch (AuthenticationException ae) {
				System.out.println("登录失败!" + ae.getMessage());
			}
		}
		return "redirect:/index.jsp";
	}
	

}

修改realm包下的ShiroRealm,修改后内容如下:

/*
 * 该类作为Realm组件使用
 */
public class ShiroRealm extends AuthenticatingRealm {

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) 
			throws AuthenticationException {
		System.out.println("传入token的哈希码值:" + arg0.hashCode());
		return null;
	}

}

如果只需要做认证的话,自定义的Realm只用继承AuthenticatingRealm类,然后实现doGetAuthenticationInfo()方法就行了,实际中认证和授权其实是分不开的,这里暂时不考虑那么多。

最后需要修改spring-context.xml中shiro的放行路径:
Shiro入门
将登录请求的url设为匿名访问,否则登录请求到达不了控制器。启动访问项目:
Shiro入门
任意输入用户名和密码,然后提交:
Shiro入门
控制台输出如下:
Shiro入门
token的哈希码值一样,说明控制器里封装的token值传入了ShiroRealm的方法里:
Shiro入门
这个方法接收到了传入的token参数,说明这个Realm是起作用的。

6.3.2、Realm业务逻辑

上面只是简单的搭建好了架子,让项目可以跑起来,下面编写ShiroRealm类的核心业务代码,内容如下:

public class ShiroRealm extends AuthenticatingRealm {

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) 
			throws AuthenticationException {
		System.out.println("AuthenticationToken=" + arg0);
		// token类型转换
		UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) arg0;
		// token中获取用户名
		String username = usernamePasswordToken.getUsername();
		System.out.println("用户名:" + username);
	    // 从数据库中查询是否存在,这里直接用静态的
		if("unknow".equals(username)) {
			throw new UnknownAccountException("用户不存在!");
		}
		// 该用户是否锁定
		if("monster".equals(username)) {
			throw new LockedAccountException("该用户被锁定!");
		}
		// 根据用户情况,构建AuthenticationInfo对象并返回,通常会使用SimpleAuthenticationInfo类
		// 以下信息从数据库中获取:
		// (1) principal:认证的实体信息,可以是username,也可以是数据表封装的用户实体对象
		Object principle = username;
		// (2) credentials:密码
		Object credentials = "ycz123456";
		// (3) realName:当前realm对象的name值,调用父类CachingRealm的getName方法
		String realName = getName();
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principle, credentials, realName);
		return info;
	}

}

然后在index.jsp页面加登出功能:
Shiro入门
配置放行登出请求的url路径:
Shiro入门
如果不执行登出的话,shiro会有默认的缓存,登录后shiro会清理缓存。

启动并访问项目,使用用户名unknow进行登录:
Shiro入门
控制台输出:
Shiro入门
使用用户名monster进行登录:
Shiro入门
控制台输出:
Shiro入门
使用其他用户名进行登录,密码使用错误的:
Shiro入门
控制台输出:
Shiro入门
使用正确的密码登录:
Shiro入门
控制台输出:
Shiro入门
到这里已经登录成功了,也就是用户已经经过了shiro的认证。并且跳转到了index.jsp页面,可以直接访问user.jsp页面资源:
Shiro入门

6.3.3、密码相关

6.3.3.1、密码的比对

在UsernamePasswordToken类的getPassword()方法上打断点,以debug的形式运行项目,输入任意用户名密码提交:
Shiro入门
Shiro入门
Shiro入门
看上一步:
Shiro入门
在同一个类里面调用了getPassword()方法。再看上一步:
Shiro入门
Shiro入门
在SimpleCredentialsMatcher类里面调用了getCredentials(AuthenticationToken token)方法。再看上一步:
Shiro入门
在同一个类里的doCredentialsMatch()方法里面调用了getCredentials()方法,而且是两次,分别处理表单提交的信息和数据库真实信息。再看上一步:
Shiro入门
Shiro入门
在AuthenticatingRealm类的assertCredentialsMatch()方法里调用了doCredentialsMatch()方法,并且这里是用CredentialsMatcher来进行密码比对的,CredentialsMatcher类的层级关系如下:
Shiro入门
最底层有6个实现类,采用了不同的算法来进行密码的加密和比对。所以通过源码得出的结论是:通过AuthenticatingRealm类的CredentialsMatcher属性进行密码的比对。

6.3.3.2、密码的MD5加密

点击Md5CredentialsMatcher类查看源码:
Shiro入门
这个类已经废弃了,官方推荐直接使用HashedCredentialsMatcher类,部分源码如下:
Shiro入门
Shiro入门
Shiro入门
Shiro入门
最终调用了SimpleHash类的构造器,SimpleHash的部分源码如下:
Shiro入门
该类有3个比较重要的属性:algorithmName(加密算法名称)、salt(盐值)、iterations(加密次数)。相应的构造方法如下:
Shiro入门
修改spring-context.xml中的realm管理器,添加credentialsMatcher属性,如下:
Shiro入门
匹配器使用HashedCredentialsMatcher匹配器,使用MD5算法进行加密,加密次数为10。上面已经在UsernamePasswordToken的getPassword()方法里面打了断点:
Shiro入门
重新debug运行项目:
Shiro入门
用户名随意,密码使用ycz123456,然后提交,跳过几步之后看tokenHashedCredentials变量的值:
Shiro入门
密码加密后的值为:58de609e5422929b4b47d1c41deae609。

建立一个测试类来测试加密:

public class Test {
	
	public static void main(String[] args) {
		String hashAlgorithmName = "MD5";
		Object credentials = "ycz123456";
		Object salt = null;
		int hashIterations = 10;
		// 进行明文密码加密
		Object result = new SimpleHash(hashAlgorithmName, credentials, salt,hashIterations);
		System.out.println(result);
	}

}

这里的加密算法、明文密码、加密次数和项目里的是对应的,按照理论,加密后的密文应该一样,运行main方法,控制台输出:
Shiro入门
对比发现确实是一样的,将此密文值复制,替换realm中的明文密码值:
Shiro入门
然后重新启动项目访问:
Shiro入门
密码使用ycz123456进行登录:
Shiro入门
登录成功。说明前台输入的明文ycz123456和后台的密文58de609e5422929b4b47d1c41deae609是匹配上的,那么用户登录成功,即认证成功。

6.3.3.2、MD5盐值加密

上面是不加盐,这样的缺点是只要明文密码一样,那么加密后的密文就一定一样,这样很不安全。所以为了保证即使明文密码一样,加密后的密文也不一样,需要加盐值然后再加密。在Test测试类中修改:

	public static void main(String[] args) {
		String hashAlgorithmName = "MD5";
		Object credentials = "ycz123456";
		Object salt = ByteSource.Util.bytes("admin");
		int hashIterations = 10;
		// 进行明文密码加密
		Object result = new SimpleHash(hashAlgorithmName, credentials, salt,hashIterations);
		System.out.println(result);
	}

运行Main()方法,控制台输出:
Shiro入门
将admin参数改为yanchengzhi:
Shiro入门
再运行Main()方法,控制台:
Shiro入门
可以看到,明文密码是一样的,但是因为加了不同的盐来进行加密,所以加密后的密文是不一样的。修改ShiroRealm,修改后的内容如下:

public class ShiroRealm extends AuthenticatingRealm {

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) 
			throws AuthenticationException {
		System.out.println("AuthenticationToken=" + arg0);
		// token类型转换
		UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) arg0;
		// token中获取用户名
		String username = usernamePasswordToken.getUsername();
		System.out.println("用户名:" + username);
	    // 从数据库中查询是否存在,这里直接用静态的
		if("unknow".equals(username)) {
			throw new UnknownAccountException("用户不存在!");
		}
		// 该用户是否锁定
		if("monster".equals(username)) {
			throw new LockedAccountException("该用户被锁定!");
		}
		// 根据用户情况,构建AuthenticationInfo对象并返回,通常会使用SimpleAuthenticationInfo类
		// 以下信息从数据库中获取:
		// (1) principal:认证的实体信息,可以是username,也可以是数据表封装的用户实体对象
		Object principle = username;
		// (2) credentials:密码
		Object credentials = null;
		if("admin".equals(username)) {
			credentials = "aca6eaa0f503fdb9857d4492918c51f0";
		} else if("yanchengzhi".equals(username)) {
			credentials = "26aedda26b00c00a70c50c67c67896af";
		}
		// (3) realName:当前realm对象的name值,调用父类CachingRealm的getName方法
		String realName = getName();
		// (4) salt:盐值
		ByteSource salt = ByteSource.Util.bytes(username);
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principle, credentials, salt,realName);
		return info;
	}

}

重启项目并访问,使用yanchengzhi + ycz123456进行登录:
Shiro入门
Shiro入门
登录是成功的,再使用admin + ycz123456进行登录:
Shiro入门
Shiro入门
登录也是成功的。加盐加密的步骤总结如下三步:

  • 构造SimpleAuthenticationInfo对象时,使用4个参数的构造器(加密算法、密码、盐值、加密次数)。
  • 使用ByteSource.Util的bytes方法来计算盐值(盐值需要唯一,一般会使用随机生成的字符串或者userid)。
  • 使用new SimpleHash(hashAlgorithmName, credentials, salt,num)来计算加盐后的加密密文值。

6.3.4、多个Realm数据源

Shiro中的Realm就相当于实体数据源,所有用户信息都是从Realm中来获取的。前面事实上只有一个Realm,而实际开发中可能有多个Realm。下面测试项目中使用两个realm。

在realm包下创建SecondRealm类,内容如下:

public class SecondRealm extends AuthenticatingRealm {

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) 
			throws AuthenticationException {
		System.out.println("[Second Realm]AuthenticationToken=" + arg0);
		// token类型转换
		UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) arg0;
		// token中获取用户名
		String username = usernamePasswordToken.getUsername();
		System.out.println("用户名:" + username);
	    // 从数据库中查询是否存在,这里直接用静态的
		if("unknow".equals(username)) {
			throw new UnknownAccountException("用户不存在!");
		}
		// 该用户是否锁定
		if("monster".equals(username)) {
			throw new LockedAccountException("该用户被锁定!");
		}
		// 根据用户情况,构建AuthenticationInfo对象并返回,通常会使用SimpleAuthenticationInfo类
		// 以下信息从数据库中获取:
		// (1) principal:认证的实体信息,可以是username,也可以是数据表封装的用户实体对象
		Object principle = username;
		// (2) credentials:密码
		Object credentials = null;
		if("admin".equals(username)) {
			credentials = "1fd8be1c60cf36a5d288c9be6569d695b14736aa";
		} else if("yanchengzhi".equals(username)) {
			credentials = "8f42eacf4558f1e3bd12ddc8de148039ac0d1d36";
		}
		// (3) realName:当前realm对象的name值,调用父类CachingRealm的getName方法
		String realName = getName();
		// (4) salt:盐值
		ByteSource salt = ByteSource.Util.bytes(username);
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principle, credentials, salt,realName);
		return info;
	}

}

事实上和前面的ShiroRealm基本一样,只是在这里更换了credentials的密码而已。如下:
Shiro入门
这个密码的由来是使用了SHA1算法加密10次得来的,前面是使用了MD5,这里是用了不同的算法,测试类如下:
Shiro入门
运行main()方法,控制台输出:
Shiro入门
然后换用户名为yanchengzhi:
Shiro入门
运行main()方法,得到另一个密文密码:
Shiro入门
Realm编写完毕,下面需要进行配置,在spring-context.xml中修改:
Shiro入门
Shiro入门
Shiro入门
圈出的地方为新添加或修改的。

下面分析源码,打开ModularRealmAuthenticator类:
Shiro入门
Shiro入门
在箭头处打断点,然后可以看下面的代码部分,如果是一个安全数据源realm,那么会调用doSingleRealmAuthentication方法,如果是多个,会调用doMultiRealmAuthentication方法。点开doMultiRealmAuthentication方法,源码如下:
Shiro入门
再打开UsernamePasswordToken类源码:
Shiro入门
还是在getPassword方法这里打断点:
Shiro入门
以debug的形式运行项目:
Shiro入门
用yanchengzhi + ycz123456进行登录:
Shiro入门
可以看到,现在ShiroRealm和SecondRealm都起了作用。点开查看:
Shiro入门
可以看到,第一个realm使用了MD5加密算法,是ShiroRealm,第二个使用了SHA1算法,是SecondRealm。放行断点,控制台输出:
Shiro入门
这里明确的可以看到,两个realm都起了作用,但有一个顺序。刚才看源码使用的是List结构:
Shiro入门
List集合中元素有顺序,所以可以理解第一个ShiroRealm先起了作用,是因为在spring-context.xml中的配置顺序问题:
Shiro入门
第一个配置的是List中下标为0的元素,第二个配置的是List中下标为1的元素。

6.3.5、认证策略

Shiro中的认证策略由AuthenticationStrategy接口管理,该接口的源码如下:
Shiro入门
Shiro入门
Shiro入门
Shiro入门
这个接口里只有4个抽象方法:beforeAllAttempts()、beforeAttempt()、afterAttempt()、afterAllAttempts(),具体由实现类来实现,这个接口的实现类如下:
Shiro入门
只有1个实现类AbstractAuthenticationStrategy:
Shiro入门
而这个类有3个子类:AllSuccessfulStrategy、AtLeastOneSuccessfulStrategy、FirstSuccessfulStrategy。那么Shiro里的认证策略就只有3种,如下进行说明:

  • FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略。
  • AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将返回所有Realm身份验证成功的认证信息。
  • AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。

而认证器ModularRealmAuthenticator默认采用的是AtLeastOneSuccessfulStrategy策略。

为了验证以上信息,进入ModularRealmAuthenticator类的源码:
Shiro入门
找到这个方法:
Shiro入门
在以下位置打上断点:
Shiro入门
修改ShiroRealm和SecondRealm中的返回信息:
Shiro入门
Shiro入门
以debug形式启动项目,并以admin + ycz123456进行登录:
Shiro入门
Shiro入门
可以看到默认的认证策略确实是AtLeastOneSuccessfulStrategy,并且返回了认证成功的所有realm信息。登录也是成功的:
Shiro入门
修改SecondRealm,将admin账号的密码改为错误的:
Shiro入门
再debug,还是以admin + ycz123456进行登录:
Shiro入门
可以看到,现在只返回了登录成功realm的信息,也就是ShiroRealm的信息,而SecondRealm是没有登录成功的,因为密码不对。但是最后是认证成功的:
Shiro入门
因为这个认证策略,只有有一个realm认证成功,那么就会认证成功。

下面修改认证策略,修改spring-context.xml,如下:
Shiro入门
圈出的为添加的部分,在这里指定认证策略为AllSuccessfulStrategy,只有所有realm都验证成功,那么才会认证成功,并且会返回所有验证成功realm的认证信息。

重新debug项目,以admin + ycz123456进行登录:
Shiro入门
控制台信息:
Shiro入门
Shiro入门
登录失败了,因为SecondRealm验证失败了,密码不对,将密码改回正确的:
Shiro入门
再以admin + ycz123456进行登录:
Shiro入门
Shiro入门
控制台:
Shiro入门
登录成功,两个realm都验证成功了,所以最终认证成功。

6.3.6、修改realms配置管理

现在的spring-context.xml中关于realms的管理配置如下:
Shiro入门
现在这个realms管理是配置在了authenticator里面,将此部分移除:
Shiro入门
然后配置在securityManager里面:
Shiro入门
启动然后访问项目:
Shiro入门
Shiro入门
登录成功。打开AuthenticatingSecurityManager类的源码:
Shiro入门
Shiro入门
在源码的afterRealmsSet()方法里做了这样一件事:如果此对象属于ModularRealmAuthenticator类型,那么将其强转为ModularRealmAuthenticator类型,然后设置给ModularRealmAuthenticator的realms属性:
Shiro入门
Shiro入门
最后将Shiro的认证策略改为默认的:
Shiro入门
将指定策略注释掉就行了,那么现在Shiro的认证策略为原来的AtLeastOneSuccessfulStrategy。认证完成后,以下将进一步讨论Shiro的授权。

6.4、授权

授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)资源(Resource)权限(Permission)角色(Role)

以下对几个对象进行说明:

  • 主体(Subject):访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。
  • 资源(Resource):在应用中用户可以访问的URL,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只有授权后才能访问相应的资源。
  • 权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许。Shiro支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的)。
  • 角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。

6.4.1、授权方式

在Shiro中,授权支持三种方式:编程式、注解式、标签式。

  • 编程式:通过写if/else授权代码块完成。伪代码如下:
if(subject.hasRole("admin")) {
    // 某些有权限的操作
} else {
    // 某些无权限的操作
}
  • 注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相应的异常。如下:
@RequiresRoles("admin")
public void helloWorld() {
    ......
}
  • 标签式:在JSP/GSP页面通过相应的标签完成。如下:
<shiro:hasRole name="admin">
......
</shiro:hasRole>

6.4.2、默认拦截器

Shiro中内置了很多默认的拦截器,比如身份验证、授权等相关的。打开org.apache.shiro.web.filter.mgt.DefaultFilter类的源码:
Shiro入门
可以看到它是一个枚举类,里面枚举了Shiro支持的所有默认拦截器,一共有11种。

关于身份验证相关的如下
Shiro入门

关于授权相关的如下
Shiro入门
其它的如下
Shiro入门
修改index.jsp,如下:
Shiro入门
加两个a标签的链接,然后user.jsp如下:
Shiro入门
yanchengzhi.jsp如下:
Shiro入门
启动项目,以yanchengzhi + ycz123456进行登录:
Shiro入门
登录成功,点击第一个链接:
Shiro入门
Shiro入门
成功跳转,退后一步,然后点击第二个链接:
Shiro入门
可以看到,现在这两个链接指向的页面资源是可以访问到的。

在spring-context.xml中配置角色权限,如下:
Shiro入门
意思是拥有admin角色才可以访问user.jsp页面,拥有yanchengzhi角色才可以访问yanchengzhi.jsp页面。现在两个用户都未配置角色,按照道理来说是访问不到这两个页面资源的。重启项目并登录到主页面:
Shiro入门
点击第一个链接:
Shiro入门
Shiro入门
点击第二个链接:
Shiro入门
跳到了未认证的页面,也就是这里的配置:
Shiro入门
事实上配置了角色权限之后,确实是访问不到user.jsp和yanchengzhi.jsp页面资源了。

6.4.3、Shiro中的Permissions

实例级访问控制

  • 这种情况通常会使用三个部件: 域、操作、被付诸实施的实例。如:user:edit:manager
  • 也可以使用通配符来定义,如:user:edit:* 、user:* : * 、user: * :manager。
  • 部分省略通配符:缺少的部件意味着用户可以访问所有与之匹配的值,比如user:edit等价于user:edit : * 、user等价于user: * : * 。
  • 注意:通配符只能 从字符串的结尾处省略部件,也就是说user:edit并不等价于user: * :edit。

6.4.4、授权的步骤

回到Quickstart类中,有这样一行:
Shiro入门
打开这个hasRole方法的实现类:
Shiro入门
Shiro入门
继续打开securityManager.hasRole()方法的实现类:
Shiro入门
Shiro入门
Shiro入门
点击进入getAuthorizationInfo()方法:
Shiro入门
此方法里有这样一行:
Shiro入门
打开doGetAuthorizationInfo()方法的实现类:
Shiro入门
是具体的实现子类,里面有我们自定义的TestRealm类,如果是多个Realm的话,那么在这里:
Shiro入门
会进入ModularRealmAuthorizer类的hasRole()方法而不是AuthorizingRealm类的hasRole方法:
Shiro入门
Shiro入门
在这个方法里会遍历所有的Realm,只要有一个Realm认证成功,那么就会返回true,即授权成功,然后继续回到AuthorizingRealm类中的hasRole()方法。

小结:授权需要继承自AuthorizingRealm类,并实现其doGetAuthenticationInfo()方法,而AuthorizingRealm类是继承自AuthenticatingRealm类的,但是没有实现AuthenticatingRealm类中的doGetAuthenticationInfo()方法,所以认证和授权只需要继承AuthorizingRealm类,并且实现其中的两个抽象方法doGetAuthorizationInfo()和doGetAuthenticationInfo()方法。

示例如下:

/**
 * 认证和授权需继承AuthorizingRealm类,并实现两个抽象方法
 * @author 17605
 *
 */
public class TestRealm extends AuthorizingRealm{

	// 用于授权的方法
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		// TODO Auto-generated method stub
		return null;
	}

	// 用于认证的方法
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		// TODO Auto-generated method stub
		return null;
	}

}

6.4.5、Realm的实现

修改ShiroRealm:
Shiro入门
让其继承AuthorizingRealm类,并实现doGetAuthorizationInf授权方法:

	// 授权方法
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		System.out.println("进入此方法进行授权!");
		// 获取登录用户信息
		Object principal = principals.getPrimaryPrincipal();
		// 利用登录的用户信息来验证当前用户的角色和权限(可能需要查询数据库)
		Set<String> roles = new HashSet<>();
		roles.add("yanchengzhi");
		if("admin".equals(principal)) {
			roles.add("admin");
		}
		// 利用其roles值创建SimpleAuthorizationInfo对象
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
		return info;
	}

如果是admin用户的话,该用户会有两个角色admin和yanchengzhi,如果是其他用户的话,只有一个角色yanchengzhi。

重启项目,以admin + ycz123456进行登录:
Shiro入门
Shiro入门
登录成功,点击第一个链接:
Shiro入门
可以访问,返回一步,点击第二个链接:
Shiro入门
Shiro入门
也可以访问,说明该用户拥有两个角色。

退出当前用户,以yanchengzhi + ycz123456进行登录:
Shiro入门
Shiro入门
登录成功,点击第一个链接:
Shiro入门
无权限访问,返回,点击第二个链接:
Shiro入门
Shiro入门
可以访问,说明该用户只拥有一个角色。进一步说明授权是成功的。

6.4.6、Shiro标签

Shiro提供了JSTL标签用于在JSP页面进行权限控制,如根据登录用户显示相应的页面按钮。常用的标签如下:

(1) guest标签

用户没有身份验证时显示相应信息,即游客访问信息。
Shiro入门
(2) user 标签

用户已经经过认证/记住我登录后显示相应的信息。
Shiro入门
(3)authenticated标签

用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的。

Shiro入门
(4) notAuthenticated标签

用户未进行身份验证,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证。
Shiro入门
(5)pincipal标签

显示用户身份信息,默认调用Subject.getPrincipal() 获取,即Primary Principal。
Shiro入门
(6)hasRole标签

如果当前Subject有角色将显示body体内容。
Shiro入门
(7) hasAnyRoles标签

如果当前Subject有任意一个角色(或的关系)将显示body体内容。
Shiro入门
(8)lacksRole标签

如果当前Subject没有角色将显示body体内容。
Shiro入门
(9) hasPermission标签

如果当前Subject有权限将显示body体内容。
Shiro入门
(10) lacksPermission标签

如果当前Subject没有权限将显示body体内容。
Shiro入门
以下对部分标签进行测试,修改index.jsp页面,在页面标头先添加shiro的标签库:
Shiro入门
然后修改body里面的内容:
Shiro入门
这里稍微作了修改,现在是根据不同的角色(权限)来显示不同的链接。

重启项目然后访问,以admin + ycz123456登录进去:
Shiro入门
现在两个链接都显示出来了,说明admin用户拥有两个角色。然后退出,以yanchengzhi + ycz123456登录进去:
Shiro入门
现在只有一个链接,说明yanchengzhi用户只拥有一个角色。

6.4.7、Shiro权限注解

Shiro中提供了5种关于权限的注解。如下:

  • @RequiresAuthentication:表示当前Subject已经通过login进行了身份验证;即Subject. isAuthenticated()返回true。
  • @RequiresUser:表示当前Subject已经身份验证或者通过记住我登录的。
  • @RequiresGuest:表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
  • @RequiresRoles(value={“admin”, “user”},logical=Logical.AND):表示当前Subjec需要角色admin和user角色。
  • @RequiresPermissions (value={“user:a”, “user:b”},logical= Logical.OR):表示当前Subject需要权限 user:a 或user:b。

以下对权限注解进行测试。创建一个service包,在该包下创建ShiroService类,具体内容如下:

public class ShiroService {
	
	// 该方法只有拥有admin角色才能访问
	@RequiresRoles({"admin"})
	public void testMethod() {
		System.out.println("test-time:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
	}

}

在spring-context.xml中注入该Service的bean:
Shiro入门
在controller中进行调用:
Shiro入门
Shiro入门
可以看到在该controller的对应方法上加了@RequiresRoles注解,表明只有拥有admin角色才可以访问该方法。最后在index.jsp中加一个访问链接:
Shiro入门
重启项目,以admin + ycz123456登录进来:
Shiro入门
点击这个链接:
Shiro入门
可以跳转,再看控制台:
Shiro入门
打印出了当前时间,说明成功的访问到了这个方法。然后退出,以yanchengzhi + ycz123456登录进来:
Shiro入门
点击同样的链接:
Shiro入门
报错了,再看控制台:
Shiro入门
没有打印出当前时间,并且抛出了异常,说明没有访问到该方法,因为没有角色权限,根据异常的提示可以看到:当前主体没有该角色,抛出的是权限的异常。

6.4.8、初始化权限和资源

Shiro入门
前面的权限资源都是在spring-context.xml中写死的,实际中并不会这样,而是会将这些权限和资源存到数据库中,然后从数据库中查询出来,动态设置。

这里的配置实际上会走到ShiroFilterFactoryBean类的某个方法里,源码如下:
Shiro入门
Shiro入门
在setFilterChainDefinitionMap方法里打上断点,然后以debug的形式启动项目访问:
Shiro入门
可以看到,这里的LinkedHashMap里存的正是上面配置的权限信息。
Shiro入门
将这些权限的配置注释掉,新建一个factory包,包下创建FilterChainDefinitionMapBuilder类,内容如下:

/**
 * 工厂类
 * @author 17605
 *
 */
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", "roles[admin]");
		map.put("/yanchengzhi.jsp","roles[yanchengzhi]");
		map.put("/**", "authc");
		return map;
	}

}

这个类只有一个方法,返回一个LinkedHashMap对象,里面存放已经设置好的权限信息。

修改spring-context.xml:
Shiro入门
圈出的为添加的部分,将FilterChainDefinitionMapBuilder类注入IOC容器中,并利用该类的对象调用buildFilterChainDefinitionMap方法,返回一个LinkedHashMap对象,最后将这个返回对象作为属性替换原来的权限配置属性:
Shiro入门
重启项目并以admin + ycz123456进行登录:
Shiro入门
Shiro入门
登录成功。点击第一个链接:
Shiro入门
跳转成功,点击第二个链接:
Shiro入门
跳转成功,点击第三个链接:
Shiro入门
跳转成功,点击退出:
Shiro入门
功能都是正常的,资源可以正常访问。

6.5、会话管理

Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如web容器tomcat),不管JavaSE还是 JavaEE环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对Web的透明支持、SSO单点登录的支持等特性。

下面介绍Shiro中一些和会话相关的API:

  • Subject.getSession():即可获取会话;其等价于Subject.getSession(true),即如果当前没有创建Session对象会创建一个;Subject.getSession(false),如果当前没有创建Session则返回null。
  • session.getId():获取当前会话的唯一标识。
  • session.getHost():获取当前Subject的主机地址。
  • session.getTimeout() & session.setTimeout(毫秒):获取/设置当前Session的过期时间。
  • session.getStartTimestamp() & session.getLastAccessTime():获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是Web 应用,每次进入ShiroFilter都会自动调用session.touch()来更新最后访问时间。
  • session.touch() & session.stop():更新会话最后访问时间及销毁会话;当Subject.logout()时会自动调用stop方法来销毁会话。如果在web中,调用 HttpSession. invalidate()也会自动调用Shiro的Session.stop方法进行销毁Shiro的会话。
  • session.setAttribute(key, val)&session.getAttribute(key)&session.removeAttribute(key):设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作。

6.5.1、Shiro中的Session对象

Shiro中提供了SessionListener接口来进行会话的管理。源码如下:
Shiro入门
提供了3个方法,onStart会话开始,onStop会话停止,onExpiration会话过期。类似于HttpSessionListener:
Shiro入门
下面通过代码测试Shiro中Session的使用。

修改ShiroHandler:
Shiro入门
在该方法中使用了HttpSession往Session域里存了一个K/V对。然后在ShiroService中:
Shiro入门
在Service层里,通过Shiro的SecurityUtils类获取到了Session对象,然后从Session域中取出存的key值。重启项目然后以admin + ycz123456进行登录:
Shiro入门
点击这个链接访问方法:
Shiro入门
成功跳转了。控制台:
Shiro入门
成功的在Service层里拿到了Session中的信息,这正是使用Shiro中提供的Session的好处,可以在服务层获取到Session的内容,这在开发中是十分方便且有用的。

6.5.2、SessionDao组件

Shiro中提供的SessionDao组件用于将Session信息持久化到本地,比如数据库中。先来看看SessionDao的层级关系:
Shiro入门
Shiro入门
说明如下:

  • SessionDao:层级关系中的最*接口,只有5个抽象方法。源码如下:
    Shiro入门
    Shiro入门
    Shiro入门
    Shiro入门
    create(Session session)方法用于创建一个Session对象的Serializable序列化对象,readSession(Serializable sessionId)方法用于反序列化Session信息的读取,update(Session session)用于Session信息的更新,delete(Session session)删除Session信息,getActiveSessions()获取所有有效的Session对象。
  • AbstractSessionDAO:提供了SessionDAO的基础实现,如生成会话ID等。部分源码如下:
    Shiro入门
    Shiro入门
  • CachingSessionDAO:提供了对开发者透明的会话缓存的功能,需要设置相应的CacheManager。部分源码如下:
    Shiro入门
  • MemorySessionDAO:直接在内存中进行会话维护。
  • EnterpriseCacheSessionDAO:提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话,我们一般会直接继承这个类重写某些方法来具体实现。这个类的源码如下:
    Shiro入门
    下面使用代码测试Shiro中Session的读写操作。

(1)创建表

先在mysql中创建sessions表用于保存即将持久化的Session信息,DDL如下:

CREATE TABLE `sessions` (
  `id` varchar(50) NOT NULL,
  `session` varchar(2550) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(2)工具类SerializableUtils

需要先创建一个工具类来对Session对象进行序列化和反序列化。创建util包,包下创建一个SerializableUtils类,具体内容如下:

package com.ycz.shiro.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.Session;

/**
 * Session序列化工具类
 * @author 17605
 *
 */
public class SerializableUtils {
	
	/**
	 * 序列化
	 * @param session
	 * @return
	 */
	public static String doSerializable(Session session) {
		try {
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(baos);
			oos.writeObject(session);
			String sessionStr = Base64.encodeToString(baos.toByteArray());
			return sessionStr;
		} catch (Exception e) {
			throw new RuntimeException("序列化Session失败!",e);
		}
	}
	
	/**
	 * 反序列化
	 * @param sessionStr
	 * @return
	 */
	public static Session reSerializable(String sessionStr) {
		try {
			byte[] bCode = Base64.decode(sessionStr);
			ByteArrayInputStream bais = new ByteArrayInputStream(bCode);
			ObjectInputStream ois = new ObjectInputStream(bais);
			Session session = (Session) ois.readObject();
			return session;
		} catch (Exception e) {
			throw new RuntimeException("反序列化失败!",e);
		}
	}

}

(3)自定义SessionDao

创建一个dao包,包下新建MySessionDao类,此类继承SessionDao接口的最底层子类EnterpriseCacheSessionDAO,具体内容如下:

package com.ycz.shiro.dao;

import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import com.ycz.shiro.util.SerializableUtils;

/**
 * 自定义SessionDao的实现,只需继承最底层子类EnterpriseCacheSessionDAO
 * 重写某些方法即可
 * @author 17605
 *
 */
@Component
public class MySessionDao extends EnterpriseCacheSessionDAO {
	

	@Autowired
	private JdbcTemplate jdbcTemplate;

	// 添加Session
	@Override
	protected Serializable doCreate(Session session) {
		Serializable sessionId = generateSessionId(session);
		assignSessionId(session, sessionId);
		String sql = "insert into sessions(id,session,create_time) values (?,?,?)";
		int res = jdbcTemplate.update(sql, sessionId, SerializableUtils.doSerializable(session),
				new Timestamp(new Date().getTime()));
		if (res > 0) {
			System.out.println("session写入成功!");
			return session.getId();
		}
		return null;
	}

	// 读取Session
	@Override
	protected Session doReadSession(Serializable sessionId) {
		String sql = "select session from sessions where id=?";
		List<String> sessionList = jdbcTemplate.queryForList(sql, String.class, sessionId);
		if (sessionList.size() == 0)
			return null;
		System.out.println("sessoin值=====>" + sessionList.get(0));
		return SerializableUtils.reSerializable(sessionList.get(0));
	}

	// 更新Session
	@Override
	protected void doUpdate(Session session) {
		if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
			return;
		}
		String sql = "update sessions set session = ? where id = ?";
		Serializable sessionId = session.getId();
		int res = jdbcTemplate.update(sql, SerializableUtils.doSerializable(session), sessionId);
		if (res > 0) {
			System.out.println("session已更新!");
		}
	}

	// session删除
	@Override
	protected void doDelete(Session session) {
		String sql = "delete from sessions where id = ?";
		int res = jdbcTemplate.update(sql, session.getId());
		if (res > 0)
			System.out.println("session已过期!");
	}

}

(4)修改缓存配置ehcache.xml

在ehcache.xml中配置shiro的缓存:

    <!-- 配置Session的缓存 -->
    <cache name="shiro-activeSessionCache"
           eternal="false"
           diskPersistent="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

(5)spring-context.xml配置

在spring-context.xml中添加对Session的管理配置,需要配置一个SessionManager,如下:

	<!-- 注入sessionId生成器 -->
	<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"></bean>
	
	<!-- 注入自定义SessionDao组件 -->
	<bean id="sessionDAO" class="com.ycz.shiro.dao.MySessionDao">
	   <property name="activeSessionsCacheName" value="shiro-activeSessionCache"></property>
	   <property name="sessionIdGenerator" ref="sessionIdGenerator"></property>
	</bean>
	
	<!-- 注入SimpleCookie组件 -->
	<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
	   <constructor-arg value="shiro-session-id"/> 
	   <property name="httpOnly" value="true"/> 
	   <property name="maxAge" value="-1"/> 
	</bean>
	
	<!-- 会话管理器 -->
	<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
	   <property name="globalSessionTimeout" value="1800000"></property>
	   <property name="deleteInvalidSessions" value="true"></property>
	   <property name="sessionValidationSchedulerEnabled" value="true"></property>
	   <property name="sessionDAO" ref="sessionDAO"></property> 
	   <property name="sessionIdCookie" ref="sessionIdCookie"/>
	   <property name="sessionIdCookieEnabled" value="true"/>
	</bean>

最后需要将这个sessionManager作为属性配置给securityManager:
Shiro入门
还需要配置一下数据源信息准备往数据库中写入数据:

	<!-- 配置数据源 -->
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
	   <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=GMT%2B8"></property>
	   <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
	   <property name="user" value="root"></property>
	   <property name="password" value="ycz123456"></property>
	</bean>
	
	<!-- 注入jdbcTemplate模板 -->
	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
	    <constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
	</bean>
	

(6)测试

重启项目并访问:
Shiro入门
控制台:
Shiro入门
写入和更新Session信息成功,这里为什么调用更新方法3次我没搞明白。然后看数据库表:
Shiro入门
成功保存了Session信息。然后以admin + ycz123456登录进去:
Shiro入门
控制台:
Shiro入门
再查看表:
Shiro入门
实际上这个session字段的超长文本是更新了的。最后点击退出链接:
Shiro入门
控制台:
Shiro入门
可以看到这里是调用了删除方法删除了原本的Session信息,然后又重新写入并且做了更新操作的,再查看表:
Shiro入门
这条记录是新的,并且原来的记录删除掉了。

6.5.3、会话验证

Shiro提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话。出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在web环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro提供了会话验证调度器SessionValidationScheduler。先加入quartz的jar包:
Shiro入门
这里注意包的版本,如果是2.x版本的话,会报错。然后在spring-context.xml中添加对此的配置:
Shiro入门
重启项目并访问:
Shiro入门
Shiro入门
以admin + ycz123456登录进去:
Shiro入门
可以看到是没问题的。

6.6、缓存

Shiro内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了
CacheManagerAware并自动注入相应的CacheManager。
Shiro入门
Shiro入门
Shiro入门
Shiro入门
可以看到通过继承关系,ShiroRealm最终是实现了CacheManagerAware接口的。而AuthenticatingRealm及AuthorizingRealm也分别提供了对AuthenticationInfo和AuthorizationInfo信息的缓存。

关于Session的缓存

如SecurityManager实现了SessionSecurityManager,其会判断SessionManager是否实现了
CacheManagerAware接口,如果实现了会把CacheManager设置给它。SessionManager也会判断相应的SessionDAO(如继承自CachingSessionDAO)是否实现了CacheManagerAware,如果实现了,会把CacheManager设置给它。 设置了缓存的SessionManager,查询时会先查缓存,如果找不到才查数据库。

6.7、记住我RememberMe

Shiro提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问。

基本流程如下:

  • 首先在登录页面选中RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来。
  • 关闭浏览器再重新打开;会发现浏览器还是记住你的。
  • 访问一般的网页,服务器端还是知道你是谁,且能正常访问。
  • 当我们访问敏感信息时,比如访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。

区分认证和记住我

  • subject.isAuthenticated():表示用户进行了身份验证登录的,即是由Subject.login进行了登录。
  • subject.isRemembered():表示用户是通过记住我登录的,此时可能并不是真正的你(如你的朋友使用你的电脑,或者你的cookie被窃取)在访问的。
  • 两者二选一,即如果subject.isAuthenticated()==true,则subject.isRemembered()==false;反之一样。

使用user拦截器还是authc拦截器

  • 访问一般网页:如个人主页之类的,我们使用user拦截器即可,user拦截器只要用户登录
    (isRemembered() || isAuthenticated())过即可访问成功。
  • 访问特殊网页:如我的订单,提交订单页面,我们使用authc拦截器即可,authc拦截器会判断用户是否是通过Subject.login(isAuthenticated()==true)登录的,如果是才放行,否则会跳转到登录页面叫你重新登录。

关于Shiro中各种拦截器的说明,如下图:
Shiro入门
以下用代码实现rememberMe的功能。

修改login.jsp登录页面,添加rememberMe选项,一般是checkbox复选框:
Shiro入门
修改ShiroHandler中的登录方法:
Shiro入门
登录方法里加一个参数来接收前端传过来的rememberMe的值,以此判断用户是否使用了记住我功能,如果使用了,则调用UsernamePasswordToken的setRememberMe方法,将参数设为true(默认是false)。

修改ShiroFilter里路径的拦截器配置:
Shiro入门
将主页面index.jsp的拦截器配为了user,即可以通过认证/记住我访问。其他两个页面加了authc拦截器,即只能通过认证来进行访问。

修改spring-context.xml中securityManager配置,添加rememberMe的过期时间:
Shiro入门
可以看到,通过修改rememberMeManager类的cookie对象的maxAge属性值,来修改过期时间,过期时间的单位以秒(s)计,这里设为了30秒。

重启项目并访问:
Shiro入门
暂时不勾选复选框,点击提交:
Shiro入门
登录成功,控制台:
Shiro入门
可以看到这里的属性值是false的,即用户没有使用记住我功能。关闭浏览器,直接访问http://localhost:8081/shiro-1/index.jsp:
Shiro入门
Shiro入门
重定向到了登录页面。

重新登录,这次勾选记住我:
Shiro入门
点击提交:
Shiro入门
登录成功,控制台:
Shiro入门
可以看到现在这里的rememberMe属性值是true的,说明用户使用了记住我功能,但是只有30秒的时间,关闭浏览器,然后在30秒内直接访问http://localhost:8081/shiro-1/index.jsp:
Shiro入门
Shiro入门
可以直接访问该页面,因为该页面使用的是user拦截器,可以通过记住我来访问。关闭浏览器,等30秒,再直接访问:
Shiro入门
Shiro入门
这时不能直接访问,直接重定向到了登录页面,因为有效期30秒已经过了,现在的Cookie过期了,需要重新登录才能访问。

7、小结

其实这篇文章是在看了尚硅谷的Shiro视频之后写的知识总结,算是跟着学习吧。虽然是跟着别人敲代码,但到了自己敲的时候还是遇到了许多坑,而且有的东西视频里的老师并没有讲到,在遇到问题时也只能百度查资料解决,所幸最后项目还是可以跑得起来。下一步是学怎么在SpringBoot中集成Shiro框架。

上一篇:shiro中文api_Shiro


下一篇:权限业务整理——仿若依