欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Shiro的基本使用

程序员文章站 2022-08-10 11:10:00
[toc] 首发日期:2019 06 03 前言 在以往的权限管理中,我们的权限管理通常是有以下几个步骤: 1.创建用户,分配权限。 2.用户登录,权限拦截器拦截请求,识别当前用户登录信息 3.从权限表中判断是否拥有权限 从以上步骤中可以提取到以下三个问题。 三个问题: 1.如何让Shiro拦截请求 ......

首发日期:2019-06-03


前言



在以往的权限管理中,我们的权限管理通常是有以下几个步骤:
1.创建用户,分配权限。
2.用户登录,权限拦截器拦截请求,识别当前用户登录信息
3.从权限表中判断是否拥有权限


从以上步骤中可以提取到以下三个问题。
三个问题:

1.如何让shiro拦截请求。
在web开发中,shiro会提供一个拦截器来对请求进行拦截。

2.shiro如何判断发起请求用户的身份?
在web开发中,会借助session来判断,如果禁用了session,那么可能需要重写一些方法。

3.如何判断权限?
shiro使用realm来判断权限。


下面的也将以这三个问题为中心来描述shiro。


shiro的介绍



Shiro的基本使用

  • shiro是一个开源的java安全(权限)框架,它能够实现身份验证、授权、加密和会话管理等功能。
  • shiro是apache旗下的产品,它的官网是: shiro官网: apache shiro
  • shiro不仅可以用于javaee环境,也可以用于javase


shiro功能

Shiro的基本使用

  • authentication:身份认证,验证用户是否拥有某个身份。
  • authorization: 权限校验,验证某个已认证的用户是否拥有某个权限。确定“谁”可以访问“什么”。
  • session management:会话管理,管理用户登录后的会话,
  • cryptography:加密,使用密码学加密数据,如加密密码。
  • web support:web支持,能够比较轻易地整合到web环境中。
  • caching:缓存,对用户的数据进行缓存,
  • concurrency:并发,apache shiro支持具有并发功能的多线程应用程序,也就是说支持在多线程应用中并发验证。
  • testing:测试,提供了测试的支持。
  • run as :允许用户以其他用户的身份来登录。
  • remember me :记住我


补充

  • 同类的比较知名的安全框架还有spring security,shiro的优点是比较简洁,功能虽然比不上spring security多样,但对于安全需求不多的时候可以使用shiro。



helloworld


依赖包:

        <dependency>
            <groupid>org.apache.shiro</groupid>
            <artifactid>shiro-core</artifactid>
            <version>1.3.2</version>
        </dependency>
        <!-- 这里有用到日志打印,所以引入 -->
        <dependency>
            <groupid>org.slf4j</groupid>
            <artifactid>slf4j-log4j12</artifactid>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupid>log4j</groupid>
            <artifactid>log4j</artifactid>
            <version>1.2.15</version>
        </dependency>

对于不同的场景有不同依赖包【可以参考这个http://shiro.apache.org/download.html#latestsource】
基础包是shiro-core,这里仅作基础示例所以仅仅导入这个包。


示例代码

【一些代码可以参考https://github.com/apache/shiro/tree/master/samples/quickstart】
1.src/main/resources/shiro.ini的代码:

# -----------------------------------------------------------------------------
# users用来定义用户
[users]
# 用户名 = 密码,角色1,角色2...
admin = secret, admin
guest = guest, guest
aa = 123456, guest
# -----------------------------------------------------------------------------
# roles用来定义角色
[roles]
# 角色 = 权限 (* 代表所有权限)
admin = *
# 角色 = 权限 (* 代表所有权限)
guest = see
aa = see


2.src/main/com/demo/shirodemo的代码:

package com.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.subject.subject;
import org.slf4j.logger;
import org.slf4j.loggerfactory;

