权限框架Shiro学习笔记(SSM版)

文章目录

本文章SSM项目地址:https://github.com/fenxianxian/shiro5/tree/master

1. 权限管理概念

  很简单,我也不想废话,但我不得不说,不然这篇文章就不完美了。因为我相信,不用学,大家也知道权限管理是什么意思,毕竟你们也经常的浏览各种网站,肯定跑不过权限这一块,比如视频网站,看的视频是所有人都能看吗?不一定吧,你充VIP了吗?还有,在CSDN写文章,是所有人都能写的吗?不一定吧,你登陆了吗?众所周知,你不登陆,你就不能在我网站*问各种资源,所以,我强迫你登陆,只有这样,我才能为你分配权限,让部分资源为你而敞开。
  大白话就是控制某某人可以/不可以访问某某功能,这里可不可以就是你的权限了,所谓的权限就是根据身份的不同进行不同的操作,不同的操作就意味着权力的不同,这就是系统中的权限管理。

2. 浅谈RBAC权限设计

  假设有一套教务管理系统,它的登陆页面大概如下:
权限框架Shiro学习笔记(SSM版)主要看那三个单选框,在登陆之前我们都会根据自己的身份去选择对应的身份进行登录,这样做的目的是系统可以根据不同的身份跳转或生成对应的页面,页面的不同归根到底就是权限的不同,比如学生有查看自己成绩的权利,但是不能查看全班同学的成绩,而老师就不一样了,只要是他管理的班级,他班下的所有学生的成绩他都可以查看,甚至可以录入成绩,修改成绩等,如下:
权限框架Shiro学习笔记(SSM版)大概就像上面那样,我只是意思意思,注意把以上内容看成一个菜单。可以看出,管理员是拥有所有权限的,而学生和教师是拥有一部分权限,这是根据他们的身份决定的,所以,应用到项目中,我们应该怎么去实现它?

1.基于页面的权限控制

  在建立数据表时,不用说,肯定会有用户表,可以存放他的用户名啊,密码这些,但为了可以在登陆的过程中根据他的身份确定他的权限,并在页面显示出来,我们可以在用户表上加上一个字段,字段名就叫state,表示状态,比如,1代表学生,2代表教师,3代表管理员。就这样。好,只需要做这一步就行了,然后写三个首页,这三个首页对应着这三种身份,也就是说,在登陆成功后(前提是用户名,密码和状态码要正确),不是要跳转到首页吗?那跳到哪个首页呀,就看你的状态是多少了,你状态多少,我就跳到哪个首页,注意,一样是首页,但是身份不同,首页的内容也不同,所谓的内容就是我上图列举出来的内容,你有什么内容你就可以做什么事,就这么简单。到这最基本的权限控制也就体现出来了。

问题:

  如上做法有什么问题吗?有,当然有,比如这时候我要对教师分配某种权限啥的,那么对于这套系统而言,如果没有提供一种可以为某位用户分配某种权限的话,是不是就无法完成了,但能完成吗,肯定可以,只不过作为管理员的我可能不懂代码,如果懂,那还好说,直接打开html,修改一下就行了,但我不懂啊,我只会操作界面,那怎么办,是不是要请工程师来完成,工程师针对这种情况,是不是可以在操作界面上给你增加一个按钮,以后要分配,点一下那个按钮就行了,即方便你,也方便我,多好。那么工程师会怎么完成呢?

2.基于权限的权限控制

  这次不基于页面来进行权限控制了,每次分配都要修改页面代码,麻烦,有没有一种办法可以让其自动改变?嗯,我们可不可以这样,建一个权限表(资源表),该权限表有两个字段,一个是id,一个是权限名,姑且叫PermissionName,比如如下:
权限框架Shiro学习笔记(SSM版)解读一下,像id为1002和1003的,还有1008和1009的,都有一个共同点,就是前面会有什么什么冒号,其实,冒号前面的是模板名来的,比如,像成绩管理这块,它下面有两个子模块,分别是成绩查询和录入成绩,成绩查询对应的就是select,录入成绩对应的就是add,说白了,就是让可读性好一点,没有什么特殊的。那么用户表这边呢,可以把state字段给去掉了,但为了在查询用户的时候顺便把他的权限也查出来,我们可以在用户和权限表之间再建一张表,叫用户权限表,也就是所谓的中间表(因为用户和权限是多对多的关系,所以需要一个中间表来表示),通过这张表,就可以让用户和权限联系起来,如下:
权限框架Shiro学习笔记(SSM版)该表表示有一位用户id为1的人,它拥有的权限有六个,如上,是不是有六条记录,注意,以上六个权限id都是学生们所拥有的权限,可以拿着权限id去权限表查。好,那么到此为止,我们在登陆的时候,肯定是先校验用户名和密码的,这不用说,然后再根据用户id去用户权限表查找相应的权限,查出来的权限可以有多个,如上图,再把查出来的多个权限拿到权限表上进行查找,查找出来后,让页面根据查找出来的权限名进行动态的变动。

问题:

  一样,这种做法有什么问题?问题就是,如果来了为新学生,那么该学生的权限肯定跟用户id为1所拥有的权限一样,因为用户id为1的用户也是一名学生,学生跟学生,权限肯定一样,好,假设新学生的用户id为2,那么在用户权限表上是不是会多出六条记录,相当于复制了一份,是不是极度冗余,如果学生过多的话,那么记录岂不是长到爆,这就是问题。

3.基于角色的权限控制

  那以上问题如何解决?其实很简单,不是权限id这块冗余了吗,那么就把权限id这块冗余的地方提取出来,只不过这次不跟用户id对应了,而是跟角色对应,什么意思,让我们再再建一张表,叫角色表,如下:
