Ehcache + Shiro Demo 线上展示地址:http://eh.itboy.net/shiro.ehcache/  

帐号:admin ,密码:sojson.com  ,尽量不要动管理员的数据。20分钟数据会重新生成。

一、Shiro简介

Apache Shiro 是 Java  的一个安全框架。我们经常看到它被拿来和 Spring  Security  来对比。大部分人认为 Shiro  Security  要简单。我的观点赞成一半一半吧。

首先 Shiro  确实和 Security  是同类型的框架,主要用来做安全,也就是我们俗称的权限校验(控制)。居多人对 Shrio  的定义为好入门。

我选型为 Shiro  ,主要的原因扩展太easy了,而且我要的功能它都有。

二、概述

前段时间出了一个基于SSM(SpringMVC + Spring + Mybatis)的Shiro 教程Demo,Cache(Nosql)是基于Redis的,但是很多同学卡在了Redis上,经常运行起来。所以现在出一版本基于  Cache  为Ehcache版本的。这样减少新入门的同学的难度,不用依赖第三方中间件。

后续会陆陆续续添加N多相关的功能。以不同版本的方式发布。

基于Redis版本地址:http://www.sojson.com/shiro

三、需要你的赞助

如果帮助到了您,请你在下载代码后,运行后,跑起来后,加群帮你解决问题后,兴奋、喜悦、愤怒、丧气、不知所措... ... 的时候,请赞助我,钱多少不重要,学生请不要赞助(富二代请忽略)。

赞助链接:http://www.sojson.com/other/subsidize.html

四、请遵循三要素

  1. 建议你看完本篇文档所有内容,再进行运行项目。
  2. 在没熟练之前,除了必要的配置修改,请勿改动任何配置和包路径。
  3. 有疑问先看文档,交流加QQ群:259217951 即可,群里没人理你@群主即可。

五、环境准备

5.1 开发工具

  Eclipse  、  MyEclipes  、Idea 等  Java  开发工具,推荐使用MyEclipes8.5以上。因为这个项目是在MyEclipes8.5MyEclipes10.7 开发的。

如果使用Eclipse的同学,请安装好Maven环境,如果没有Maven环境,又不想安装,那么请在附件中下载依赖包,自己把项目转成Java Web项目然后进行运行。

如果使用Idea 作为开发工具的同学,注意配置resources ,还有一些其他的配置需要自己处理,不像导入  MyEclipes  直接能跑起来。

5.2 依赖第三方

因为是基于  Ehcache  ,所以也没有其他第三方。

主要就是一个  Mysql  数据库。数据库的版本为 Mysql5.6  ,估计Mysql5.5 Mysql5.6 都没问题,Mysql5.7 有小小问题,因为一些默认配置导致有些语法可能不支持,这个慎用。

下面有最新,我测试可用的Mysql 安装教程:Mysql5.6下载安装,Mysql5.7下载安装 ,Windows64位,绿色安装(解压缩安装)图文安装教程

如果安装出现Mysql 权限问题:ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) 解决。

六、环境配置及要求,JDK版本,初始化配置

6.1 JDK版本要求

  JDK  版本要求为JDK1.7+ ,我开发的版本是1.7.0_80 JDK1.6 有些jar 包会报错,推荐使用JDK1.7 以上。

如果出现以下类似错误,那就是JDK版本不相符。

Unsupported major.minor version 51.0 

其中各个版本对应的提示如下:

JDK1.5  对应为Unsupported major.minor version 49.0
JDK1.6  对应为Unsupported major.minor version 50.0
JDK1.7  对应为Unsupported major.minor version 51.0
JDK1.8  对应为Unsupported major.minor version 52.0

6.2 初始化配置

6.2.1 Mysql数据库初始化

本教程不支持自动创建表和插入数据,在项目的init/sql 下有三个sql 文件,分别为:tables.sql (插入表)、init.data.sql (插入初始化数据)、init_shiro_demo.sql (插入初始化存储过程)。

执行的过程为: tables.sql (插入表)===> init.data.sql (插入初始化数据)就可以了。

存储过程可以是定时任务 com.sojson.common.timer.ToTimer 中定时任务调用的存储过程。每20分钟一次。想看看效果的同学,可以把spring.xml 配置文件中的spring-timer.xml 注释打开就可以。

数据库配置:jdbc.properties 配置你的数据库链接。

jdbc.url=jdbc:mysql://localhost:3306/shiro.demo
jdbc.username=root
jdbc.password=123456

具体 druid 配置:Druid数据库配置详细介绍

其他配置默认的即可,先跑起来,跑起来没问题后,看看配置文件,有问题和疑问在群里交流。

PS:如果你确实要用Mysql5.7,那么在Mysql的安装目录下找到。my.ini 或者my_default.ini 里面配置sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES 即可。