public class shirodemo {
    private static final logger log = loggerfactory.getlogger(shirodemo.class);
    public static void main(string[] args) {
        //1.创建securitymanagerfactory
        inisecuritymanagerfactory factory =
                new inisecuritymanagerfactory("classpath:shiro.ini");
        //2.获取securitymanager,绑定到securityutils中
        securitymanager securitymanager = factory.getinstance();
        securityutils.setsecuritymanager(securitymanager);
        //3.获取一个用户识别信息
        subject currentuser = securityutils.getsubject();
        //4.判断是否已经身份验证
        if (!currentuser.isauthenticated()) {
            // 4.1把用户名和密码封装为 usernamepasswordtoken 对象
            usernamepasswordtoken token = new usernamepasswordtoken("guest", "guest");
            // 4.2设置rememberme
            token.setrememberme(true);
            try {
                // 4.3登录.
                currentuser.login(token);
            }
            catch (unknownaccountexception uae) { //用户不存在异常
                log.info("****---->用户名不存在: " + token.getprincipal());
                return;
            }
            catch (incorrectcredentialsexception ice) {// 密码不匹配异常
                log.info("****---->" + token.getprincipal() + " 的密码错误!");
                return;
            }
            catch (lockedaccountexception lae) {// 用户被锁定
                log.info("****---->用户 " + token.getprincipal() + " 已被锁定");
            }
            catch (authenticationexception ae) { // 其他异常,认证异常的父类
                log.info("****---->用户" + token.getprincipal() + " 验证发生异常");
            }
        }

        // 5.权限测试:
        //5.1判断用户是否有某个角色
        if (currentuser.hasrole("guest")) {
            log.info("****---->用户拥有角色guest!");
        } else {
            log.info("****---->用户没有拥有角色guest");
            return;
        }
        //5.2判断用户是否执行某个操作的权限
        if (currentuser.ispermitted("see")) {
            log.info("****----> 用户拥有执行此功能的权限");
        } else {
            log.info("****---->用户没有拥有执行此功能的权限");
        }

        //6.退出
        system.out.println("****---->" + currentuser.isauthenticated());
        currentuser.logout();
        system.out.println("****---->" + currentuser.isauthenticated());

    }
}


代码解析

解析一下上面的代码做了什么:

对于shiro.ini:

1.在[users]标签下以用户名 = 密码,角色1,角色2...的格式创建了用户
2.在[roles]标签下以角色 = 权限 (* 代表所有权限)的格式为用户分配了角色

对于shirodemo.java:

1.使用shiro.ini来获取了inisecuritymanagerfactory
2.通过inisecuritymanagerfactory获取securitymanager,并绑定到securityutils中
3.使用securityutils获取一个用户识别信息subject
4.对subject对象判断是否已经身份验证(authenticated)
5.将用户名和密码封装成usernamepasswordtoken对象,调用subject对象的login方法来进行登录
6.登录成功后,调用subject对象的hasrole方法来判断用户是否拥有某个角色
7.调用subject对象的ispermitted方法来判断用户是否拥有某个行为
8.调用subject对象的logout方法来退出。


补充

上面展示了一个”登录-权限验证-退出“的流程。但上面的一些代码还是硬编码的,比如说上面的用户名和密码还是从ini表中获取的,比如说上面的权限信息还是从ini中获取的,这些问题都是需要解决的,下面会进行解决。



一些概念

先来了解一些概念。

Shiro的基本使用

  • application code:代表着应用,应用使用subject来标识自己的身份,以及使用subject来进行认证和授权。
  • subject:代表着“当前用户”。
  • shiro securitymanager:应用使用subject来进行认证和授权,实际上执行认证和授权的是securitymanager
  • realm:securitymanager的认证和授权需要使用realm,realm负责获取用户的权限和角色等信息,再返回给securitymanager来进行判断。
  • 【下图是对这些概念的补充】
    Shiro的基本使用

  • authenticator:认证器,对用户身份进行认证的。
  • authorizer:授权器,对用户进行授权的。用来判断用户是否拥有某个权限。
  • realms:补充上面realm的内容:realm是可以有多个的。用户的认证和授权都需要realm,也就是说authenticator和authorizer的实际操作还是交给了realm
  • cachemanager:缓存控制器,来管理如用户、角色、权限等的缓存的
  • cryptography :加密模块,提供对”密码“的加密等功能。



realm


  • realm是真正负责处理认证和授权的组件。
  • securitymanager要完成认证,需要realm返回一个authenticationinfo,authenticationinfo会携带存储起来的正确的用户认证信息,用来与用户提交的信息进行比对,如果信息不匹配,那么会认证失败。
  • securitymanager要完成授权,需要realm返回一个authorizationinfo


认证

下面的例子是以继承了authenticatingrealm的自定义realm来实现自定义认证。
认证依赖于方法dogetauthenticationinfo,需要返回一个authenticationinfo,通常返回一个他的子类simpleauthenticationinfo,构造方法的第一个参数是用户名,第二个是验证密码,第三个是当前realm的classname。


package com.demo.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.authenticatingrealm;