权限框架Shiro学习笔记(SSM版)有了角色表,为了可以根据角色来查权限,我们可以再再再一次建一张表,叫角色权限表,这个就不截图了,跟用户权限表一样一样的,只不过这次如果加入一位新学生进来,我们只为它分配一个角色就行了,所以在用户表上我们要加上一个字段,叫角色id,比如1110,那么,我们就可以根据角色id到角色权限表里查,把他的权限查出来,这样不就行了?

问题:

  到此为止,有用户表,用户角色表,角色表,角色权限表,权限表这5张表,当然还少了用户权限表,但目前来看用户权限表被用户角色表给替代了,因为我说了,用户权限表有缺点,但是,问题来了,随着需求的变化,这次我不是为某一个角色分配权限,如果是的话,好办,我在角色权限表中增加一条记录,但这次,我是要为某一个特定的用户增加某种权限,不是针对角色的哦,那这次的用户权限表不就发挥他的作用了吗,所以,这点要注意。

收尾

  以上,像基于角色的权限控制和基于权限的权限控制已经形成最基本的RBAC权限模型了。(ง ˙o˙)ว

3. 走进Shiro

LOGO:
权限框架Shiro学习笔记(SSM版)
  这logo很形象,盾牌,意味着安全,那也就是说,Shiro是做安全这方面的框架,而事实也正是如此,只不过此时我们还没开始正式学习Shiro,所以体会不到,没关系,我们继续。
  在前面我们曾提到过 “权限管理” 这个概念,而在意义上它是包含两部分的,也就是身份认证和授权。那什么是身份认证?身份认证就是判断你的身份是否是我的合法用户,非黑名单的那种。而授权则表示当你的身份认证通过后,你在我系统所拥有的操作权限,哪些可以为你放开,哪些不可以,不是说你登陆了之后,我就得把我系统里面的所有资源都给你享用,有些可是要充VIP的哦,像这些都得由我给你控制,授予。

1.身份认证

  身份认证,就是判断一个用户是否为合法用户的处理过程。最简单的身份认证方式就是系统通过核对用户输入的用户名和密码,看其是否与系统中存储的该用户的用户名和密码一致,来判断用户身份是否正确。还有像指纹呀,刷脸啊,刷卡呀都属于身份认证的范畴。
  对于身份认证,我们会抽取出如下几个对象,

  1. Subject(主体):所谓的Subject,其实指的是“访问系统的用户”。反正你就理解为它指的就是当前用户,所谓的当前用户,就是我登陆哪个系统,我就是哪个系统的当前用户,我就是Subject,Subject就是我,也就是说,进行认证的都可称为Subject,那换句话说,Subject要想认证,它应该少不了用户名和密码。
  2. Principal(身份信息):是主体进行身份认证的标识,标识必须具有唯一性,如用户名,手机号,邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
  3. Credential(凭证):是只有主体自己知道的安全信息,如密码,指纹,证书等。

如下:
权限框架Shiro学习笔记(SSM版)当然了,在subject里除了有Principal和Credential,还有一个叫token,令牌的意思,那什么是令牌,公式:令牌=身份信息+凭证信息。但注意,令牌可不是简简单单的只是包含身份信息和凭证信息,还有其它。

2.授权

  授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。授权可简单理解为who对what(which)进行how操作,如下:

  1. Who:Who,即主体,主体需要访问系统中的资源。
  2. What:即资源(Resource),如系统菜单,页面,按钮,类方法,系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
  3. How:权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限,用户添加权限,某个类方法的调用权限,编号为001的用户的修改权限等,通过权限可知主体对哪张资源都有哪些操作许可。权限分为粗颗粒和细颗粒,粗颗粒权限是指对资源类型的权限,细颗粒权限是指对资源实例的权限。

4. Shiro架构

权限框架Shiro学习笔记(SSM版)

什么是Shiro?

  Shiro是一个基于java的开源的安全管理框架,可以完成认证,授权,会话管理,加密,缓存等功能。

架构图解释

  首先,一看那架构图,涉及到的名词就很多,其中最引人注目的是中间那块,也就是叫Security Manager,翻译过来叫安全管理,它是Shiro架构的核心,所有与安全有关的操作都会与SecurityManager进行交互。好,我们看它里面的内容:

  • Authenticator(认证器):Authenticator是负责执行和响应用户的身份验证(登录)尝试的组件。当用户尝试登录时,该逻辑由验证器执行。验证器知道如何与存储相关用户/帐户信息的一个或多个领域进行协调。从这些领域获得的数据被用来验证用户的身份,以保证用户真正是他们所说的那个人。
  • Authorizer(授权器):Authorizer是负责确定应用程序中用户访问控制的组件。 它是最终决定是否允许用户做某事的机制。 与Authenticator一样,Authenticator也知道如何与多个后端数据源协调访问角色和权限信息。 Authorizer使用此信息来准确确定是否允许用户执行给定的操作。常见的如:验证某个用户身份拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限。
  • SessionManager(会话管理器):会话管理,即用户登陆后就是一次会话,在没有退出之前,它的所有信息都在会话中,会话可以是普通的java SE环境的,也可以是Web环境。它是管理Session生命周期的组件;
  • CacheManager(缓存管理器):将用户权限数据存储在缓存,这样可以提高性能。
  • Cryptography(密码管理):shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
  • Realms:Shiro从Realm中获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它就需要从Realm获取相应的用户进行比较以确定用户身份是否合法,也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。比如用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。

整体流程