6.3 Maven环境说明

本教程是用  Maven  管理Jar包及运行打包,如果你发现  Maven  一直在下载jar 包,时间过久的话,建议你换成阿里的数据源。打开你的  Maven  目录的setting.xml 文件,如果没有直接添加即可,官方群里有setting.xml 文件作为参考。

主要改个本地Maven 目录,改成你自己的目录即可:

<localRepository>E:\maven\repository</localRepository>

再配置一个mirror,找到标签为mirrors,然后在里面添加或者修改为阿里的Maven库,如下图:

  <mirrors>
	<mirror>
		 <id>alimaven</id>
		 <name>aliyun maven</name>
		 <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
		 <mirrorOf>central</mirrorOf>        
	</mirror>
  </mirrors>

Maven的Mirror和Repository 的详细讲解 ,可以详细了解下。

异常解决

常见的异常有3种。

  1. 如果有部分包打红× ,可以删除这个包路径,再次项目右键 Maven选项(每个工具不一样) ==> Update Maven Dependencies 更新即可。
  2. 如果下载好Jar 包,也不报错,但是在运行项目的时候,报错zip 相关异常,那么删除Maven 目录下所有下好的jar 包,然后再来下载一次。因为是下载的包是损坏的。
  3. 还有   Maven  的  JDK  版本需要和你项目一致,有的工具默认配置是JDK1.5

6.4 其他说明

编码格式:UTF-8 ,换成其他编码格式可能会有瑕疵。

Spring相关Jar版本为:Spring 4.2.5

前端页面采用:Bootstarp 3.2 

其他依赖:jQuery1.8.3   、layer 控件。

6.5 View层说明

http://www.sojson.com/shiro 之前的Shiro Demo 是  JSP  和  Freemarker  混合模版,从这个Shiro Demo开始,我只用  Freemarker  ,后期也会出  Freemarker  视频教程。当然为了习惯(只会)JSP的同学,我也双模版配置不修改,您想改成JSP可以直接支持,JSP是以WEB-INF下的view目录为根目录。

七、教程功能详细说明

下面各点是针对(SSM)SpringMvc + Spring + Mybatis框架说明,以及一些使用方式和基本功能介绍。

7.1.1 框架基本介绍

本教程是SSM(  SpringMVC  +Spring  +   Mybatis  +   Freemarker  )  +   Ehcache  做的整体Shiro Demo ,其他框架需要自己自行解决,所以不做框架 其他 的讲解,其实是大同小异。

7.1.2 分页介绍

本框架里的分页比较Low,分页的ServiceImpl 要继承 BaseMybatisDao<T>  ,这里泛型的<T> 为当前实体对象对应的Mapper.xml 文件,其实就是Mapper.xml namespace 。调用父类的findPage 相关。

Service Impl Java 代码:

public Pagination<UserRoleAllocationBo> findUserAndRole(ModelMap modelMap,
		Integer pageNo, Integer pageSize) {
	//findUserAndRole : 为查询数据(sqlID)
	//findCount	  : 为查询符合数据的总条数(sqlID)
	return super.findPage("findUserAndRole", "findCount", modelMap, pageNo, pageSize);
}

分页查询使用缺省的sqlId

public Pagination<UPermission> findPage(Map<String,Object> resultMap, Integer pageNo,
		Integer pageSize) {
	/**
	 * 调用父类的分页查询,默认数据查询sqlId = findAll , count 查询sqlId = findCount
	 */
	return super.findPage(resultMap, pageNo, pageSize);
}

Mapper.xml 文件 Sql 代码,和上面一 一对应:

 <select id="findCount" resultMap="BaseResultMap" >
	 select count(id) from  u_user
 	<include refid="where_all" />
 </select>
 
 <!-- 用户权限分配的分页查询 -->
 <select id="findUserAndRole" resultType="com.sojson.permission.bo.UserRoleAllocationBo">
 	select u.id,u.nickname,u.email,u.create_time,u.last_login_time,u.status ,group_concat(ur.name) roleNames,group_concat(ur.id)roleIds from
 u_user u