public class myrealm extends authenticatingrealm {
    @override
    protected authenticationinfo dogetauthenticationinfo(authenticationtoken token)  {
        system.out.println("myrealm认证中---->用户:"+token.getprincipal());
        // 可以从token中获取用户名来从数据库中查询数据
        usernamepasswordtoken uptoken = (usernamepasswordtoken) token;
        string password="123456";// 假设这是从数据库中查询到的用户密码
        // 创建一个simpleauthenticationinfo,第一个参数是用户名,第二个是验证密码,第三个是当前realm的classname
        // 验证密码会与用户提交的密码进行比对
        simpleauthenticationinfo info = new simpleauthenticationinfo(uptoken.getusername(),password,this.getname());
        return info;
    }
}


当创建了一个realm之后,需要告诉securitymanager,所以在shiro.ini中配置:

# -------------------------------------------------------------------
[main]
myrealm = com.demo.realms.myrealm
# --------由于自定义认证,所以去除users,roles------------------------

这样子就可以进行自定义认证了,在上面的用户密码中都设置了"123456",所以如果输入的密码不正确都会认证失败。但上面没有设置授权,所以代码中要去掉授权的判断。


授权

下面的例子是以继承了authorizingrealm的自定义realm来实现自定义认证和自定义授权。
授权依赖于方法dogetauthorizationinfo,需要返回一个authorizationinfo,通常返回一个他的子类simpleauthorizationinfo。构造simpleauthorizationinfo可以空构造,也可以传入一个set<string> roles来构造。


package com.demo.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.authorizationinfo;
import org.apache.shiro.authz.simpleauthorizationinfo;
import org.apache.shiro.realm.authorizingrealm;
import org.apache.shiro.subject.principalcollection;

import java.util.hashset;
import java.util.set;

public class realmfordouble extends authorizingrealm {
//    授权
    @override
    protected authorizationinfo dogetauthorizationinfo(principalcollection principals) {
        // 1. 获取授权的用户
        object principal = principals.getprimaryprincipal();
        system.out.println("realmfordouble授权中---->用户:"+principal);
        //2.下面使用set<string> roles来构造simpleauthorizationinfo
        simpleauthorizationinfo info = null;
//        simpleauthorizationinfo info = new simpleauthorizationinfo();
        set<string> roles = new hashset<>();
        if ("admin".equals(principal)){
            roles.add("admin"); // 假设这个角色是从数据库中查出的
            // 如果simpleauthorizationinfo实例化了,
            // 可以这样来加角色,行为需要这样添加
            // 角色可以传构造函数来实例化simpleauthorizationinfo
//            info.addrole("admin");
//            info.addstringpermission("*");
        }
        if ("guest".equals(principal)){
            roles.add("guest");
        }
        info = new simpleauthorizationinfo(roles);
        return info;
    }

//    认证
    @override
    protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception {
        system.out.println("realmfordouble认证中---->用户:"+token.getprincipal());
        usernamepasswordtoken uptoken = (usernamepasswordtoken) token;
        string password="123456";// 假设这是从数据库中查询到的用户密码
        // 创建一个simpleauthenticationinfo,第一个参数是用户名,第二个是验证密码,第三个是当前realm的classname
        // 验证密码会与用户提交的密码进行比对
        simpleauthenticationinfo info = new simpleauthenticationinfo(uptoken.getusername(),password,this.getname());
        return info;
    }
}


ini文件中也需要对应修改:

# -------------------------------------------------------------------
[main]
myrealm = com.demo.realms.realmfordouble
# --------由于自定义认证,所以去除users,roles------------------------


这样子就可以进行自定义认证和授权了,上面的认证信息和授权信息都是可以修改成从数据库中获取的。


几个父类:

authorizingrealm:可以同时认证和授权。
authenticatingrealm:用于认证。
realm:既可以用于认证也可以用于授权。


几个方法

有好几个父类,怎么判断他们能否进行认证或授权呢?

  • 认证依赖于方法dogetauthenticationinfo
  • 授权依赖于方法dogetauthorizationinfo
  • supports(authenticationtoken token)方法可以处理非usernamepasswordtoken的情况。


异常

当认证错误时会抛出异常,下面列举一下常见的异常。
Shiro的基本使用

  • unknownaccountexception:用户不存在的异常
  • excessiveattemptsexception:登录失败次数过多的异常
  • incorrectcredentialsexception:密码不匹配的异常
  • lockedaccountexception:用户已被锁定的异常
  • expiredcredentialsexception:用户登录凭证已过期的异常。
  • authenticationexception:身份认证异常的基类。



集成于spring mvc


