Shiro 教程基于SSM(SpringMVC + Spring + Mybatis)EHCache版本

一、Shiro简介

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

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

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

二、概述

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

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

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

三、需要你的赞助

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

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

四、请遵循三要素

  1. 建议你看完本篇文档所有内容,再进行运行项目。
  2. 在没熟练之前,除了必要的配置修改,请勿改动任何配置和包路径。
  3. 有疑问先看文档,交流群: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.5Mysql5.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_80JDK1.6有些jar包会报错,推荐使用JDK1.7以上。

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

 
  1. Unsupported major.minor version 51.0

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

 
  1. JDK1.5对应为Unsupported major.minor version 49.0
  2. JDK1.6对应为Unsupported major.minor version 50.0
  3. JDK1.7对应为Unsupported major.minor version 51.0
  4. 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配置你的数据库链接。

 
  1. jdbc.url=jdbc:mysql://localhost:3306/shiro.demo
  2. jdbc.username=root
  3. 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目录,改成你自己的目录即可:

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

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

 
  1. <mirrors>
  2. <mirror>
  3. <id>alimaven</id>
  4. <name>aliyun maven</name>
  5. <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
  6. <mirrorOf>central</mirrorOf>
  7. </mirror>
  8. </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.xmlnamespace。调用父类的findPage相关。

Service Impl Java 代码:

 
  1. publicPagination<UserRoleAllocationBo> findUserAndRole(ModelMap modelMap,
  2. Integer pageNo,Integer pageSize){
  3. //findUserAndRole : 为查询数据(sqlID)
  4. //findCount : 为查询符合数据的总条数(sqlID)
  5. returnsuper.findPage("findUserAndRole","findCount", modelMap, pageNo, pageSize);
  6. }