权限框架Shiro学习笔记(SSM版)

5. Shiro环境搭建

  新建一个普通的maven项目。新建完之后,在pom.xml中加入以下坐标:

<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>
 <!-- 防止报java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory错误 -->
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

  然后在resources文件夹下创建shiro.ini文件,该文件是用来模拟数据库中的持久化数据的,也就是说,我们现在暂且不跟数据库打交道,而是通过一个文本文件shiro.ini来配置数据,到时候,如果我们跟数据库打交道了,就不用shiro.ini了。
  让我们往shiro.ini加入点东西,如下:

[users]
xiaochen=123456
chenxian=abcd

中括号中的user表示用户,加了s代表多个用户,也就是说,在它之下,我们可以配置多个用户,比如,用户名和密码,如果我们往下看的话,是不是猜都能猜出来,格式就是:用户名=密码,而事实也正是如此。我们还可以扩展一下格式:用户名=密码,角色1,角色2。
  以上shiro.ini里的内容,就是数据库里的内容,还是那句话,模拟。好了,既然有了用户名和密码,我们是不是可以做一下认证,如下代码:

package com.cht.test;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

//shiro认证
public class TestShiro {

    public static void main(String[] args) {
        //1. 要想认证,首先得先有安全管理器SecurityManager,我们可以看第4节的Shiro架构图,你就会发现,
        //我们那所谓的认证器,授权器都是SecurityManager里的一个部件,同时,在应用程序这端,不管
        //是C,C++啥的,还有PHP,Python,甚至是Java都是需要经过SecurityManager的,有了SecurityManager。
        //我们就可以开始做权限控制了,而我之前说过,权限控制包含身份认证,而身份认证这件事是不是交
        //由SecurityManager里的Authenticator认证器来完成呀。
        //1.1 先创建一个安全管理器工厂类(注:IniSecurityManagerFactory已废弃)
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //1.2 通过工厂获取安全管理器
        SecurityManager securityManager = factory.getInstance();
        //2. 有了securityManager就可以做认证了,但是做认证,你得有用户名和密码啥的,因为你不发用户名和密码给我,
        //我怎么对你进行校验,所以,我们得有一个subject对象,让它把用户名和密码携带过来
        //2.1 将安全管理器告知SecurityUtils,或者说注入,那也就是说,在底层,将会用到securityManager
        SecurityUtils.setSecurityManager(securityManager);
        //2.2 获取Subject主体对象
        //拿到Subject就好办了,因为通过Subject就可以完成我们Shiro几乎所有的功能
        //(因为这几乎的所有功能都被SecurityManager给管理起来了,而Subject又是调
        //SecurityManager的,所以功能都被间接的调用,当然不包括加密),往后,我们面
        //对的都是Subject,通过Subject里的方法(底层调用的是SecurityManager,也就是
        //说Subject充其量就是个传话的),就可以完成我们想要的功能
        Subject subject = SecurityUtils.getSubject();
        //2.3 获取token对象,把用户名和密码进行封装
        AuthenticationToken authenticationToken = new UsernamePasswordToken("xiaochen","123456");
        //2.4 携带token进行登录(跟携带用户名和密码本质一样),开始认证
        try {
            subject.login(authenticationToken);
            System.out.println("认证成功");
        }catch (AuthenticationException e){ //用户名错误,密码错误都能接收
            System.out.println("认证失败");
            e.printStackTrace();  //如果认证失败,走catch块
        }

    }
}

答案肯定是认证成功,如果我们把UsernamePasswordToken的第二个参数改成不是123456,那么就认证失败,或者我们把用户名改成其它的,只要不是Shiro.ini里的用户名就行。
  注意,密码错误抛出的异常是IncorrectCredentialsException(Incorrect:无效,不正确; Credentials:凭证),而用户名错误的异常是UnknownAccountException。

补充subject对象里的方法

System.out.println(subject.getPrincipal()); //获取凭证,其实取的用户名
System.out.println(subject.isAuthenticated());//为true,表示已认证成功
System.out.println(subject.hasRole("student"));//判断是否为student角色
System.out.println(subject.isPermitted("insert"));//判断是否有添加的权限
subject.logout();//登出

扩展shiro.ini