依赖包

    <dependencies>
        <dependency>
          <groupid>junit</groupid>
          <artifactid>junit</artifactid>
          <version>4.11</version>
          <scope>test</scope>
        </dependency>
        <dependency>
              <groupid>org.springframework</groupid>
              <artifactid>spring-context</artifactid>
              <version>4.2.4.release</version>
        </dependency>

        <dependency>
              <groupid>org.springframework</groupid>
              <artifactid>spring-webmvc</artifactid>
              <version>4.2.4.release</version>
        </dependency>

          <dependency>
              <groupid>org.springframework</groupid>
              <artifactid>spring-jdbc</artifactid>
              <version>4.2.4.release</version>
          </dependency>

          <dependency>
              <groupid>javax.servlet</groupid>
              <artifactid>jstl</artifactid>
              <version>1.2</version>
              <scope>runtime</scope>
          </dependency>

          <dependency>
              <groupid>javax.servlet</groupid>
              <artifactid>javax.servlet-api</artifactid>
              <version>3.1.0</version>
              <scope>provided</scope>
          </dependency>
          <dependency>
              <groupid>org.slf4j</groupid>
              <artifactid>slf4j-log4j12</artifactid>
              <version>1.6.1</version>
          </dependency>
          <dependency>
              <groupid>log4j</groupid>
              <artifactid>log4j</artifactid>
              <version>1.2.15</version>
          </dependency>

          <dependency>
              <groupid>org.apache.shiro</groupid>
              <artifactid>shiro-ehcache</artifactid>
              <version>1.3.2</version>
          </dependency>
          <dependency>
              <groupid>net.sf.ehcache</groupid>
              <artifactid>ehcache-core</artifactid>
              <version>2.4.3</version>
          </dependency>

          <dependency>
              <groupid>org.apache.shiro</groupid>
              <artifactid>shiro-core</artifactid>
              <version>1.3.2</version>
          </dependency>
          <dependency>
              <groupid>org.apache.shiro</groupid>
              <artifactid>shiro-spring</artifactid>
              <version>1.3.2</version>
          </dependency>
          <dependency>
              <groupid>org.apache.shiro</groupid>
              <artifactid>shiro-web</artifactid>
              <version>1.3.2</version>
          </dependency>

  </dependencies>


步骤

1.先配置shiro拦截器:

  <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>


2.匹配springmvc的dispatcherservlet:

  <servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.dispatcherservlet</servlet-class>
    <init-param>
      <param-name>contextconfiglocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>spring</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>


3.配置spring的contextloaderlistener:

  <context-param>
    <param-name>contextconfiglocation</param-name>
    <param-value>classpath:applicationcontext.xml</param-value>
  </context-param>
  
  <listener>
    <listener-class>org.springframework.web.context.contextloaderlistener</listener-class>
  </listener>


4.配置springmvc.xml【springmvc的配置文件】:

<?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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemalocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.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.0.xsd">

    <context:component-scan base-package="com.progor"></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>


5.在applicationcontext.xml中配置shiro:

<?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:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemalocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.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.0.xsd">

    <!--1. 配置 securitymanager!-->
    <bean id="securitymanager" class="org.apache.shiro.web.mgt.defaultwebsecuritymanager">
        <!--缓存管理器-->
        <property name="cachemanager" ref="cachemanager"/>
        <!--realms-->
        <property name="realms">
            <list>
                <ref bean="jdbcrealm"/>
            </list>
        </property>
    </bean>

    <!--2. 配置 cachemanager缓存管理器.-->
    <bean id="cachemanager" class="org.apache.shiro.cache.ehcache.ehcachemanager">
        <!--缓存配置文件(这里暂不涉及,可以随便拷贝一个)-->
        <property name="cachemanagerconfigfile" value="classpath:ehcache.xml"/>
    </bean>

    <!--3. 配置 realm-->
    <bean id="jdbcrealm" class="com.progor.realms.myrealm">
    </bean>

    <!--4. 配置 lifecyclebeanpostprocessor,用来管理shiro一些bean的生命周期-->
    <bean id="lifecyclebeanpostprocessor" class="org.apache.shiro.spring.lifecyclebeanpostprocessor"/>

    <!--5. 启用shiro 的注解。但必须在配置了 lifecyclebeanpostprocessor 之后才可以使用-->
    <bean class="org.springframework.aop.framework.autoproxy.defaultadvisorautoproxycreator"
          depends-on="lifecyclebeanpostprocessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor">
        <property name="securitymanager" ref="securitymanager"/>
    </bean>

    <!--6. 配置 shirofilter.-->
    <bean id="shirofilter" class="org.apache.shiro.spring.web.shirofilterfactorybean">
        <property name="securitymanager" ref="securitymanager"/>
        <property name="loginurl" value="/login.jsp"/>
        <property name="successurl" value="/list.jsp"/>
        <property name="unauthorizedurl" value="/unauthorized.jsp"/>
        <property name="filterchaindefinitions">
            <value>
                /login.jsp = anon
                /shiro/login = anon
                /shiro/logout = logout
                /** = authc
            </value>
        </property>
    </bean>
</beans>


6.创建控制器来接受登录请求,执行shiro认证:

package com.progor.controller;

import org.apache.shiro.securityutils;
import org.apache.shiro.authc.authenticationexception;
import org.apache.shiro.authc.usernamepasswordtoken;
import org.apache.shiro.subject.subject;
import org.springframework.stereotype.controller;
import org.springframework.web.bind.annotation.requestmapping;

@controller
@requestmapping("/shiro")
public class usercontroller {

    @requestmapping("/login")
    public string login(string username,string password){
        system.out.println(username+":"+password);
        subject currentuser = securityutils.getsubject();
        if (!currentuser.isauthenticated()) {
            usernamepasswordtoken token = new usernamepasswordtoken(username, password);
            token.setrememberme(true);
            try {
                currentuser.login(token);
            }
            catch (authenticationexception ae) {
                system.out.println("登录失败: " + ae.getmessage());
            }
        }
        return "redirect:/admin.jsp";
    }
    @requestmapping("/logout")
    public string logout(){
        subject subject = securityutils.getsubject();
        subject.logout();
        system.out.println("退出成功");
        return "redirect:/login.jsp";
    }
}


7.创建realm:

package com.progor.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.authorizationinfo;
import org.apache.shiro.authz.simpleauthorizationinfo;
import org.apache.shiro.realm.authenticatingrealm;
import org.apache.shiro.realm.authorizingrealm;
import org.apache.shiro.subject.principalcollection;

import java.util.hashset;
import java.util.set;

public class myrealm extends authorizingrealm {
    //    授权
    @override
    protected authorizationinfo dogetauthorizationinfo(principalcollection principals) {
        object principal = principals.getprimaryprincipal();
        system.out.println("realmfordouble授权中---->用户:"+principal);
        simpleauthorizationinfo info = null;
        set<string> roles = new hashset<>();
        if ("admin".equals(principal)){
            roles.add("admin");
        }
        if ("guest".equals(principal)){
            roles.add("guest");
        }
        info = new simpleauthorizationinfo(roles);
        return info;
    }
    //    认证
    @override
    protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception {
        system.out.println("realmfordouble认证中---->用户:"+token.getprincipal());
        usernamepasswordtoken uptoken = (usernamepasswordtoken) token;
        string password="123456";// 假设这是从数据库中查询到的用户密码
        simpleauthenticationinfo info = new simpleauthenticationinfo(uptoken.getusername(),password,this.getname());
        return info;
    }
}


8.创建几个jsp,用于权限测试:
一个用于登录的login.jsp,一个用于验证登录成功的admin.jsp。【预期结果是如果未登录,那么访问admin.jsp会重定向到login.jsp】
login.jsp:

    <form action="shiro/login" method="post">
        username: <input type="text" name="username"/>
        <br><br>
        password: <input type="password" name="password"/>
        <br><br>
        <input type="submit" value="submit"/>
    </form>

admin.jsp:

<body>
    <p>这是admin.jsp</p>
    <a href="shiro/logout">退出</a>
</body>

9.配置ehcache.xml,可以参考https://github.com/apache/shiro/blob/master/samples/spring-mvc/src/main/resources/ehcache.xml


代码解析

1.配置shirofilter是为了让shirofilter能够拦截请求来进行权限判断。
2.applicationcontext中配置的shiro请参考注释。
3.ehcache.xml是缓存管理器的配置文件。


补充

上述的代码简略地演示了在spring环境中shiro的运行流程。下面将会对一些细节进行描述。



shirofilter拦截


上面的shirofilter中有如下图的代码
Shiro的基本使用
这主要是用来定义shirofilter拦截哪些请求,以及怎么拦截请求的。


拦截器链

在上图中,左边是url,右边是拦截器。
常见的拦截器有:

  • anon:任何人都可以访问
  • authc:只有认证后才可以访问
  • logout:只有登录后才可以访问
  • roles[角色名]:只有拥有特定角色才能访问,例如/admin.jsp = roles[user]
  • perms["行为"]:只有拥有某种行为的才能访问,例如/admin/deluser = prems["user:delete"]
  • 想了解更多拦截器,可以参考shiro.apache.org/web.html#default-filters


url匹配

  • 在上图中,有用到/**,这是代表所有请求,是为了拦截其余未定义拦截规则的请求。
  • 其实这透露了url匹配是从上到下的,比如login.jsp由于前面定义了/login.jsp = anon,所以就不会交给/**来拦截了。
  • 另外,是可以有多个拦截器的,所以/admin/** = authc, roles[administrator]也是可以的。


url属性

上面的shirofilter还配置了下图的属性,这是用来定义发生一些情况时跳转到哪个页面的。

  • 比如配置了loginurl,那么发起未认证的请求都会跳转到loginurl
  • successurl是用来定义登录成功后调整到哪个页面(如果controller跳转了视图那么这个失效)
  • unauthorizedurl是用来定义访问不是自己权限的时跳转到哪个页面(普通的authc不会触发,roles会触发。)。
    Shiro的基本使用


拦截器链的自定义

在上面都是使用硬编码的方式来定义拦截器链。下面将解决这个硬编码问题
一种方法是使用filterchainresolver来处理,这里使用map的方式来处理。
定义一个类,核心方法是返回一个linkedhashmap【有序是为了确保从上到下匹配】:

package com.progor.utils;
import java.util.linkedhashmap;

public class filterchainmap {
    // 使用静态工厂
    public static linkedhashmap<string, string> getfilterchainmap(){
        linkedhashmap<string, string> map = new linkedhashmap<>();
        // 下面的数据可以从数据库中查询出来。
        map.put("/login.jsp", "anon");
        map.put("/shiro/login", "anon");
        map.put("/shiro/logout", "logout");
        map.put("/admin.jsp", "authc");
        map.put("/**", "authc");
        return map;
    }
}


修改applicationcontext.xml:

    <bean id="shirofilter" class="org.apache.shiro.spring.web.shirofilterfactorybean">
        <property name="securitymanager" ref="securitymanager"/>
        <property name="loginurl" value="/login.jsp"/>
        <property name="successurl" value="/list.jsp"/>
        <property name="unauthorizedurl" value="/unauthorized.jsp"/>
        <property name="filterchaindefinitionmap" ref="filtechainmap"></property>
        <!--去掉filterchaindefinitions-->
    </bean>
    <!--核心是获取这个map,由于使用了静态工厂,所以这样定义这个bean-->
    <bean id="filtechainmap"  class="com.progor.utils.filterchainmap" factory-method="getfilterchainmap" ></bean>


补充:

上面讲述了shirofilter的配置,解决了请求的拦截问题。



密码加密


在上面的密码比对中,都是使用明文来比对。
而通常来说,被存储起来的用户密码通常都是加密后的。也就是说,在使用simpleauthenticationinfo返回的认证信息时候,里面的密码信息是被加密过的,如果我们直接拿用户提交的明文密码匹配的话就会匹配失败,所以我们应该还需要告诉shiro使用什么加密方式来进行密码比较。
在shiro中,使用credentialsmatcher来解决这个问题。


算法加密

在配置realm的时候,可以定义一个credentialsmatcher属性,例如:

    <bean id="jdbcrealm" class="com.progor.realms.myrealm">
        <property name="credentialsmatcher">
            <bean class="org.apache.shiro.authc.credential.hashedcredentialsmatcher">
                <!--定义加密的算法-->
                <property name="hashalgorithmname" value="md5"></property>
            </bean>
        </property>
    </bean>

Shiro的基本使用


多重加密

密码加密一次后可以得到一串hash值,但还可以进行多次加密来提高安全性。

    <bean id="jdbcrealm" class="com.progor.realms.myrealm">
        <property name="credentialsmatcher">
            <bean class="org.apache.shiro.authc.credential.hashedcredentialsmatcher">
                <!--定义加密的算法-->
                <property name="hashalgorithmname" value="md5"></property>
                <!--定义加密的次数-->
                <property name="hashiterations" value="1024"></property>
            </bean>
        </property>
    </bean>


盐值加密

除了多重加密,还可以加入一个”盐值“来进行加密。一个人的名字可能是会重复的,但如果带上他的身份证的话,那么这个人就是特定唯一的。密码也是如此,直接将密码进行加密可能还是比较容易分析出来的(网上有一些md5的密码库,常见的加密结果很容易查找出来),但如果加入一个具有比较罕见的参数来进行加密的话,那么得到的结果就会难以解析了。
盐值由于不是每一个加密都是一样的,所以不能在realm中设置,需要在返回authenticationinfo时带上,这样securitymanager就会对提交的明文密码依据加密算法、加密次数和盐值来进行加密后再与authenticationinfo中的密码进行比对。

    protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception {
        system.out.println("realmfordouble认证中---->用户:"+token.getprincipal());
        usernamepasswordtoken uptoken = (usernamepasswordtoken) token;
        string password="e10adc3949ba59abbe56e057f20f883e";// md5(123456)
        string salt = "lilei";//假设这个盐值是从数据库中查出的
        bytesource credentialssalt = bytesource.util.bytes(salt);
        simpleauthenticationinfo info = new simpleauthenticationinfo(uptoken.getusername(),password,credentialssalt,this.getname());
        return info;
    }



多realm认证


  • realm是可以有多个的。
  • 多个realm代表可以使用不同来源的认证信息来进行用户认证。【适用于一些需要从多个来源查询认证信息的情况】


多个realm的配置

上面已经讲述过realm的定义方法了,所以这里主要讲怎么让shiro知道这多个realm。
只需要把新的realm配置成bean,并告诉securitymanager即可。

    <bean id="securitymanager" class="org.apache.shiro.web.mgt.defaultwebsecuritymanager">
        <property name="cachemanager" ref="cachemanager"/>
        <property name="realms">
            <list>
                <ref bean="jdbcrealm"/>
                <ref bean="secondrealm"/>
            </list>
        </property>
    </bean>
    <!--省略其他配置 -->
    <bean id="jdbcrealm" class="com.progor.realms.myrealm">
        <property name="credentialsmatcher">
            <bean class="org.apache.shiro.authc.credential.hashedcredentialsmatcher">
                <property name="hashalgorithmname" value="md5"></property>
                <property name="hashiterations" value="1024"></property>
            </bean>
        </property>
    </bean>
    <bean id="secondrealm" class="com.progor.realms.secondrealm"></bean>

对于上面的多个realm的认证,你可以尝试两个地方使用不同的密码来进行测试,借助sysout的话你会发现确实经过了两个realm.


认证策略

  • 对于多个realm的默认的认证策略是只要其中一个通过了即可认证通过,也就是说一个密码不匹配,一个密码匹配,最终的结果将会是认证通过。
  • 这个认证策略是可以修改的,需要对authenticator认证器进行配置。
    • firstsuccessfulstrategy:只要有一个 realm 验证成功即可,只返回第一个 realm 身份验证成功的认证信息,其他的忽略;
    • atleastonesuccessfulstrategy:只要有一个 realm 验证成功即可,和 firstsuccessfulstrategy不同,返回所有 realm 身份验证成功的认证信息;
    • allsuccessfulstrategy:所有 realm 验证成功才算成功,且返回所有 realm 身份验证成功的认证信息,如果有一个失败就失败了。
      可以在applicationcontext.xml中配置authenticator来设置认证策略。
      <bean id="authenticator"
            class="org.apache.shiro.authc.pam.modularrealmauthenticator">
          <property name="authenticationstrategy">
              <bean class="org.apache.shiro.authc.pam.allsuccessfulstrategy"></bean>
          </property>
      </bean>
    使用上述的代码后,需要所有的realm都验证成功才能认证成功。



授权


上面讲了权限拦截,下面讲一下怎么给请求/页面/业务来设置权限。


拦截链式

第一种是上面展示的使用拦截器链的方式,这种方式可以拦截一些请求/页面的非法权限操作。
Shiro的基本使用


编程式

编程式就是在代码中使用hasrole或ispermitted等方法来进行权限判断。

    @requestmapping("/deluser")
    public string deluser(){
        subject subject = securityutils.getsubject();
        if (subject.hasrole("admin")){
            //一系列操作....
            system.out.println("执行了删除用户的操作");
            return "redirect:/admin.jsp";
        }else{
            system.out.println("你没有权限执行");
            return "redirect:/unauthorized.jsp";
        }
    }

除了hashrole,常见的方法还有:

  • hasroles(list<string> roleidentifiers):拥有list中的所有角色才返回true
  • hasallroles(collection<string> roleidentifiers):拥有集合中的所有角色才返回true
  • ispermitted(string... permissions):是否拥有某个行为(支持传入多个参数)


注解式

注解式就是使用注解来进行权限管理。【这些注解不能用在controller中】

public class userservice {
    @requiresroles("admin") // 需要角色admin
    public void deluser(){
        system.out.println("执行了删除用户的操作");
    }
}

除了·@requiresroles("admin"),常见的注解还有:

  • @requirespermissions():是否拥有某个权限
    注意,注解的使用需要以下两个bean:
<bean id="lifecyclebeanpostprocessor" class="org.apache.shiro.spring.lifecyclebeanpostprocessor"/>

<bean class="org.springframework.aop.framework.autoproxy.defaultadvisorautoproxycreator" depends-on="lifecyclebeanpostprocessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor">
    <property name="securitymanager" ref="securitymanager"/>
</bean>


补充

除此之外,还可以在jsp中进行授权,这将在后面再讲。



shiro标签

也可以在jsp中进行授权。


首先导入标签库:
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

  • <shiro:guest></shiro:guest>:当用户没进行认证时,显示标签中的内容。
  • <shiro:user></shiro:user>:当用户进行认证了,显示标签中的内容。
  • <shiro:authenticated></shiro:authenticated>:当用户已经认证时,显示标签中的内容。
  • <shiro:notauthenticated></shiro:notauthenticated>:当用户未认证的时候显示标签中的内容(包括“remember me”的时候)
  • <shiro:principal />:用来获取用户凭证(用户名等)(从authenticationinfo中获取),标签所在的位置将被替换成凭证信息
  • <shiro:principal property="username" />:如果存入的用户凭证是一个对象,那么可以使用property指定获取这个对象中的属性。
  • <shiro:hasrole name="角色"></shiro:hasrole>:拥有指定角色,将显示标签中的内容。
  • <shiro:hasanyroles name="角色1,角色2..."></shiro:hasanyroles>:只要拥有多个角色中的一个就显示标签中的内容。
  • <shiro:lacksrole name="角色"></shiro:lacksrole>:没有某个角色将不显示标签中的内容
  • <shiro:haspermission name="行为"></shiro:haspermission>:如果拥有某个行为的权限,那么显示标签中的内容
  • <shiro:lackspermission name="行为"></shiro:lackspermission>:如果没有拥有某个行为,那么显示标签中内容


示例

<!-- 一个未登录的场景 -->
<shiro:guest>
    hi there!  please <a href="login.jsp">login</a> or <a href="signup.jsp">signup</a> today!
</shiro:guest>

<!-- 已登录过,准备切换其他用户的场景 -->
<shiro:user>
    welcome back john!  not john? click <a href="login.jsp">here<a> to login.
</shiro:user>

<!-- 显示登录用户的用户名的场景 -->
hello, <shiro:principal/>, how are you today?

<!-- 用户已经认证通过的场景 -->
<shiro:authenticated>
    <a href="/logout">退出</a>.
</shiro:authenticated>

<!-- 拥有某个角色的场景 -->
<shiro:hasrole name="administrator">
    <a href="createuser.jsp">创建用户</a>
</shiro:hasrole>

<!-- 拥有某个行为的场景 -->
<shiro:haspermission name="user:create">
    <a href="createuser.jsp">创建用户</a>
</shiro:haspermission>



remember me


remember me 主要用于再次访问时仍然保留认证状态的场景。例如,离开某个网站后,两三天再打开仍然保留你的登录信息。


setrememberme

remember me的功能的一个前提是在认证时使用了setrememberme :
Shiro的基本使用
为true才会“记住我”。


rememberme权限级别

记住我的权限并不是authc,而是user【用户已经身份验证/记住我】
所以做实验的要记得修改拦截器链。


参数设置

maxage:过期时间
httponly:禁止使用js脚本读取到cookie信息
【其他的不太常用,有兴趣的自查。还有domain之类的】
一种配置方法:

<bean id="securitymanager" class="org.apache.shiro.web.mgt.defaultwebsecuritymanager">
        <property name="cachemanager" ref="cachemanager"/>
        <property name="realms">
            <list>
                <ref bean="jdbcrealm"/>
            </list>
        </property>
        <property name="remembermemanager" ref="remembermemanager"/>
    </bean>
    
    <bean id="remembermecookie" class="org.apache.shiro.web.servlet.simplecookie">
        <constructor-arg value="rememberme"/><!-- cookie的名称 -->
        <property name="httponly" value="true"/>
        <property name="maxage" value="60"/><!-- 过期时间:60s -->
    </bean>

    <bean id="remembermemanager"  class="org.apache.shiro.web.mgt.cookieremembermemanager">
        <property name="cookie" ref="remembermecookie"/>
    </bean>

第二种配置方法:

    <bean id="securitymanager" class="org.apache.shiro.web.mgt.defaultwebsecuritymanager">
        <property name="cachemanager" ref="cachemanager"/>
        <property name="realms">
            <list>
                <ref bean="jdbcrealm"/>
            </list>
        </property>
        <property name="remembermemanager.cookie.maxage" value="15"/>
    </bean>


补充

  • remember借助了cookie来实现记住登录状态,但这是不太安全的,因为(黑客)把cookie窃取了也能进行登录。



结语

这里仅仅只是“开了个门”,shiro的世界还有很多广阔的地方。比如会话管理、单点登录【这些什么时候有空再写吧】
如果你想了解更多,可以参考shiro官方参考手册http://shiro.apache.org/reference.html;
除此之外,张开涛的shiro的pdf也是可以值得一看的。