分页查询使用缺省的sqlId

 
  1. publicPagination<UPermission> findPage(Map<String,Object> resultMap,Integer pageNo,
  2. Integer pageSize){
  3. /**
  4. * 调用父类的分页查询,默认数据查询sqlId = findAll , count 查询sqlId = findCount
  5. */
  6. returnsuper.findPage(resultMap, pageNo, pageSize);
  7. }

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

 
  1. <selectid="findCount"resultMap="BaseResultMap">
  2. select count(id) from u_user
  3. <includerefid="where_all"/>
  4. </select>
  5. <!-- 用户权限分配的分页查询 -->
  6. <selectid="findUserAndRole"resultType="com.sojson.permission.bo.UserRoleAllocationBo">
  7. 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
  8. u_user u
  9. left join u_user_role uur on uur.uid = u.id
  10. left join u_role ur on ur.id = uur.rid
  11. <where>
  12. <iftest="findContent != null and findContent !='' ">
  13. and (
  14. LOWER(u.nickname) like LOWER(CONCAT("%",#{findContent,jdbcType=VARCHAR},"%")) or
  15. LOWER(u.email) like LOWER(CONCAT("%",#{findContent,jdbcType=VARCHAR},"%"))
  16. )
  17. </if>
  18. </where>
  19. group by u.id
  20. </select>
 
  1. <selectid="findAll"resultMap="BaseResultMap">
  2. select
  3. <includerefid="Base_Column_List"/>
  4. from u_permission
  5. <includerefid="where_all"/>
  6. <includerefid="limit_sql"/>
  7. </select>
  8. <selectid="findCount"resultMap="BaseResultMap">
  9. select count(id) from u_permission
  10. <includerefid="where_all"/>
  11. </select>

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

前端HTML页面输出:

 
  1. <#if page?exists>
  2. <divclass="pagination pull-right">
  3. ${page.pageHtml}
  4. </div>
  5. </#if>

Java 分页处理代码:

 
  1. publicString getPageHtml(){
  2. Map<String,Object> map =newHashMap<String,Object>();
  3. map.put("pageNo",this.getPageNo());
  4. map.put("totalPage",this.getTotalPage());
  5. //从Freemarker 获取分页html
  6. returnnewFreemarker().getTemplate("common/page/html_page.ftl", map);
  7. }

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

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

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

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

 
  1. <tx:adviceid="txAdvice"transaction-manager="transactionManager">
  2. <tx:attributes>
  3. <tx:methodname="publish*"/>
  4. <tx:methodname="save*"/>
  5. <tx:methodname="add*"/>
  6. <tx:methodname="update*"/>
  7. <tx:methodname="insert*"/>
  8. <tx:methodname="create*"/>
  9. <tx:methodname="del*"/>
  10. <tx:methodname="load*"/>
  11. <tx:methodname="init*"/>
  12. <tx:methodname="*"read-only="true"/>
  13. </tx:attributes>
  14. </tx:advice>
  15. <!-- AOP配置-->
  16. <aop:config>
  17. <aop:pointcutid="myPointcut"
  18. expression="execution(public * com.sojson.*.service.impl.*.*(..))"/>
  19. <aop:advisoradvice-ref="txAdvice"pointcut-ref="myPointcut"/>
  20. </aop:config>

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

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

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

7.1.4 View层Freemarker 配置

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

 
  1. <!--===============通用视图解析器 begin===============-->
  2. <beanid="viewResolverCommon"
  3. class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  4. <propertyname="prefix"value="/WEB-INF/views/"/>
  5. <propertyname="suffix"value=".jsp"/>
  6. <!-- 可为空,方便实现自已的依据扩展名来选择视图解释类的逻辑-->
  7. <propertyname="viewClass">
  8. <value>org.springframework.web.servlet.view.InternalResourceView
  9. </value>
  10. </property>
  11. <propertyname="order"value="1"/>
  12. </bean>
  13. <!-- 视图解析器 -->
  14. <!-- 配置freeMarker视图解析器 -->
  15. <beanid="viewResolverFtl"
  16. class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
  17. <propertyname="viewClass"value="com.sojson.core.freemarker.extend.FreeMarkerViewExtend"/>
  18. <!-- 把Freemarker 扩展一下,把相关属性加入进去。。。 -->
  19. <propertyname="contentType"value="text/html; charset=utf-8"/>
  20. <propertyname="cache"value="true"/>
  21. <propertyname="suffix"value=".ftl"/>
  22. <propertyname="order"value="0"/>
  23. </bean>
  24. <beanid="viewResolver"
  25. class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  26. <propertyname="order"value="2"></property>
  27. <propertyname="viewClass"
  28. value="org.springframework.web.servlet.view.JstlView"/>
  29. <propertyname="prefix"value="/WEB-INF/views/"/>
  30. <propertyname="suffix"value=".jsp"></property>
  31. </bean>
  32. <!-- 配置freeMarker 拓展-->
  33. <beanid="freemarkerConfig"
  34. class="com.sojson.core.freemarker.extend.FreeMarkerConfigExtend">
  35. <propertyname="templateLoaderPath">
  36. <value>/WEB-INF/ftl/</value>
  37. </property>
  38. <propertyname="freemarkerVariables">
  39. <map>
  40. <entrykey="xml_escape"value-ref="fmXmlEscape"/>
  41. <entrykey="api"value-ref="api"/>
  42. </map>
  43. </property>
  44. <propertyname="defaultEncoding">
  45. <value>utf-8</value>
  46. </property>
  47. <propertyname="freemarkerSettings">
  48. <props><!-- 315360000 -->
  49. <propkey="template_update_delay">0</prop><!-- Freemarker 模版缓存时间。开发环境不用开启,单位毫秒 -->
  50. <propkey="defaultEncoding">UTF-8</prop>
  51. <propkey="url_escaping_charset">UTF-8</prop>
  52. <propkey="locale">zh_CN</prop>
  53. <propkey="boolean_format">true,false</prop>
  54. <propkey="datetime_format">yyyy-MM-dd HH:mm:ss</prop>
  55. <propkey="date_format">yyyy-MM-dd</prop>
  56. <propkey="time_format">HH:mm:ss</prop>
  57. <!-- <prop key="number_format">0.######</prop>-->
  58. <propkey="number_format">#</prop>
  59. <propkey="whitespace_stripping">true</prop>
  60. <propkey="auto_import">
  61. <!-- Freemarker macro 文件应用 -->
  62. /common/config/top.ftl as _top,
  63. /common/config/left.ftl as _left,
  64. /common/config/menu.ftl as _menu
  65. </prop>
  66. </props>
  67. </property>
  68. </bean>
  69. <beanid="fmXmlEscape"class="freemarker.template.utility.XmlEscape"/>

7.1.5 Freemarker API标签封装

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

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

 
  1. <!-- 自动扫描 标签 -->
  2. <context:component-scanbase-package="com.sojson.*.*.tag;com.sojson.*.tag"/>
  3. <!-- api 标签功能主类 -->
  4. <beanname="api"class="com.sojson.core.tags.APITemplateModel"></bean>

  Java  后台代码:

 
  1. /**
  2. * 测试封装的 Freemarker 标签代码 。
  3. */
  4. @Component
  5. publicclassUserInfoTagextendsSuperCustomTag{
  6. //支持注入
  7. @Autowired
  8. UUserService userService;
  9. /**
  10. * 继承父类的方法,必须实现,返回类型为Object,方便使用,这样就可以返回Map、List、以及自己的JavaBean
  11. */
  12. @Override
  13. protectedObject result(Mapparams){
  14. Long userId = getLong(params,"userId");//从页面取得userId
  15. return userService.selectByPrimaryKey(userId);
  16. }
  17. }

  Freemarker  前端使用:

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

 
  1. <!-- shiro + ehcache jar -->
  2. <dependency>
  3. <artifactId>ehcache-core</artifactId>
  4. <groupId>net.sf.ehcache</groupId>
  5. <version>2.5.0</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.apache.shiro</groupId>
  9. <artifactId>shiro-ehcache</artifactId>
  10. <version>1.2.2</version>
  11. </dependency>
  12. <!-- shiro end -->

Shiro-ehcache,其实我偏向自己实现比较好管理。  Ehcache  配置文件在resources目录下发现有2Ehcache相关的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进行控制。

 
  1. <beanid="shiroFilter"class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  2. <propertyname="securityManager"ref="securityManager"/>
  3. <propertyname="loginUrl"value="/u/login.shtml"/>
  4. <propertyname="successUrl"value="/"/>
  5. <propertyname="unauthorizedUrl"value="/?login"/>
  6. <!-- 基本系统级别权限配置-->
  7. <propertyname="filterChainDefinitions">
  8. <value>
  9. /page/login.jsp = anon <!-- 登录相关不拦截 -->
  10. /page/register/* = anon
  11. /page/index.jsp = authc
  12. /page/addItem* = authc,roles[数据管理员]
  13. /page/file* = authc,roleOR[普通用户,数据管理员]
  14. /page/listItems* = authc,roleOR[数据管理员,普通用户]
  15. /page/showItem* = authc,roleOR[数据管理员,普通用户]
  16. /page/updateItem*=authc,roles[数据管理员]
  17. /** = anon <!-- 其他不拦截 -->
  18. </value>
  19. </property>
  20. <!-- 自定义shiro 的 Filter -->
  21. <propertyname="filters">
  22. <util:map>
  23. <entrykey="login"value-ref="login"></entry>
  24. </util:map>
  25. </property>
  26. </bean>

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

 
  1. <propertyname="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  的“-”去掉了。

 
  1. publicclassSOSJONSessionIdGeneratorextendsJavaUuidSessionIdGenerator{
  2. publicfinalstaticString SID_KEY ="uid:%s:%s";
  3. @Override
  4. publicSerializable generateId(Session session){
  5. //这里只是做一个小小的演示,具体功能你可以自己丰富。
  6. String sid =super.generateId(session).toString();
  7. return sid.replaceAll("-","");
  8. }
  9. }

8.3.2 会话Cookie模板

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

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

 
  1. <!-- 会话Cookie模板 -->
  2. <beanid="sessionIdCookie"class="org.apache.shiro.web.servlet.SimpleCookie">
  3. <!--cookie的name,我故意取名叫xxxxbaidu -->
  4. <constructor-argvalue="sid_demo"/>
  5. <propertyname="httpOnly"value="true"/>
  6. <!--cookie的有效时间 -->
  7. <propertyname="maxAge"value="-1"/>
  8. <!-- 配置存储Session Cookie的domain为 一级域名
  9. <property name="domain" value=".sojson.com"/>
  10. -->
  11. </bean>

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

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

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

8.4 Shiro Remember Me(记住我)

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

Remember Me Cookie配置:

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

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

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

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

Remember Me 加密Key配置:

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

8.5 Shiro Session 配置

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

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

 
  1. <!-- custom shiro session listener -->
  2. <beanid="customShiroSessionDAO"class="com.sojson.core.shiro.CustomShiroSessionDAO">
  3. <propertyname="shiroSessionRepository"ref="sessionShiroCacheManager"/>
  4. <propertyname="sessionIdGenerator"ref="sessionIdGenerator"/>
  5. </bean>
  6. <!-- 手动操作Session,管理Session -->
  7. <beanid="customSessionManager"class="com.sojson.core.shiro.session.CustomSessionManager">
  8. <propertyname="shiroSessionRepository"ref="sessionShiroCacheManager"/>
  9. <propertyname="customShiroSessionDAO"ref="customShiroSessionDAO"/>
  10. </bean>
  11. <!-- Session Manager -->
  12. <beanid="sessionManager"class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
  13. <!-- 相隔多久检查一次session的有效性 -->
  14. <propertyname="sessionValidationInterval"value="1800000"/>
  15. <!-- session 有效时间为半小时 (毫秒单位)-->
  16. <propertyname="globalSessionTimeout"value="1800000"/>
  17. <propertyname="sessionDAO"ref="customShiroSessionDAO"/>
  18. <!-- session 监听,可以多个。 -->
  19. <propertyname="sessionListeners">
  20. <list>
  21. <refbean="customSessionListener"/>
  22. </list>
  23. </property>
  24. <!-- 间隔多少时间检查,不配置是60分钟 -->
  25. <propertyname="sessionValidationScheduler"ref="sessionValidationScheduler"/>
  26. <!-- 是否开启 检测,默认开启 -->
  27. <propertyname="sessionValidationSchedulerEnabled"value="true"/>
  28. <!-- 是否删除无效的,默认也是开启 -->
  29. <propertyname="deleteInvalidSessions"value="true"/>
  30. <propertyname="sessionIdCookie"ref="sessionIdCookie"/>
  31. </bean>
  32. <!-- session 创建、删除、查询 -->
  33. <beanid="sessionShiroCacheManager"class="com.sojson.core.shiro.cache.impl.SessionShiroCacheManager">
  34. <propertyname="ehCacheManager"ref="ehCacheManager"/>
  35. </bean>
  36. <!-- 会话验证调度器 -->
  37. <beanid="sessionValidationScheduler"class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler">
  38. <!-- 间隔多少时间检查,不配置是60分钟 -->
  39. <propertyname="interval"value="${session.validate.timespan}"/>
  40. <propertyname="sessionManager"ref="sessionManager"/>
  41. </bean>

8.6 小插曲——静态注入

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

 
  1. <!-- 静态注入,相当于调用SecurityUtils.setSecurityManager(securityManager) -->
  2. <beanclass="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
  3. <propertyname="staticMethod"value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
  4. <propertyname="arguments"ref="securityManager"/>
  5. </bean>

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

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

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

 
  1. @Override
  2. protectedboolean isAccessAllowed(ServletRequest request,
  3. ServletResponse response,Object mappedValue)throwsException{
  4. UUser token =TokenManager.getToken();
  5. if(null!= token || isLoginRequest(request, response)){// &&// isEnabled()
  6. returnBoolean.TRUE;
  7. }
  8. if(ShiroFilterUtils.isAjax(request)){// ajax请求
  9. Map<String,String> resultMap =newHashMap<String,String>();
  10. LoggerUtils.debug(getClass(),"当前用户没有登录,并且是Ajax请求!");
  11. resultMap.put("login_status","300");
  12. resultMap.put("message",
  13. "\u5F53\u524D\u7528\u6237\u6CA1\u6709\u767B\u5F55\uFF01");// 当前用户没有登录!
  14. ShiroFilterUtils.out(response, resultMap);
  15. }
  16. returnBoolean.FALSE;
  17. }

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

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

十、Freemarker for Shiro 标签使用

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

 
  1. <!-- freemarker + shiro(标签) begin -->
  2. <dependency>
  3. <groupId>net.mingsoft</groupId>
  4. <artifactId>shiro-freemarker-tags</artifactId>
  5. <version>0.1</version>
  6. <scope>private</scope>
  7. </dependency>
  8. <!-- freemarker + shiro(标签) begin -->

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

十一、JSP for Shiro 标签使用

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

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

 
  1. <%@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的项目也可以获取,但是可能用起来不是那么好用,方法如下:

 
  1. //上一个浏览的非Ajax的地址,在登录后,取得地址,如果不为null,那么就跳转过去。
  2. 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 获取上一个请求的信息方式:

 
  1. /**
  2. 当Shiro 的Filter拦截请求后,如果不满足条件(return false),就会走这个方法
  3. */
  4. @Override
  5. protectedboolean onAccessDenied(ServletRequest request,ServletResponse response)
  6. throwsException{
  7. //保存Request和Response,登录后可以取到
  8. saveRequestAndRedirectToLogin(request, response);
  9. returnBoolean.FALSE ;
  10. }
  11. //登录后,取到之前的Request中的一些信息。
  12. SavedRequest saveRequest =WebUtils.getSavedRequest(request);
  13. saveRequest.getMethod();//之前的请求方法
  14. saveRequest.getQueryString();//之前请求的条件
  15. saveRequest.getRequestURI();//之前请求的路径
  16. saveRequest.getRequestUrl();//之前请求的全路径

十三、禁止用户登录

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

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

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

Java具体实现代码:

 
  1. /**
  2. * 查询要禁用的用户是否在线。
  3. * @param id 用户ID
  4. * @param status 用户状态
  5. */
  6. publicvoid forbidUserById(Long id,Long status){
  7. //获取所有在线用户
  8. for(UserOnlineBo bo : getAllUser()){
  9. Long userId = bo.getId();
  10. //匹配用户ID
  11. if(userId.equals(id)){
  12. //获取用户Session
  13. Session session = shiroSessionRepository.getSession(bo.getSessionId());
  14. //标记用户Session
  15. SessionStatus sessionStatus =(SessionStatus) session.getAttribute(SESSION_STATUS);
  16. //是否踢出 true:有效,false:踢出。
  17. sessionStatus.setOnlineStatus(status.intValue()==1);
  18. //更新Session
  19. customShiroSessionDAO.update(session);
  20. }
  21. }
  22. }

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

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

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

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

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

十五、用户密码修改

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

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

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

Java代码实现:

 
  1. /**
  2. * 个人资料修改
  3. * @return
  4. */
  5. @RequestMapping(value="updateSelf",method=RequestMethod.POST)
  6. @ResponseBody
  7. publicMap<String,Object> updateSelf(UUser entity){
  8. try{
  9. userService.updateByPrimaryKeySelective(entity);
  10. resultMap.put("status",200);
  11. resultMap.put("message","修改成功!");
  12. /**
  13. * 重新帮用户登录一次,可以显示最新的资料
  14. */
  15. UUser token =TokenManager.getToken();
  16. Boolean rememberMe = token.getRememberMe();
  17. token = userService.selectByPrimaryKey(token.getId());
  18. //修改之后再Login一下,覆盖现有登录信息
  19. TokenManager.login(token, rememberMe);
  20. }catch(Exception e){
  21. resultMap.put("status",500);
  22. resultMap.put("message","修改失败!");
  23. LoggerUtils.fmtError(getClass(), e,"修改个人资料出错。[%s]",JSONObject.fromObject(entity).toString());
  24. }
  25. return resultMap;
  26. }

十六、权限管理

16.1 权限结构设计(RBAC3)

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

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

16.2 权限增删改查

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

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

17.1 角色的增删改查

17.2 权限分配给角色

十八、对用户赋予角色

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

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

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

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

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

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

二十、Shiro Demo 项目下载

Shiro Demo 线上展示地址:http://demo.www.sojson.com/shiro.ehcache/

如果你不是Maven项目,请在此处下载Jar包:http://pan.baidu.com/s/1geLHXS3 密码: puym

项目源码下载:还没提交。

猜你喜欢

转载自qq8446666.iteye.com/blog/2348774