[roles]
#格式:角色名=权限名,权限名
#代表role1拥有user的添加,更新的权限。以下的值可以简写为"user:insert,update"(注:必须加引号)
role1=user:insert,user:update
#代表role2拥有添加,更新,删除权限
role2=insert,update,delete
#值为user:*代表role3拥有user的所有权限
role3=user:*
#值为*代表role4拥有所有权限
role4=*
[urls]
#url地址=内置filter或自定义filter,角色名["权限1",...]
# 访问时出现/login的url必须去认证,支持authc对应的对应的Filter
/login = authc
# 任意的url都不需要进行认证等功能
/** = anon
# 所有的内容都必须保证用户已经登录
/** = user
# 访问/abc时必须保证用户具有role1和role2的角色
/abc = roles["role1,role2"]
# 访问/ab时必须保证有user:insert权限
/ab = perms["user:insert"]

注意,以上说的什么内置filter啥的翻译过来就是内置过滤器,指的是authc,anon,user这些,也就是说,这三个就代表了过滤器,只不过它采用的是别名,而真正的过滤器类如下:

public enum DefaultFilter {
    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);
}

这样你就会看到像authc所对应的过滤器类是谁了,你也可以点进去,主要看的是isAccessAllowed方法。

[main]
#没有身份认证时的跳转地址
shiro.loginUrl = /user/login/page
#角色和权限校验不通过时的跳转地址
shiro.unauthorizedUrl = /author/error
#登出后的跳转地址,回首页
shiro.redirectUrl = / 

多多少少了解下即可。

6. Shiro与Web集成

项目结构如下:
权限框架Shiro学习笔记(SSM版)
打开pom.xml,如下maven坐标:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.cht</groupId>
  <artifactId>shiro5</artifactId>
  <version>1.0-SNAPSHOT</version>
  
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>4.3.6.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-web</artifactId>
      <version>1.4.0</version>
    </dependency>
  </dependencies>

</project>

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
  <!--
      接收所有请求,以通过请求路径,识别是否需要安全校验,如果需要则触发安全校验,做访问校验时,会遍历过滤器链。
      (链中包含shiro.ini中urls内使用的过滤器)
      会通过ThreadContext在当前线程中绑定一个subject和SecurityManager,供请求内使用,可以通过
      SecurityUtils.getSubject()获取Subject
      此处shiroFilter的用处:
      1.  在进入DispatcherServlet之前多了一份拦截
      2, 在项目启动时,会初始化好Shiro的环境,比如会把以下工作做好:
           IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
           SecurityManager securityManager = factory.getInstance();
           SecurityUtils.setSecurityManager(securityManager);
          这样我们就可以在程序中直接使用SecurityUtils.getSubject()了。比如UserController中的loginLogic方法
      -->
  <filter>
      <filter-name>shiroFilter</filter-name>
      <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
  </filter>
  <filter-mapping>
      <filter-name>shiroFilter</filter-name>
      <url-pattern>/*</url-pattern>
  </filter-mapping>
  <!--在项目启动时,加载web-info或classpath下的shiro.ini,并构建WebSecurityManager。构建所有配置
  中使用的过滤器链(anon,authc等),ShiroFilter会获取此过滤器链-->
  <listener>
      <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
  </listener>
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/mvc.xml</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

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:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="com.cht"/>
    <mvc:annotation-driven/>
    <bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/" ></property>
        <property name="suffix" value=".jsp"></property>
    </bean>

</beans>

User

package com.cht.pojo;

public class User {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

UserController

package com.cht.controller;

import com.cht.pojo.User;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.Map;

@Controller
@RequestMapping("/user")
public class UserController {

    @GetMapping("/login")
    public String login(){
        System.out.println("去往登录页面");
        return "login";
    }

    @PostMapping("/login")
    public String loginLogic(User user,Map<String,Object> map){
        ModelAndView modelAndView = new ModelAndView();
        System.out.println("login logic");//登录 逻辑
        //获取subject  调用login
        Subject subject = SecurityUtils.getSubject();
        //创建用于登录的令牌
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        // 登录失败会抛出异常,则交由异常解析器处理,登录成功,继续往下执行
        try{
            subject.login(token); //默认从Ini文件获取
        }catch (IncorrectCredentialsException e){
            map.put("message","密码错误");
            return "error";
        }catch (UnknownAccountException e){
            map.put("message","用户名错误");
            return "error";
        }
        return "index";
    }

    @RequestMapping("permission")
    public String permission(){
        return "permission";
    }

    @RequestMapping("all")
    public String all(){
        return "all";
    }
}

shiro.ini

[users]
xiaochen=123456
#chenxian具有role1角色
chenxian=abcd,role1
[roles]
role1=user:query
[urls]
# 注意以下的书写顺序,比如不要把/** = user放在最前面,
# 因为它管理的范围太大了,毕竟它是从上到下执行的,如果匹配了,就不会往下执行。
#去往登录的路上不用拦截,认证,所以用anon
/user/login = anon
/user/all = authc,perms["user:query"]
# 所有的内容都必须保证用户已经登录,
# 如果没登录,那么跳转地址看[main],在最下面
/** = user
[main]
#没有身份认证时的跳转地址
shiro.loginUrl = /user/login
#角色和权限校验不通过时的跳转地址
shiro.unauthorizedUrl = /user/permission

all.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>查询所有</title>
</head>
<body>
    all,我进来了
</body>
</html>

error.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>错误</title>
</head>
<body>
${message}
</body>
</html>

index.jsp

<%@ page contentType="text/html; charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
index页面
</body>
</html>

login.jsp

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

permission.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>权限不足</title>
</head>
<body>
对不起,你的权限不足,不能进入all.jsp页面
</body>
</html>

测试:

  1. 在项目启动后,无论是何种路径,比如像http://localhost:8080/sfdsfsdf这种随便写的路径都会自动跳转到登陆页面,也就是http://localhost:8080/user/login,因为还没有登陆,被限制了。自己去看shiro.ini中的[urls]和[main]即可。

  2. 用账号为xiaochen,密码是abcd测试,对照shiro.ini可知,有用户名为xiaochen的,但是密码却不是abcd,所以,当我们点击登陆按钮,将会弹出如下页面:
    权限框架Shiro学习笔记(SSM版)用户名错误就不演示了。

  3. 再用正确的用户名和密码登陆,如下:
    权限框架Shiro学习笔记(SSM版)

  4. 测试http://localhost:8080/user/all,注意,当前是账号为xiaochen的环境,如下:
    权限框架Shiro学习笔记(SSM版)

  5. 再用chenxian的账号去登陆,再来测试http://localhost:8080/user/all,如下:
    权限框架Shiro学习笔记(SSM版)

7. Shiro标签

  shiro标签就是对页面元素的访问控制,比如有些菜单因为你的权限不足不足以查看,那么这些菜单就不应该出现,出现了也是摆设,而事实上确实有这种需求,就不应该出现。那么怎么做到,这就需要用到shiro的标签了。说白了,通过shiro标签,我们就可以在页面判断,哪些该显示,哪些不该显示,就像java中的逻辑判断一样。这样,同一个页面,不同的用户看到的内容就有所差别。

导入shiro标签库

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

知道加到jsp页面的哪个位置吧。一旦加上去的话,我们就可以在页面使用shiro标签了,如下:
权限框架Shiro学习笔记(SSM版)

身份认证

  1. <shiro:authenticated >表示已登陆,如下,我们以上节跟web整合的案例演示,用login.jsp随便写上一句话,注意,只看效果,无特殊意思,如下:
    权限框架Shiro学习笔记(SSM版)做好后重启一下项目,项目启动后自动会进入login.jsp,到时候肯定是还未登陆状态,那么你就可以看我已经登录了这句话是否会显示出来,然后再登陆,登陆完后再看一下那几句话有没有显示出来。如果未登陆状态没有显示,登陆后才显示那就是对的,标签起作用了。最后,与之相反的标签是<shiro:notAuthenticated >。

  2. <shiro:user >,常用,包含已登录,且配合记住我,用户体验好。

  3. <shiro:principal />,就是获取凭证的意思,显示的是你的用户名,一般嵌套在<shiro:authenticated >标签或<shiro:user >标签里,也就是说,你登陆了,那我就可以把你的用户名显示出来,或者你曾经点击过记住我,那我下次再打开的时候就不用登陆了,自动显示我的用户名。

  4. <shiro:guest >,表示游客,也就是未登录或未记住我。

角色校验

  1. <shiro:hasAnyRoles name=“admin,manager”>,表示是其中的任何一个角色。
  2. <shiro:hasRole name=“admin”>,是指定角色。
  3. <shiro:lacksRole name=“admin”> ,不是指定角色。

权限校验

  1. <shiro:hasPermission name=“user:delete”>,表示有用户的删除权限。
  2. <shiro:lacksPermission name=“user:delete”>,表示没有用户的删除权限。

8. 自定义Realm

  存在的问题:目前所有的用户,角色,权限数据都在ini文件中,不利于管理,实际项目开发中的这些信息,应该在数据库中,所以需要为这三类信息建表。
  当前的默认Realm,是IniRealm,我们拆分解读一下,Ini指的就是ini文件,Realm我前面也提到过,它是用来获取数据的,相当于Dao,那也就是说,如果我们没有自定义Reaml,那么Shiro它需要数据就会调用IniReaml,让IniReaml去我们指定的ini文件读取数据,但是,在真实的项目开发中,我第一段也说了,数据是存在数据库里的,不是存在某一个文件里的,毕竟,数以万计的用户是不可能写在文件里的,所以,我们就不能用它默认的IniReaml了,而是用我们自定义的Reaml,让我们的这个自定义的Reaml去数据库里获取数据,所以,明白了吗?
  既然这样,要查数据库,那么我们是不是得先在数据库里建表呀,我们就采用RBAC的思想来建吧,如下:

create table t_user(
	id int primary key auto_increment,
	username varchar(20) not null unique,
	password varchar(100) not NULL
)engine=innodb default charset=utf8;

create table t_role(
	id int primary key auto_increment,
	role_name varchar(50) not null unique,
	create_time timestamp not null
)engine=innodb default charset=utf8;

create table t_permission(
	id int primary key auto_increment,
	permission_name varchar(50) not null unique,
	create_time timestamp
)engine=innodb default charset=utf8;

create table t_user_role(
	id int primary key auto_increment,
	user_id int references t_user(id),
	role_id int references t_role(id),
	unique(user_id,role_id)
)engine=innodb default charset=utf8;

create table t_role_permission(
	id int primary key auto_increment,
	permission_id int references t_permission(id),
	role_id int references t_role(id),
	unique(permission_id,role_id)
)engine=innodb default charset=utf8;

我们再插入几条数据,如下:

insert into t_user(username,password) values('小飞老师',123456),('小齐同学',436678);
insert into t_role(role_name,create_time) values("teacher","2021/7/3"),("student","2021/7/4");
insert into t_permission(permission_name,create_time) values("student:manage","2020/10/9"),("student:study","2020/10/9");#分别是管理学生的权限和学生学习的权限
insert into t_user_role(user_id,role_id) values(1,1),(2,2);
insert into t_role_permission(permission_id,role_id) values(1,1),(2,2);

  下面我们接着第6节的项目继续下去,只不过这次我们采用ssm的方式来进行项目改造,并从原先默认的realm变为现在的自定义realm,但是却有个问题,因为我们学过ssm的都知道,配置文件是非常多的,而这么多的配置文件贴出来也没意义,我就把项目都上传到百度网盘了,地址为https://pan.baidu.com/s/1D4dQRZEWyoITdyy5CFPe4A,提取码为hh82。需要的自行下载研究,或者直接看我说的github地址也一样,只不过github里的项目是完整项目。而在这,我只贴重要的代码,如下:

//自定义Realm
//Realm的职责就是为shiro加载用户,角色和权限数据,供内部校验使用,现在既然库中有数据了,就需要用自定义的Realm去加载。所以现在我们就自己建一个自定义的Realm类吧
//以下两个方法不是我们来调,而是securityManager需要数据时自动来调用
public class MyRealm extends AuthorizingRealm {

    //做权限,角色校验
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("进行权限,角色验证。。。");
        //获取用户名
        String username = (String) principalCollection.getPrimaryPrincipal();
        //根据用户名查询权限和角色信息(这里注意一下,因为我此处是根据用户名去查询,所以我们得要求用户名是唯一的)
        RoleService roleService
                = ContextLoader.getCurrentWebApplicationContext().getBean("roleServiceImpl", RoleService.class);
        PermissionService permissionService
                = ContextLoader.getCurrentWebApplicationContext().getBean("permissionServiceImpl", PermissionService.class);
        Set<String> roles = roleService.queryAllRolenameByUsername(username);
        Set<String> permissions = permissionService.queryAllPermissionByUsername(username);
        //将查询出来的数据封装成AuthorizationInfo
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roles);
        simpleAuthorizationInfo.addStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }

    //做身份认证(查用户名,密码的)
    //何时触发:subject.login(token);
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("进行身份认证。。。");
        //根据传进来的令牌token,获取用户名
        String username = (String) authenticationToken.getPrincipal();
        //查询用户信息
        UserService userService
                = ContextLoader.getCurrentWebApplicationContext().getBean("userServiceImpl", UserService.class);
        //查询到用户信息
        User user = userService.queryUserByUsername(username);
        //判断用户身份为空
        if(user==null){ //如果为空
            return null; //后续会抛出异常,也就是所谓的UnKnownAccountException
        }
        //将用户信息封装在AuthenticationInfo对象中
        //第一个参数:数据库里的用户名  第二个参数:数据库里的密码 第三个参数:可以不太在意,可以理解为当前realm的标识,名字
        SimpleAuthenticationInfo info
                = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),this.getName());
        return info;
    }
}

  以上就是自定义Realm类,该自定义Reaml类是不需要我们手动调用的,是SecurityManager需要数据自动调用的,那它是怎么知道有这么一个自定义类呢?这就需要shiro.ini文件了,shiro.ini文件有如下几句话,如下:

[main]
...
#realm1是随便写的。下面为声明自定义的realm
realm1 = com.cht.realm.MyRealm
#安装Realm,关联到SecurityManager。注意,如果有多个realm,那么下面的值记得用逗号隔开,也就是$realm1,$realm2
securityManager.realms=$realm1

也就是说,shiro.ini这里已经配置好了有哪些自定义Realm,那么SecurityManager当然就知道咯。
  以上自定义realm类说明一下,如果realm只做身份认证,则继承:AuthenticatingRealm。如果realm要负责身份认证和权限校验,则可以继承AuthorizingRealm。
  方法体里所做的事,我就不多说了,反正就是调service,然后service调dao去数据库里查,然后比对,就这些,需要注意的是ContextLoader.getCurrentWebApplicationContext(),写这句话的时候,别忘了去web.xml里加上这么一句话,如下:

<!--配置下面监听器的作用是让MyRealm类里的ContextLoader.getCurrentWebApplicationContext()发挥作用-->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>

9. Shiro登陆流程简单梳理

从subject.login(token);开始梳理:

  1. 其实subject.login底层是securityManager.login,好,往里走。
  2. 将会看到这样的代码,AuthenticationInfo info = this.authenticate(token);
  3. 走进authenticate方法,将会看到this.authenticator.authenticate(token);,authenticator是不是就是身份校验器呀,也就是说,底层会调用身份校验器来完成身份认证。
  4. 继续往里跟,会判断你的这个token是否为null,为null抛异常,不为空,那没事,继续走,将会看到AuthenticationInfo info = this.doAuthenticate(token);,注意这个doAuthenticate方法就是真正干事的,进入该方法。
  5. 该方法会获取所有的Realm,如果是自定义的realm,那就会获取到自定义的realm,如果是一个Realm,那就会进入doSingleRealmAuthentication方法。
  6. 进入后将会看到AuthenticationInfo info = realm.getAuthenticationInfo(token);,进入getAuthenticationInfo方法,就会看到AuthenticationInfo info = this.doGetAuthenticationInfo(token);而doGetAuthenticationInfo这个方法不就是我们写自定义realm里的那两个方法的其中一个吗?
  7. 进入我们自己写的doGetAuthenticationInfo方法,假设我们填写的用户名是正确的,那么return出去的info就是有值的,不为null。
  8. 在前面不为info不为null的基础上,开始对密码进行验证,也就是this.assertCredentialsMatch(token, info);方法,该方法里有密码比对器来对其进行比对。说白了,就是从token里拿出密码,和从info里拿出密码来进行比对。密码不一致,就会抛出IncorrectCredentialsException异常。

10. 加密

  用户的密码是不允许明文存储的,一旦泄露,用户的隐私信息就会暴露出来,所以,密码必须加密,生成密文,然后数据库中的密码存的也是密文。
  在加密过程中需要使用到一些“不可逆加密”,如MD5,sha等。所谓不可逆是指:加密函数A,明文“abc”,A(“abc”)=密文,不能通过密文反推出“abc”,即使密文泄露密码仍然安全。
  shiro支持hash(散列)加密,常见的如md5,sha等。

  • 基本加密过程:md5(明文),sha(明文)得到明文的密文,但明文可能比较简单导致密文容易被破解。
  • 加盐加密过程:系统生成一个随机salt=“xxxxx”,md5(明文+salt),sha(明文+salt),则提升了密文的复杂度。
  • 加盐多次迭代加密过程:如果迭代次数为2,则加密2次,md5(明文+salt)=密文a,md5(密文a+salt)=最终密文。sha(明文+salt)=密文a,sha(密文a+salt)=最终密文。则进一步提升了密码的复杂度,和被破解的难度。

  加密过程中建议使用salt,并制定迭代的次数,迭代次数的建议值1000+。

public static void main(String[] args) {
   String pwd = "1234";
   String salt = UUID.randomUUID().toString();//盐
   //将密码用md5加密,并加上盐,最后迭代1000次,最终加密出来的结果再转为Base64,就是最终的密文了
   String s = new Md5Hash(pwd, salt, 1000).toBase64();//或者是toString()
   System.out.println(s);
}

注册

  有了上面的铺垫之后,注册就简单了,无非就是获取到用户的密码,然后加密,再把加密好的放到数据库里,这套流程就在service里添加用户时完成的,如下:

public Integer insertUser(User user) {
    String salt = UUID.randomUUID().toString();
    String pwd = new Sha256Hash(user.getPassword(), salt, 1000).toBase64();
    user.setPassword(pwd);
    return userMapper.insertUser(user);
}

  进行登陆时,肯定会用到密码,那么这时,我们获取用户传过来的密码,就要对它进行验证,怎么验证呢?首先,人家在注册时用的是md5加密还是sha加密你是不是得知道呀,也就是说,要统一,因为,相同的密码,经过相同的加密规则,最终得到的密文就是一样的,所以,在注册的时候既然用的是sha加密规则,那么在登陆的时候也得用sha的加密规则,并且要保证salt,和迭代次数一致,只有这样,我们在进行和数据库里的密文比对时,才会判断它传过来的明文密码是否正确。
  但是却有一个问题,salt是随机的呀,那该如何解决?解决办法就是在注册时把用到的salt也存起来,那么这时我们就得修改一下我们的User实体类了,加上一个属性,就叫salt,然后数据库里的t_user表上也加上一个字段,也叫salt。所以以上代码要优化一下,如下:

public Integer insertUser(User user) {
    String salt = UUID.randomUUID().toString();
    String pwd = new Sha256Hash(user.getPassword(), salt, 1000).toBase64();
    user.setPassword(pwd);
    user.setSalt(salt);
    return userMapper.insertUser(user);
}

登陆

  在shiro.ini加上以下配置,如下:

#声明密码比对器
credentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName = sha-256
credentialsMatcher.hashIterations = 1000
credentialsMatcher.hashSalted = true
#true=hex格式,也就是toString()   false=base64格式,也就是toBase64()
credentialsMatcher.storedCredentialsHexEncoded = false
#交由自定义reaml
realm1.credentialsMatcher = $credentialsMatcher

然后再在自定义realm类下的doGetAuthenticationInfo方法下,把原先的SimpleAuthenticationInfo info
= new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),this.getName());改为SimpleAuthenticationInfo info
= new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());

11. 与Spring集成

  Shiro一旦跟Spring集成,那么shiro.ini就可以去掉了,因为一切都交由Spring管理了。

maven坐标

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

编写spring-shiro.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        ">
   <!--shiro配置
    Realm-->
    <bean id="myRealm" class="com.cht.realm.MyRealm">
        <property name="userService" ref="userServiceImpl"/>
        <property name="roleService" ref="roleServiceImpl"/>
        <property name="permissionService" ref="permissionServiceImpl"/>
        <property name="credentialsMatcher" >
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="SHA-256"/>
                <property name="storedCredentialsHexEncoded" value="false"/>
                <property name="hashIterations" value="1000"/>
            </bean>
        </property>
    </bean>
    <!--DefaultWebSecurityManager是SecurityManager的默认实现-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!--把realm交由securityManager去接管-->
        <property name="realm" ref="myRealm"/>
    </bean>
    <!--shiroFilter
    生产SpringShiroFilter
    (持有shiro的过滤相关规则,可进行请求的过滤校验,校验请求是否合法-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!--注入核心-->
        <property name="securityManager" ref="securityManager"/>
        <!--编写过滤器链,充分说明了ShiroFilter会来执行这些过滤器链,
        执行过程中需要数据校验就会调用核心securityManager,核心又拥有realm,realm又拥有数据,自然可以校验-->
        <property name="filterChainDefinitions">
            <value>
            	/user/regist = anon
                /user/all = authc,perms["student:study"]
                /** = user
            </value>
        </property>
        <!--没有身份认证时的跳转地址-->
        <property name="loginUrl" value="/user/login"/>
        <!--角色和权限校验不通过时的跳转地址-->
        <property name="unauthorizedUrl" value="/user/permission"/>
    </bean>
    
</beans>

  注意spring-shiro.xml是通过applicationContext.xml来访问的,也就是如下:

<import resource="spring-shiro.xml"></import>

web.xml

  在web.xml下增加如下过滤器,并把原先的ShiroFilter过滤器以及EnvironmentLoaderListener监听器注释掉。

 <!--会从spring工厂中获取和它同名的Bean,(id="shiroFilter")
   接到请求后调用bean的doFilter方法,进行访问控制-->
<filter>
    <filter-name>shiroFilter</filter-name>  <!--注意,这行的shiroFilter可不是随便乱写的,看上面的注释说明-->
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <!--DelegatingFilterProxy不处理业务,传话的-->
    <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>

  然后回到我们写的自定义Reaml,把出现过ContextLoader.getCurrentWebApplicationContext()的统统注释掉,我们不通过这种方式来获取service类,而是用我们以前学的注入service,也就是在该自定义Reaml加上以下三个属性:

private UserService userService;
private RoleService roleService;
private PermissionService permissionService;

注意要在类上加上@Setter注解,表示用set注入。

12. 记住我

  在登陆后,可以将用户名存在cookie中,下次访问时,可以先不登陆,就可以识别身份,在确定需要身份认证时,比如购买,支付或其它一些重要操作时,再要求用户登陆即可,用户体验好。由于可以保持用户信息,系统后台也可以更好的监控,记录用户行为,积累数据。

//如果需要记住我的话,需要在token中设置
token.setRememberMe(true);//shiro默认支持记住我,只要有此设置则自动运作,也就是说,当我们关闭浏览器,再次打开网页,是不用登陆的。但注意,记住我对过滤器链中的authc是无效的,该认证还是要认证,比如你访问/user/all。
subject.login(token); 

  以上记住我默认记265天,如果要想改为7天,如下:

<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <!--rememberMe是cookie值中的key,value是用户名的密文
    cookie["rememberMe":"deleteMe"] 此cookie每次登陆后都会写出,用于清除之前的cookie
    cookie["rememberMe":"username的密文"] 此cookie也会在登陆后写出,用于记录最新的username
    (如上设计,既能保证每次登陆后重新记录cookie,也能保证切换账号时,记录最新账号)-->
    <property name="name" value="rememberMe"/>
    <!--cookie只在http请求中可用,那么通过js脚本将无法读取到cookie信息,有效防止cookie被窃取-->
    <property name="httpOnly" value="true"/>
    <!--cookie生命周期,单位秒-->
    <property name="maxAge" value="2592000"/>  <!--30天-->
</bean>
<!--记住我管理器-->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
    <property name="cookie" ref="rememberMeCookie"/>
</bean>

最后注意securityManager这边,如下:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
   <!--把realm交由securityManager去接管-->
    <property name="realm" ref="myRealm"/>
    <property name="rememberMeManager" ref="rememberMeManager"/>  <!--默认记住我是1年,但此时已经关联了新的记住我,所以是7天-->
</bean>

13. Session管理

  Shiro中的会话管理,其实跟java web里边的httpSession是一致的,都是表示客户端跟服务器的一次会话。一般我们在controller层使用httpSession,在非controller,比如service层我们可以用shiro给我们提供的session,照样可以获取httpSession里的数据。
  Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如Web容器tomcat),不管Java SE或者Java EE都可以使用,提供了会话管理,会话事件监听,会话存储/持久化,容器无关的集群,失效/过期支持,对Web的透明支持,SSO单点登录的支持等特性。

相关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():获取会话的启动时间及最后访问时间,如果是Java SE应用需要自己定期调用session.touch()去更新最后访问时间,如果是web应用,每次进入ShiroFilter都会自动调用session.touch()来更新最后访问时间。
  • session.touch() & session.stop():更新会话最后访问时间及销毁会话;当Subject.logout()时会自动调用stop方法来销毁会话,如果是web中,调用HttpSession.invalidate()也会自动调用Shiro Session.stop()方法来进行销毁Shiro的会话。

在Java SE中使用

Subject subject = SecurityUtils.getSubject();
//获取session
Session session = subject.getSession();
//session超时时间,单位,毫秒;0,马上过期;负数,不会过期,正数,对应毫秒后过期
session.setTimeout(10000);
//session存取值
session.setAttribute("name","cht");
session.getAttribute("name");
//销毁session
session.stop();

java EE环境

<!--增加session管理相关配置
会话cookie模板,默认可省-->
<!--存sessionId的cookie-->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <!--cookie的key="sid"-->
    <property name="name" value="JSESSIONID"/>
    <!--只允许http请求访问cookie-->
    <property name="httpOnly" value="true"/>
    <!--cookie的过期时间,-1:存活一个会话,单位:秒,默认为-1-->
    <property name="maxAge" value="-1"/>
</bean>

<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <property name="sessionIdCookie" ref="sessionIdCookie"/>
    <!--session全局超时时间,单位:毫秒,30分钟,默认值为1800000-->
    <property name="globalSessionTimeout" value="1800000"/>
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    .....
    <property name="sessionManager" ref="sessionManager"/>
</bean>

session监听

package com.cht.session;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListenerAdapter;

public class MySessionListener extends SessionListenerAdapter {
    //session创建时触发
    @Override
    public void onStart(Session session) {
        System.out.println("session create");
    }
    //session停止时触发   session.logout()  session.stop()
    @Override
    public void onStop(Session session) {
        System.out.println("session stop");
    }
    //session过期时触发,静默时间超过过期时间
    //但注意它不会主动告知已过期,而是当你再一次访问的时候,它才会去校验是否过期,如果过期,就触发
    @Override
    public void onExpiration(Session session) {
        System.out.println("session expire");
    }
}
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
   ....
   <property name="sessionListeners">
       <list>
           <bean class="com.cht.session.MySessionListener"></bean>
       </list>
   </property>
</bean>

session检测

<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    ...
    <!--session检测的时间间隔,单位毫秒,那15000毫秒就是15秒,也就是说,每15秒检测一次,看看session是否过期,如果过期了,将调用监听器里的onExpiration方法-->
    <property name="sessionValidationInterval" value="15000"/>
</bean>

14. Shiro注解

加在类上

@Controller
@RequestMapping("/user")
@RequiresAuthentication //类中的所有⽅法都需要身份认证
@RequiresRoles(value={"manager","admin"},logical= Logical.OR)//类中的所有⽅法都需要⻆⾊,"或"
public class ShiroController  {
...
}

加在方法上

@Controller
@RequestMapping("/user")
public class  ShiroController2{
  ...
	@RequiresPermissions({"user:query","user:delete"})  //有对应权限,默认是 "且"
	@RequiresUser //记住我 或 已身份认证
	public String  hello(){
	...
	}
	@RequiresGuest //游客身份
	public String  hello2(){
	...
	}
}
上一篇:05 Shrio Realm


下一篇:wordpress中前台如何去掉左上角w标志