left join u_user_role uur on uur.uid = u.id
left join u_role ur on ur.id = uur.rid
<where>
 	<if test="findContent != null and findContent !='' " >
       and (
       LOWER(u.nickname) like  LOWER(CONCAT("%",#{findContent,jdbcType=VARCHAR},"%")) or
       LOWER(u.email) like  LOWER(CONCAT("%",#{findContent,jdbcType=VARCHAR},"%"))
       )
     </if>
     </where>
group by u.id 
 </select>
<select id="findAll" resultMap="BaseResultMap" >
	 select 
  <include refid="Base_Column_List" />
  from u_permission 
  <include refid="where_all"/>
  <include refid="limit_sql" />
</select>
<select id="findCount" resultMap="BaseResultMap" >
 select count(id) from  u_permission
	<include refid="where_all" />
</select>

具体请结合代码,Debug 断点看看。

前端HTML页面输出:

<#if page?exists>
	<div class="pagination pull-right">
		${page.pageHtml}
	</div>
</#if>

Java 分页处理代码:

public String getPageHtml(){
	Map<String,Object> map = new HashMap<String,Object>();
	map.put("pageNo", this.getPageNo());
	map.put("totalPage", this.getTotalPage());
	//从Freemarker 获取分页html
	return new Freemarker().getTemplate("common/page/html_page.ftl", map);
}

具体分页输出,请看项目中的Freemarker.getTemplate(...) 方法,上个项目是直接拼串输出,这次这里作为一个Demo 处理了下,采用Freemarker 模版输出。

7.1.3 基本结构,以及事务控制

Controller ==> Service (事务控制层) ==> Dao ==> SqlMapper ==>   Mysql  数据库。

对于事务AOP 配置和之前的版本是一致的。事务配置在spring-mybatis.xml 配置文件中。

<tx:advice id="txAdvice" transaction-manager="transactionManager">
	<tx:attributes>
		<tx:method name="publish*" />
		<tx:method name="save*" />
		<tx:method name="add*" />
		<tx:method name="update*" />
		<tx:method name="insert*" />
		<tx:method name="create*" />
		<tx:method name="del*" />
		<tx:method name="load*" />
		<tx:method name="init*" />
		<tx:method name="*"  read-only="true"/>
	</tx:attributes>
</tx:advice>
<!-- AOP配置--> 
<aop:config>
	<aop:pointcut id="myPointcut"
		expression="execution(public * com.sojson.*.service.impl.*.*(..))" />
	<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut" />
</aop:config>

事务控制层在ServiceImpl 层。public * com.sojson.*.service.impl.*.*(..)  的意思是, com.sojson.*.service.impl 下所有类的所有方法,而且方法参数不限。符合这些条件的加入事务。

事务的规则是通过方法名前缀来定义的。上面定义了常用的publish save add ....诸如此类,是走默认事务级别。匹配不中的走readOnly。

所以此处事务配置要注意,如果你修改包路径,这里也要修改,并且要符合规则,要不然会出现找不到数据库的Connection而报错。并且会造成事务无效。

7.1.4 View层Freemarker 配置

View层配置为通用配置,支持  Freemarker  和  JSP  配置,具体配置参考  SpringMvc  配置spring-mvc.xml 

<!--===============通用视图解析器 begin===============-->
	<bean id="viewResolverCommon"
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/WEB-INF/views/" />
		<property name="suffix" value=".jsp" />
<!--			可为空,方便实现自已的依据扩展名来选择视图解释类的逻辑-->
	<property name="viewClass">
		<value>org.springframework.web.servlet.view.InternalResourceView
		</value>
	</property>
	<property name="order" value="1" />
</bean>

<!-- 视图解析器 -->
<!-- 配置freeMarker视图解析器 -->
<bean id="viewResolverFtl"
	class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
	<property name="viewClass" value="com.sojson.core.freemarker.extend.FreeMarkerViewExtend" />
	<!-- 把Freemarker 扩展一下,把相关属性加入进去。。。 -->
	<property name="contentType" value="text/html; charset=utf-8" />
	<property name="cache" value="true" />
	<property name="suffix" value=".ftl" />
	<property name="order" value="0" />
</bean>
<bean id="viewResolver"
	class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<property name="order" value="2"></property>
	<property name="viewClass"
		value="org.springframework.web.servlet.view.JstlView" />
	<property name="prefix" value="/WEB-INF/views/" />
	<property name="suffix" value=".jsp"></property>
</bean>

<!-- 配置freeMarker 拓展-->
<bean id="freemarkerConfig"
	class="com.sojson.core.freemarker.extend.FreeMarkerConfigExtend">
	<property name="templateLoaderPath">
		<value>/WEB-INF/ftl/</value>
	</property>
	<property name="freemarkerVariables">
		<map>
			<entry key="xml_escape" value-ref="fmXmlEscape" />
			<entry key="api" value-ref="api"/>
		</map>
	</property>
	<property name="defaultEncoding">
		<value>utf-8</value>
	</property>
	<property name="freemarkerSettings">
		<props><!-- 315360000 -->
			<prop key="template_update_delay">0</prop><!-- Freemarker 模版缓存时间。开发环境不用开启,单位毫秒 -->
			<prop key="defaultEncoding">UTF-8</prop>
			<prop key="url_escaping_charset">UTF-8</prop>
			<prop key="locale">zh_CN</prop>
			<prop key="boolean_format">true,false</prop>
			<prop key="datetime_format">yyyy-MM-dd HH:mm:ss</prop>
			<prop key="date_format">yyyy-MM-dd</prop>
			<prop key="time_format">HH:mm:ss</prop>
<!--			<prop key="number_format">0.######</prop>-->
			<prop key="number_format">#</prop>
			<prop key="whitespace_stripping">true</prop>
			<prop key="auto_import">
				<!-- Freemarker macro 文件应用 -->
				/common/config/top.ftl as _top,
				/common/config/left.ftl as _left,
				/common/config/menu.ftl as _menu
			</prop>
		</props>
	</property>
</bean>
<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>

7.1.5 Freemarker API标签封装

采用  Freemarker  的自定义标签功能封装得到使用起来更为简单的方式,下面来一下实列代码。这样的优点,它不是  HTTP  、  TCP  之类的请求,它是Freemarker 在后台组装的时候调用方法而已。这样有利于业务模块抽取和区分。

spring-mvc.xml 里有如下自动扫描tag 标签类代码:

<!-- 自动扫描 标签 -->
<context:component-scan base-package="com.sojson.*.*.tag;com.sojson.*.tag" />
<!-- api 标签功能主类 -->
<bean name="api" class="com.sojson.core.tags.APITemplateModel"></bean>

  Java  后台代码:

/**
 * 测试封装的 Freemarker 标签代码 。 
 */
@Component
public class UserInfoTag extends SuperCustomTag {
	//支持注入
	@Autowired
	UUserService userService;
	/**
	 * 继承父类的方法,必须实现,返回类型为Object,方便使用,这样就可以返回Map、List、以及自己的JavaBean 
	 */
	@Override
	protected Object result(Map params) {
		Long userId = getLong(params, "userId");//从页面取得userId
		return userService.selectByPrimaryKey(userId);
	}
}

  Freemarker  前端使用:

<#--
参数说明:
	target:目标tag类,就是上述Java代码中的 com.sojson.user.tag.UserInfoTag 类名首字母小写得到的Tag。
	userId:自定义,这样可以定义多个,后台直接从Map获取即可。你可以 a="1",b="2",c="true",诸如此类。
	返回值:{outTagName:你的返回值}
-->
<@api target="userInfoTag" userId="1">
	<#--
		 后台返回值接收的Key为:outTagName  
		Key名称修改的话在 com.sojson.core.statics.Constant.OUT_TAG_NAME
	-->
	<#if outTagName?exists>
			nickname:${outTagName.nickname}
			email:${outTagName.email}
			最后登录时间:${outTagName.lastLoginTime?string('yyyy-MM-dd HH:mm:ss')}
	</#if>
</@api>

7.1.6 多种验证码集成

当我们在注册的时候,需要输入验证码,现在默认配置的动态(gif )格式验证码。Shiro Demo 的注册页面即可查看。

项目中package:com.sojson.common.utils.vcode 包是验证码的封装包。

并且提供了一个VerifyCodeUtils.java 的验证码工具类。

使用方法参见:CommonController.java 类中的getVCode() 方法和getGifCode() 方法。

 Java生成验证码合集(一)简单版

 Java生成验证码合集(二)GJF版

八、Shiro Demo中Shro功能相关详细说明

8.1 Shiro + Ehcache 集成

上一个ShiroDemo 采用的Redis,篇前也说了,部分同学对Redis还是比较陌生,所以才有这一版本的出现。

首先Ehcache采用的是2.5版本。Maven引入jar。

<!-- shiro + ehcache jar -->
<dependency>
	<artifactId>ehcache-core</artifactId>
	<groupId>net.sf.ehcache</groupId>
	<version>2.5.0</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-ehcache</artifactId>
	<version>1.2.2</version>
</dependency>
<!-- shiro end -->

Shiro-ehcache ,其实我偏向自己实现比较好管理。  Ehcache  配置文件在resources 目录下发现有2 Ehcache 相关的config 文件。及ehcache-config.xml(本项目中的Vcache所使用)ehcache-shiro.xml (Shiro Auth相关权限,User Info使用)。

如果你仔细看发现Vcache 缓存工具类,使用的是  Ehcache  的路线,而  Shiro  使用的是Shiro-ehcache 路线,而他们又是同一种缓存,在结合的时候,我碰了几次墙。主要是  cache  的name 不能一样或是没有。

8.2 Shiro 系统级别权限初始加载

为了更详细的介绍,我写了一篇专门介绍这个权限配置加载的相关问题==> Shiro教程,Shiro 配置文件详细解释,Shiro自定义Filter配置,建议现行看完。

  Shiro  权限配置一般使用的有两种,一种是采用注解的方式,在我们的  Controller  方法上,或者Action 方法上写入一些权限判断注解,具体怎么使用,我不做介绍,我主要推荐使用配置的方式。这也是我们现在要讲到的配置方式加载系统基础权限控制,采用对Url 进行控制。

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    	<property name="securityManager" ref="securityManager" />
    	<property name="loginUrl" value="/u/login.shtml" />
    	<property name="successUrl" value="/" />
    	<property name="unauthorizedUrl" value="/?login" />
    	
    	<!--	基本系统级别权限配置-->
    	<property name="filterChainDefinitions" >
    		<value>
    			/page/login.jsp = anon	<!-- 登录相关不拦截 -->
    			/page/register/* = anon
    			/page/index.jsp = authc
    			/page/addItem* = authc,roles[数据管理员]
    			/page/file* = authc,roleOR[普通用户,数据管理员]
    			/page/listItems* = authc,roleOR[数据管理员,普通用户]
    			/page/showItem* = authc,roleOR[数据管理员,普通用户]
    			/page/updateItem*=authc,roles[数据管理员]
    			/** = anon <!-- 其他不拦截 -->
               </value>
    	</property>
    	<!-- 自定义shiro 的 Filter -->
          <property name="filters">
              <util:map>
                 <entry key="login" value-ref="login"></entry>
              </util:map>
          </property>
    </bean>

通常采用以上配置,不过本教程采用动态加载方式,就是从数据库获取,从配置文件获取等方式,也就是你可以把权限存储一部分放在数据库,一部分存储到配置文件,然后修改更新。本教程是如下方式加载(粘贴代码去掉“\” )。

<property name="filterChainDefinitions" value="#\{shiroManager.loadFilterChainDefinitions()\}"/>

配置文件加载相关看这里:

1. Shiro教程(十)Shiro 权限动态加载与配置精细讲解。

2. Shiro教程,Shiro 配置文件详细解释,Shiro自定义Filter配置

3. Java有序读取配置文件,有序读取ini配置文件  (这个我为什么放在这里说呢,因为好多人都不知道我们平常用的*.properties 配置是无序的,而我们现在加载要有序)

8.3 会话Session 相关

8.3.1 会话Session ID生成器

这个Session ID  生成器,在当前Demo 里采用的是  UUID  去掉“-” ,其实我们平常用的也是  UUID  。如果你想重写继承org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator 然后重写即可,我也重写了,只是做了一下小改动,就是把  UUID  的“-” 去掉了。

public class SOSJONSessionIdGenerator extends JavaUuidSessionIdGenerator {
	public final static String SID_KEY = "uid:%s:%s";
	@Override
	public Serializable generateId(Session session) {
		//这里只是做一个小小的演示,具体功能你可以自己丰富。
		String sid = super.generateId(session).toString();
		return sid.replaceAll("-", "");
	}
}

8.3.2 会话Cookie模板

这里重要的一点是domain属性,它是配置Session的Cookie存储的域,如果不配置默认存储当前域,如果你是域名方式,最好是配置。掌握了这一点,在相同一级域名下做单点登录就好做多了。原理其实很简单。

如下配置,我把domain 配置为.sojson.com ,意思为不管是sojson.com 下的几级域名都对一级域名下有读写权限,换种说法不管是www.sojson.com admin.sojson.com user.admin.sojson.com  都可以获取到这个  Cookie  值,懂了吗?

<!-- 会话Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
	<!--cookie的name,我故意取名叫xxxxbaidu -->
    <constructor-arg value="sid_demo"/>
    <property name="httpOnly" value="true"/>
    <!--cookie的有效时间 -->
    <property name="maxAge" value="-1"/>
    <!-- 配置存储Session Cookie的domain为 一级域名
    <property name="domain" value=".sojson.com"/>
     -->
</bean>

如果对一级域名、二级域名等不了解,请看这篇介绍:单个项目多个二级域名简单实现思路

Shiro简单解决多个二级域名的单点登录,请看这里详细介绍:Shiro 通过配置Cookie 解决多个二级域名的单点登录问题

自主实现CAS(单点登录)思路设计:N多系统单点登录,实现、解决方案。四种解决方案

8.4 Shiro Remember Me(记住我)

当我们经常遇到一些网站,在登录的时候有一个勾选(checkbox )记住我选项,当你勾选后一段时间再次访问,你还是登录状态,感觉有点神奇,当然在你技术一定的积累上,你自己也可以实现,我也曾经实现过,还是总是不那么完美。Shiro Remember Me 就是这个功能。

Remember Me Cookie配置:

<!-- 用户信息记住我功能的相关配置 -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg value="rememberMe"/>
    <property name="httpOnly" value="true"/>
    <!-- 配置存储rememberMe Cookie的domain为 一级域名		这里如果配置需要和Session回话一致更好。
    <property name="domain" value=".sojson.com"/>
     -->
    <property name="maxAge" value="2592000"/><!-- 30天时间,记住我30天 -->
</bean>

记住上面的一个注释:配置存储rememberMe Cookie domain 为 一级域名, 这里如果配置需要和Session回话一致更好

如此配置后,在Java Login 的时候,设置Remember Me true 就会有此效果。

ShiroToken token = new ShiroToken(user.getEmail(), user.getPswd());
//这里如果是true,下次会记住登录,有效时间是在spring-shiro.xml中的rememberMeCookie配置中的有效期。
token.setRememberMe(rememberMe);
SecurityUtils.getSubject().login(token);

Remember Me 加密Key配置:

<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
    <!-- rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)-->
    <property name="cipherKey"
              value="#{T(org.apache.shiro.codec.Base64).decode('3AvVhmFLUs0KTA3Kprsdag==')}"/>
    <property name="cookie" ref="rememberMeCookie"/>
</bean>

8.5 Shiro Session 配置

我们看过Shiro Session(org.apache.shiro.session)源码的同学返现一个情况,Shiro的Session和HttpSession没有关系。实际Shiro 是用Session 代理了HttpSession。

而对Session管理配置文件中配置较多,请看源代码比较妥当,基本都有注释:

<!-- custom shiro session listener -->
<bean id="customShiroSessionDAO" class="com.sojson.core.shiro.CustomShiroSessionDAO">
    <property name="shiroSessionRepository" ref="sessionShiroCacheManager"/>
    <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
</bean>
<!-- 手动操作Session,管理Session -->
<bean id="customSessionManager" class="com.sojson.core.shiro.session.CustomSessionManager">
	<property name="shiroSessionRepository" ref="sessionShiroCacheManager"/>
	 <property name="customShiroSessionDAO" ref="customShiroSessionDAO"/>
</bean>

<!-- Session Manager -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
	<!-- 相隔多久检查一次session的有效性   -->
 	<property name="sessionValidationInterval" value="1800000"/>  
 	 <!-- session 有效时间为半小时 (毫秒单位)-->  
	<property name="globalSessionTimeout" value="1800000"/>
   	<property name="sessionDAO" ref="customShiroSessionDAO"/>
   	<!-- session 监听,可以多个。 -->
   	<property name="sessionListeners">
       <list>
           <ref bean="customSessionListener"/>
       </list>
  	</property>
  	<!-- 间隔多少时间检查,不配置是60分钟 -->	
	<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
	<!-- 是否开启 检测,默认开启 -->
	<property name="sessionValidationSchedulerEnabled" value="true"/>
	 <!-- 是否删除无效的,默认也是开启 -->
	<property name="deleteInvalidSessions" value="true"/>
	<property name="sessionIdCookie" ref="sessionIdCookie"/>
</bean>
<!-- session 创建、删除、查询 -->
<bean id="sessionShiroCacheManager" class="com.sojson.core.shiro.cache.impl.SessionShiroCacheManager" >
	 <property name="ehCacheManager" ref="ehCacheManager"/>
</bean>
<!-- 会话验证调度器 -->
<bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler">
	 <!-- 间隔多少时间检查,不配置是60分钟 -->
     <property name="interval" value="${session.validate.timespan}"/>
     <property name="sessionManager" ref="sessionManager"/>
</bean>

8.6 小插曲——静态注入

这里还是讲一下Spring提供的静态注入,要不然怕你看到这里不知道这是什么意思,其实当你了解Spring的静态注入后,你会发现你很多地方用得着,就不用去getBean("name")了,直接静态注入即可。spring-shiro.xml中用到了,如下:

<!-- 静态注入,相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
    <property name="arguments" ref="securityManager"/>
</bean>

静态注入详细讲解:Spring 静态注入讲解(MethodInvokingFactoryBean)

九、Shiro Ajax 请求拦截、跳转方案

我们知道Ajax不能做页面redirectforward跳转,所以Ajax请求假如没登录,那么这个请求给用户的感觉就是没有任何反应,而用户又不知道用户已经退出或是  Session  超时了。这个时候如何解决?

@Override
protected boolean isAccessAllowed(ServletRequest request,
		ServletResponse response, Object mappedValue) throws Exception {
	UUser token = TokenManager.getToken();
	if (null != token || isLoginRequest(request, response)) {// &&// isEnabled()
		return Boolean.TRUE;
	}
	if (ShiroFilterUtils.isAjax(request)) {// ajax请求
		Map<String, String> resultMap = new HashMap<String, String>();
		LoggerUtils.debug(getClass(), "当前用户没有登录,并且是Ajax请求!");
		resultMap.put("login_status", "300");
		resultMap.put("message",
				"\u5F53\u524D\u7528\u6237\u6CA1\u6709\u767B\u5F55\uFF01");// 当前用户没有登录!
		ShiroFilterUtils.out(response, resultMap);
	}
	return Boolean.FALSE;
}

以上代码和教程代码一致,类路径com.sojson.core.shiro.filter.LoginFilter

详细说明,请看这篇:Shiro 教程,Ajax请求拦截跳转页面方案

十、Freemarker for Shiro 标签使用

首先Jar包的引入,可能有点偏门,如果你对  Shiro  稍微熟练,又对  Shiro  自定义标签熟悉的话,可以自己写一套。  Maven  引入:

<!-- freemarker + shiro(标签) begin -->
<dependency>
    <groupId>net.mingsoft</groupId>
    <artifactId>shiro-freemarker-tags</artifactId>
    <version>0.1</version>
    <scope>private</scope>
</dependency>
<!-- freemarker + shiro(标签) begin -->

具体用法请参考这:Shiro教程(八)Shiro Freemarker标签的使用,这里就不做过多叙述了。具体看项目Freemarker 中的使用。

十一、JSP for Shiro 标签使用

虽然当前Shiro Demo没有使用JSP,但是鉴于用JSP的同学较多,而当前项目又支持JSP,那么在这里还是提一下。

首先在JSP顶部像引入JSTL相关库一样的这么引入:

<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>  

如果报错,或者是飘红,那么请你首先把这个链接复制到浏览器,看是否能打开,如果能打开,那么就剪切、粘贴、剪切、粘贴重复保存几次看看。

JSP Shiro标签详细讲解:Shiro教程(九)Shiro JSP标签的使用

十二、Shiro 登录后获取登录之前的最后一个访问地址

这个功能乍一看,不懂什么意思,先说明这个功能。下面来举栗子:

场景1:当我访问http://www.sojson.com/admin.shtml  的时候,它是一个要登录的页面,那么  Shiro  控制它跳转到登录页,那么登录完毕了呢?应该怎么跳转,可能很多人会想跳转到项目的首页,那要是我继续跳转到原来访问的页面http://www.sojson.com/admin.shtml 呢?是不是用户体验瞬间上升了?

场景2:当当前页面Session失效的时候,跳转到登录,然后应该跳转到哪里?应该是最后操作的页面吧,其实和上面差不多。

其实我们平时非Shiro的项目也可以获取,但是可能用起来不是那么好用,方法如下:

//上一个浏览的非Ajax的地址,在登录后,取得地址,如果不为null,那么就跳转过去。
String url = (String) request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE);

上面的WebUtils 是  Shiro  里面的工具类,但是这个变量却不是Shiro的,它是:javax.servlet.forward.request_uri ,如果不是  Shiro  项目,你直接用这个“javax.servlet.forward.request_uri” 即可。

Shiro 获取上一个请求的信息方式:

/**
当Shiro 的Filter拦截请求后,如果不满足条件(return false),就会走这个方法
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
		throws Exception {
	//保存Request和Response,登录后可以取到
	saveRequestAndRedirectToLogin(request, response);
	return Boolean.FALSE ;
}

//登录后,取到之前的Request中的一些信息。
SavedRequest saveRequest = WebUtils.getSavedRequest(request);
saveRequest.getMethod();//之前的请求方法
saveRequest.getQueryString();//之前请求的条件
saveRequest.getRequestURI();//之前请求的路径
saveRequest.getRequestUrl();//之前请求的全路径

十三、禁止用户登录

这个功能其实是一个改变用户数据库表里的一个字段,本Demo中:1:有效,0:禁止登录

然后踢出用户登录状态。代码详细请查看CustomSessionManager.java 类的forbidUserById(Long id, Long status) 方法。

而再次登录的话,需要再登录,而登录的地方限制了用户状态为(0:禁止登录)的用户登录。

Java具体实现代码:

    /**
     * 查询要禁用的用户是否在线。
     * @param id		用户ID
     * @param status	用户状态
     */
    public void forbidUserById(Long id, Long status) {
    	//获取所有在线用户
    	for(UserOnlineBo bo : getAllUser()){
    		Long userId = bo.getId();
    		//匹配用户ID
    		if(userId.equals(id)){
    			//获取用户Session
    			Session session = shiroSessionRepository.getSession(bo.getSessionId());
    			//标记用户Session
    			SessionStatus sessionStatus = (SessionStatus) session.getAttribute(SESSION_STATUS);
    			//是否踢出 true:有效,false:踢出。
    			sessionStatus.setOnlineStatus(status.intValue() == 1);
    			//更新Session
    			customShiroSessionDAO.update(session);
    		}
    	}
    }

十四、在线有效Session显示,在线Session踢出

从  Ehcache  获取所有的  Session  ,然后获取有效Session ,也就是包含用户信息的Session ,然后进行页面展示,这里没做分页,支持踢出、激活功能,踢出后当对方再次操作的时候,会提示“你已经被踢出,请重新登录”,下图展示功能。

点击上方“踢出”功能后,不管你点击哪个链接,或者是刷新当前页面,都会提示如下页面。

踢出后,不能直接退出,要不然用户感觉莫名其妙。所有增加了一个Filter。SimpleAuthFilter.java如果标记为踢出,会提示用户。具体查看源码以及配合项目的使用。

这个看情况,如果你需要用户直接跳转到登录页面去,那么你直接执行获取用户然后 logout 即可。

十五、用户密码修改

用户密码修改功能很简单,但是有同学转不过弯来,首先密码修改常规做法基本就这2种 :

  • 先输入原来的密码,然后输入新密码,先校验原来的密码是正确的才更新新密码。
  • 直接输入新密码或者根据密码保护填写正确直接更新密码。

但是有人转不过弯来,我怎么校验他原来的密码是否正确,我们校验密码和登录是一样的,按你注册的时候加密的规则加密他输入的老密码,然后进行对比,正确方可修改,和登录功能是一样样的。

Java代码实现:

/**
 * 个人资料修改
 * @return
 */
@RequestMapping(value="updateSelf",method=RequestMethod.POST)
@ResponseBody
public Map<String,Object> updateSelf(UUser entity){
	try {
		userService.updateByPrimaryKeySelective(entity);
		
		resultMap.put("status", 200);
		resultMap.put("message", "修改成功!");
		/**
		 * 重新帮用户登录一次,可以显示最新的资料
		 */
		UUser token = TokenManager.getToken();
		Boolean rememberMe = token.getRememberMe();
		token = userService.selectByPrimaryKey(token.getId());
		//修改之后再Login一下,覆盖现有登录信息
		TokenManager.login(token, rememberMe);
	} catch (Exception e) {
		resultMap.put("status", 500);
		resultMap.put("message", "修改失败!");
		LoggerUtils.fmtError(getClass(), e, "修改个人资料出错。[%s]", JSONObject.fromObject(entity).toString());
	}
	return resultMap;
}

十六、权限管理

16.1 权限结构设计(RBAC3)

权限设计基于RBAC3的结构,即权限赋予角色,角色赋予用户的结构。如下图:

对RBAC的理解:RBAC 介绍,结合案例讲解。

16.2 权限增删改查

当前Demo 依赖于URL控制权限,如下图:

十七、角色的增删改查,以及赋予权限

17.1 角色的增删改查

17.2 权限分配给角色

十八、对用户赋予角色

当我们把相关权限赋予给角色哦,那每个用户对应到哪些权限?其实这里就是  RBAC3  的宗旨,一切基于角色,也就是所有的用户只能赋予角色,这个和生活中是一样的,下面举个栗子。

比如:老王是某公司的技术总监,而且同时是规划部的部长,那老王就是有2个角色,或者说是3个角色,都有公司员工、技术总监、规划部长,那老王就拥有这3个角色的权限。这样懂了吧。

十九、管理员权限自动添加

为什么有这个需求?管理员的权限控制,无非有2种,一是对管理员不做控制,二是对管理员拥有所有权限。而我们系统就是第二种。

每当系统添加了一个新的权限,管理员的角色自动补充一条记录,这样促使管理员角色拥有所有权限。

public UPermission insertSelective(UPermission record) {
	//添加一条权限记录
	permissionMapper.insertSelective(record);
	
	//每添加一个权限,都往【系统管理员 	888888】里添加一次。保证系统管理员有最大的权限
	executePermission(new Long(1), String.valueOf(record.getId()));
	return record;
}

二十、项目功能详细介绍

更多功能详细介绍请看这里:http://eh.itboy.net/shiro.ehcache/demo/index.shtml (不需要登录访问)。

二十一、Shiro Demo 项目下载

Shiro Demo 线上展示地址:http://eh.itboy.net/shiro.ehcache/  

帐号:admin ,密码:sojson.com ,尽量不要动管理员的数据。20分钟数据会重新生成。

21.1 项目源码获取详细说明。

介于  Demo  维护的成本,以及日常的开支。此项目源码需要 50RMB ,之前赞助过本站的够50RMB的同学直接找群主要即可。

收费介绍请看这里:http://eh.itboy.net/shiro.ehcache/demo/resource.shtml

收费请在这里打开网址扫码即可: http://www.sojson.com/other/subsidize.html    ,扫码后加群说明即可。群主会给您源码。 收到的钱用来本站的开支(目前入不敷出,还是负数)。

介意要钱的,请下载 Redis + Shiro版本的Demo项目:http://www.sojson.com/shiro ,从实际来说,这个版本更有学习价值。