手把手教你玩转Shiro(2)

如果没有看第一篇关于Shiro的,可以先看看这篇文章哦,因为是一个前导知识  Shiro前导知识 ,,对于深层次理解会有很好的效果呢!而且本篇也有利用到前面写的知识点哦。~

一:Shiro的认识

       Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
既然shiro将安全认证相关的功能抽取出来组成一个框架,使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
shiro使用广泛,shiro可以运行在web应用,非web应用,集群分布式应用中越来越多的用户开始使用shiro。
java领域中spring security(原名Acegi)也是一个开源的权限管理框架,但是spring security依赖spring运行,而shiro就相对独立,最主要是因为shiro使用简单、灵活,所以现在越来越多的用户选择shiro。


架构的详细分析:

Subject
Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
SecurityManager 
SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
Authenticator
Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
Authorizer
Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
realm
Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
sessionManager
       sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDAO
        SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
CacheManager
        CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
Cryptography
Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

二:SpringMVC+Spring+Hibernate+Shiro的整合

步骤:

第一步:整合SpringMVC+Spring+Hibernate三大框架

关于这个的话,我就不多说了,因为我之前的那一篇专题就是写的这个,如果不是很懂的就可以看一下该文章SSH框架的整合

第二步:整合Shiro

导包:shiro-core , shiro-web , shiro-sping , shiro-ehcache,shiro-quartz(任务调度)或者可以直接使用shiro-all的这个jar包,但是不建议这样使用,因为会添加多余的一些jar。

认证流程:(后面就可以按照这个过程进行配置)


(1)web.xml中添加shiro配置

<!--配置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)Spring容器中的application.xml文件中添加shiro配置

<!--
        配置shiro的内容
    一:配置SecurityManager
    二:配置echche
    三:配置realm
    四:配置bean生命周期管理
    五:启用IOC容器的shiro注解
    六:配置ShiroFilter,其中的I必须和web,xml中的shirofileter名字一样
    -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator"></property>
        <property name="realms" ref="jdbcRealm"/>
    </bean>
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>
    <bean id="authenticator"
          class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
        </property>
    </bean>
    <!--配置一个realm实现类,该类实现realm接口-->
    <bean id="jdbcRealm" class="com.hnu.scw.realm.MyShiroRealm"></bean>
    <!--管理spring容器中的IOC的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>
    <!--配置shiro管理页面的控制过滤器,下面这个id必须和web.xml中配置的shiro的name一样,如果不一样就会在加载的时候报错,因为在初始化的时候,就会去找对应web.xml中的filter-name的这个name的bean,找不到就必然出错了-->
    <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"/>
        <!--
        	配置哪些页面需要受保护.,其实就是一个过滤链
        	以及访问这些页面需要的权限.
        	1). anon 可以被匿名访问
        	2). authc 必须认证(即登录)后才可能访问的页面.
        	3). logout 登出.
        	4). roles 角色过滤器
        -->
        <property name="filterChainDefinitions">
            <value>
                <!--
                anon表示的是能够进行匿名访问的url,这里就只设置了login.jsp可以为通过url访问
                /** 表示的是通配符的情况,其他的都要进行权限验证处理,另外可以用?匹配一个字符,*匹配多个,**匹配路径中的零个或者多个路径
                -->
                /login.jsp = anon
                /** = authc
            </value>
        </property>
    </bean>

关于上面写的那些页面的控制的内容的详细解析如下:

过滤器简称

对应的java类

anon

org.apache.shiro.web.filter.authc.AnonymousFilter

authc

org.apache.shiro.web.filter.authc.FormAuthenticationFilter

authcBasic

org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

perms

org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

port

org.apache.shiro.web.filter.authz.PortFilter

rest

org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter

roles

org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

ssl

org.apache.shiro.web.filter.authz.SslFilter

user

org.apache.shiro.web.filter.authc.UserFilter

logout

org.apache.shiro.web.filter.authc.LogoutFilter


anon:例子/admins/**=anon 没有参数,表示可以匿名使用。

authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,FormAuthenticationFilter是表单认证,没有参数

roles例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。

perms例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"]当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。

rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为postgetdelete等。

port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString

是你访问的url里的?后面的参数。

authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证

ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https

user:例如/admins/user/**=user没有参数表示必须存在用户身份认证通过或通过记住我认证通过的可以访问,当登入操作时不做检查

注:

anonauthcBasicauchcuser是认证过滤器,

permsrolessslrestport是授权过滤器


这是搭建项目的JSP页面的结构:

下面这是写的实现realm接口的类----------对应配置文件中的设置

package com.hnu.scw.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.realm.Realm;

/**
 * @author scw
 * @create 2018-01-09 11:30
 * @desc 实现realm接口,便于shiro框架
 **/
public class MyShiroRealm implements Realm {
    @Override
    public String getName() {
        return null;
    }

    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        return false;
    }

    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}

OK,这个就已经搭建成功了,那么我们直接运行:


那么,我们修改一下url地址,测试一下,看是否没有进行过权限验证的处理,能否进行跳转呢?


结果的话,那么就自己亲自运行一下呗~!!(其实,它会默认到login.jsp页面中去!!!原因就是没有经过权限验证处理)

三:Shiro的工作流程

描述:通过下面这个图,从而来了解shiro的验证流程。(也对上面二中出现的url访问的结果进行解析)


四:shiro的验证详解


(1)验证的大体过程


(2)实现验证过程

1:登陆界面--login.jsp

<%--
  Created by IntelliJ IDEA.
  User: scw
  Date: 2018/1/9 0009
  Time: 13:27
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登陆</title>
</head>
<body>
<h1>欢迎登陆</h1>
<form action="/shiro/login" method="post">
    账号:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    <input type="submit"  value="登陆">
</form>
</body>
</html>

2:验证登陆的controller

package com.hnu.scw.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
 * @author Administrator
 * @create 2018-01-09 16:06
 * @desc 进行shiro处理的controller类
 **/
@Controller
@RequestMapping(value = "/shiro")
public class ShiroController {
    @RequestMapping(value = "/login")
    public String login(String username , String password){
        //获取subject对象
        Subject currentUser = SecurityUtils.getSubject();
        //判断是否已经有权限
        if(!currentUser.isAuthenticated()){
            //把用户名和密码封装成usernamepasswordToken对象
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            //设置记住token
            token.setRememberMe(true);
            currentUser.login(token);
        }
        return "success";
    }
}

3:shiro登陆验证

package com.hnu.scw.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.Realm;
/**
 * @author scw
 * @create 2018-01-09 11:30
 * @desc 实现realm接口,便于shiro框架
 **/
public class MyShiroRealm extends AuthenticatingRealm {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1:将token转为usernameandpaswordToken对象
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
        //2:获取到用户名和密码
        String username = usernamePasswordToken.getUsername();
        char[] password = usernamePasswordToken.getPassword();
        //3:进行数据库方法操作,获取到username对应的数据信息进行验证(这里就模拟进行了操作)
        System.out.println("我在进行获取用户操作");
        //4:如果输入的是unknown账号,则抛出用户不存在异常(当然,这里只是模拟一下不存在用户的状态)
        if("unknow".equals(username)){
            throw new UnknownAccountException("用户不存在");
        }//假设输入的为master就为已锁定的用户
        else if("master".equals(username)){
            throw new LockedAccountException("用户已锁定");
        }
        //5:根据用户的情况,来构建AuthenticationInfo对象,通常使用SimpleAuthenticationInfo对象
        Object principal = username;
        //6:设置登陆密码(这里只是进行模拟一下,实际情况是判断输入的和获取到的用户信息的对比)
        Object credentials = "123456";
        String realmName = getName();
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, realmName);
        System.out.println("正在进行验证处理");
        return simpleAuthenticationInfo;
    }
}

4:登陆成功界面success.jsp

<%--
  Created by IntelliJ IDEA.
  User: Administrator
  Date: 2018/1/9 0009
  Time: 16:19
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登陆成功</title>
</head>
<body>
<h1>登陆成功</h1>
<a href="/shiro/loginout">退出登陆</a>
</body>
</html>
总结:

1:当我们输入账号为"unknow"的时候,就会抛出用户不存在的异常

2:当我们输入账号为"master"的时候,就会抛出用户锁定的异常

3:当我们输入其他的用户名,并且密码是为123456的时候,那么就会到登陆成功的界面

4:但是,如果我们在进行登陆了一次成功之后,我们再用(1)和(2)的账号进行登陆的时候,我们会奇迹的发现,竟然现在可以直接登陆成功了???那么,这个肯定是不行的,到底是怎么一回事呢?其实很简单,都是因为shiro缓存的原因,所以这也就是为什么我在登陆成功之后,会有一个退出登陆的超链接。那为什么,当我们点击那个链接进行退出之后,就又回到正常的状态了呢?其实,这里面有一个知识点,那就是shiro的logout的设置。比如下面的设置:

 <!--配置shiro管理页面的控制过滤器 , 其中的Id必须和web,xml中的shiro中的fileter-name名字一样-->
    <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"/>
        <!--
        	配置哪些页面需要受保护.
        	以及访问这些页面需要的权限.
        	1). anon 可以被匿名访问
        	2). authc 必须认证(即登录)后才可能访问的页面.
        	3). logout 登出.
        	4). roles 角色过滤器
        -->
        <property name="filterChainDefinitions">
            <value>
                <!--
                anon表示的是能够进行匿名访问的url,这里就只设置了login.jsp可以为通过url访问
                /** 表示的是通配符的情况,其他的都要进行权限验证处理
                -->
                /login.jsp = anon
                /shiro/loginout=logout        //这里就是设置退出登陆的链接,这其实配置的链接可以是不存在的,但是一定要对应着JSP中页面对应的那个退出按钮的链接即可,反正保持它们一致即可。
                /shiro/login = anon
                /** = authc
            </value>
        </property>
    </bean>
根据上面的内容,就是通过设置了/shiro/loginout = logout来实现的,这也就是和我们登陆退出的那个链接一样的。所以,现在明白了它里面的处理机制了吧。

5:通过上面的代码,其实我们在验证的时候,发现了一个问题,就是验证密码是一个明文的形式,那么这个肯定是不安全的,那么就需要对密码进行加密验证。那么,下面就说一下,shiro中的盐值MD5加密。

知识点:

首先说一下,为什么需要用到盐值,我们在开发的时候会遇到一个情况,就是可能用户的用户名不同,但是密码相同,那么这样在保存到数据库的时候就可以看到密码相同的内容了,为了避免出现这样的问题,所以需要利用另外一个唯一的字段结合MD5的密码加密生成一个新的密码,从而达到一种不会出现任何相同密码的情况。(也就是说,即使密码相同,但是用户名不一样,那么密码就不会相同了)

步骤:需要在spring容器中添加关于realm的配置内容:

第一步:

<!--配置一个realm实现类,该类实现realm接口-->
    <bean id="jdbcRealm" class="com.hnu.scw.realm.MyShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"></property>
                <!--设置MD5加密的次数,因为有时候一次过于简单,所以就多几次-->
                <property name="hashIterations" value="1024"></property>
            </bean>
        </property>
    </bean>
第二步:编写realm验证实现类
package com.hnu.scw.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;
/**
 * @author scw
 * @create 2018-01-09 11:30
 * @desc 实现realm接口,便于shiro框架
 **/
public class MyShiroRealm extends AuthenticatingRealm {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1:将token转为usernameandpaswordToken对象
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
        //2:获取到用户名和密码
        String username = usernamePasswordToken.getUsername();
        char[] password = usernamePasswordToken.getPassword();
        //3:进行数据库方法操作,获取到username对应的数据信息进行验证(这里就模拟进行了操作)
        System.out.println("我在进行获取用户操作");
        //4:如果用户不存在,则抛出异常
        if("unknow".equals(username)){
            throw new UnknownAccountException("用户不存在");
        }
        else if("master".equals(username)){
            throw new LockedAccountException("用户已锁定");
        }
        //5:根据用户的情况,来构建AuthenticationInfo对象,通常使用SimpleAuthenticationInfo对象
        Object principal = username;
        //6:设置登陆密码(这里只是进行模拟一下,实际情况是判断输入的和获取到的用户信息的对比)
        Object credentials = null;
        if("admin".equals(username)){
           credentials = "038bdaf98f2037b31f1e75b5b4c9b26e"; //这个通过下面的那个main方法来进行假设生成(当然,以后在实际的项目中,肯定是在注册用户的时候,这个就用main中的形式进行生成然后保存到数据库中的啦,这里就再通过利用即可)
        }else if("customer".equals(username)){
           credentials = "53734d23e8a0c90aec94d0dbd2685675"; //同理,这个也是通过main方法进行假设生成
        }
        //设置盐值----这里必须要用一个唯一的值,我这里就假设用户名是唯一的,所以就传入username
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        //获取到真实名,这个直接调用父类的方法即可
        String realmName = getName();
        SimpleAuthenticationInfo simpleAuthenticationInfo = null;
        //如果要用盐值加密的话,就要用到下面的这个构造方法
        simpleAuthenticationInfo =  new SimpleAuthenticationInfo(principal ,credentials ,credentialsSalt , realmName);
        return simpleAuthenticationInfo;
    }
    /**
     * 通过盐值和MD5加密,生成密码
     */
    public static void main(String[] args){
        //设置加密的形式
        String hashAlgorithmName = "MD5";
        //设置加密的密码
        Object credentials = "123456";
        //设置加密的盐值
        Object salt = ByteSource.Util.bytes("customer");
        //设置MD5加密的次数
        int hashIterations = 1024;
        //生成MD5通过盐值加密后的密码(其实,这个就是在我们进行注册用户的时候,密码就可以这样设置,
        // 所以上面就可以通过这样的方式再进行验证)
        Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    } 
}

第三步:进行测试

通过上面的形式的话,我们即使用户名用"admin"和"customer"的话,生成的密码就也会不一样,那么我们保存到数据库中内容也就不一样啦。这样是不是比单纯的MD5更加安全了呢?

知识点:MD5是一种不可逆的加密算法,这样的好处在于不能够轻易进行逆向处理密码了,所以相对更加安全,另外的话,还可以采用SHA-1的方法哦。

OK,上面模拟的是一些虚拟操作,即不是通过数据库来进行的获取用户信息的,那么实际开发中,具体的认证代码又是怎么杨的呢?可以看看下面的代码:(具体根据需求来,我写的这个只是最基本的构造)

//realm的认证方法,从数据库查询用户信息
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {
		
		// token是用户输入的用户名和密码 
		// 第一步从token中取出用户名
		String userCode = (String) token.getPrincipal();

		// 第二步:根据用户输入的userCode从数据库查询
		SysUser sysUser = null;
		try {
			sysUser = sysService.findSysUserByUserCode(userCode);//就是调用获取用户名的相关信息,这个service调用mapper的方法就不写了,很简单,就是一个select操作
		} catch (Exception e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		// 如果查询不到返回null
		if(sysUser==null){//
			return null;
		}
		// 从数据库查询到密码
		String password = sysUser.getPassword();		
		//盐
		String salt = sysUser.getSalt();
		// 如果查询到返回认证信息AuthenticationInfo	
		//activeUser就是用户身份信息(关于这个对象,我们看看我上一篇关于shiro的文章)
		ActiveUser activeUser = new ActiveUser();	
		activeUser.setUserid(sysUser.getId());
		activeUser.setUsercode(sysUser.getUsercode());
		activeUser.setUsername(sysUser.getUsername());
		//..
		//根据用户id取出菜单
		List<SysPermission> menus  = null;
		try {
			//通过service取出菜单 
			menus = sysService.findMenuListByUserId(sysUser.getId());
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//将用户菜单 设置到activeUser
		activeUser.setMenus(menus);
		//将activeUser设置simpleAuthenticationInfo
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
				activeUser, password,ByteSource.Util.bytes(salt), this.getName());
		return simpleAuthenticationInfo;
	}

那么,认证的已经实现了,那么具体的controller中应该是如何的呢?我们可以有下面的一个登陆验证的处理方式:

//登陆提交地址,和applicationContext-shiro.xml中配置的loginurl一致
	@RequestMapping("login")
	public String login(HttpServletRequest request)throws Exception{
		//如果登陆失败从request中获取认证异常信息,shiroLoginFailure就是shiro异常类的全限定名
		String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
		//根据shiro返回的异常类路径判断,抛出指定异常信息
		if(exceptionClassName!=null){
			if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
				//最终会抛给异常处理器
				throw new CustomException("账号不存在"); //这个是自定义的异常
			} else if (IncorrectCredentialsException.class.getName().equals(
					exceptionClassName)) {
				throw new CustomException("用户名/密码错误");
			} else if("randomCodeError".equals(exceptionClassName)){
				throw new CustomException("验证码错误 ");
			}else {
				throw new Exception();//最终在异常处理器生成未知错误
			}
		}
		//此方法不处理登陆成功(认证成功),shiro认证成功会自动跳转到上一个请求路径
		//登陆失败还到login页面
		return "login";
	}
注意:上面,是有额外写了一个springmvc统一处理异常的处理方法的。这里就不多介绍,在我其他的关于springmvc的文章都有很详细的说过这个知识点。(更直接的,我们看我上一篇关于shiro的文章,在里面就有写到这个问题哦!!!)

五:多realm的验证

描述:在开发项目的时候,我们有时候会碰到一种情况,就是说,使用了不同种数据源,比如有些数据表是在oracle,有些是在mysql中,那么也采用了不同的加密算法来进行保存数据,所以,就需要用到多realm的加密策略。

具体的步骤如下:

第一步:配置Spring容器中关于shiro的配置,在上面的基础上,添加如下的内容:(原来只配了一个jdbcRealm)

<!--配置一个realm管理-->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="realms">
            <list>
                <ref bean="jdbcRealm"></ref>
                <ref bean="jdbcRealm2"></ref>
            </list>
        </property>
    </bean>
    <!--配置一个realm实现类,该类实现realm接口,使用的是MD5加密-->
    <bean id="jdbcRealm" class="com.hnu.scw.realm.MyShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"></property>
                <!--设置MD5加密的次数,因为有时候一次过于简单,所以就多几次-->
                <property name="hashIterations" value="1024"></property>
            </bean>
        </property>
    </bean>
    <!--再配置一个realm实现类,该类实现realm接口,使用的是SHA1加密-->
    <bean id="jdbcRealm2" class="com.hnu.scw.realm.MyShiroRealm2">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="SHA1"></property>
                <!--设置MD5加密的次数,因为有时候一次过于简单,所以就多几次-->
                <property name="hashIterations" value="1024"></property>
            </bean>
        </property>
    </bean>

知识点:也许你觉得上面这样配置了就很好了,那么就有点要出乎你的意料了,因为,上面的这样配置并不是最好的,因为在查看源码的时候可以发现,其实在加载多个realm的时候,其实是通过securityManager来通过set方法来进行多个realm的装配的,所以,把对于realms的管理放到securityManger的bean中进行管理将会更好,所以,应该是配置成如下:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator"></property>
        <property name="realms">      <!--将bean中的authenticator的关于realms的配置放到如下位置-->
            <list>
                <ref bean="jdbcRealm"></ref>
                <ref bean="jdbcRealm2"></ref>
            </list>
        </property>
    </bean>
第二步:编写继承AuthenticatingRealm的类(其实这个和配置一个的时候的内容基本一样,就是该一下加密算法而已,因为在上面,我的第一个realm是用的MD5加密,而这里就使用SHA1加密算法,所以这就可以解决不同种数据源,就采用不同的加密算法)

package com.hnu.scw.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;
/**
 * @author scw
 * @create 2018-01-09 11:30
 * @desc 实现realm接口,便于shiro框架
 * 设置该realm为第二个,并且该realm实现的是SHA1加密
 **/
public class MyShiroRealm2 extends AuthenticatingRealm {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
       System.out.println("我是第二个realm!!!!!!!!!!!");
        //1:将token转为usernameandpaswordToken对象
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
        //2:获取到用户名和密码
        String username = usernamePasswordToken.getUsername();
        char[] password = usernamePasswordToken.getPassword();
        //3:进行数据库方法操作,获取到username对应的数据信息进行验证(这里就模拟进行了操作)
        System.out.println("我在进行获取用户操作");
        //4:如果用户不存在,则抛出异常
        if("unknow".equals(username)){
            throw new UnknownAccountException("用户不存在");
        }
        else if("master".equals(username)){
            throw new LockedAccountException("用户已锁定");
        }
        //5:根据用户的情况,来构建AuthenticationInfo对象,通常使用SimpleAuthenticationInfo对象
        Object principal = username;
        //6:设置登陆密码(这里只是进行模拟一下,实际情况是判断输入的和获取到的用户信息的对比)
        Object credentials = null;
        if("admin".equals(username)){
           credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06";
        }else if("customer".equals(username)){
           credentials = "02b74f7abdea2e99e2a9b8c0414e98fbdc64fd0a";
        }
        //设置盐值----这里必须要用一个唯一的值,我这里就假设用户名是唯一的,所以就传入username
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        //获取到真实名,这个直接调用父类的方法即可,其实获取到该类的绝对路径
        String realmName = getName();
        SimpleAuthenticationInfo simpleAuthenticationInfo = null;
        //如果要用盐值加密的话,就要用到下面的这个构造方法
        simpleAuthenticationInfo =  new SimpleAuthenticationInfo(principal ,credentials ,credentialsSalt , realmName);
        return simpleAuthenticationInfo;
    }
    /**
     * 通过盐值和MD5加密,生成密码
     */
    public static void main(String[] args){
        //设置加密的形式,使用SHA1加密
        String hashAlgorithmName = "SHA1";
        //设置加密的密码
        Object credentials = "123456";
        //设置加密的盐值
        Object salt = ByteSource.Util.bytes("admin");
        //设置MD5加密的次数
        int hashIterations = 1024;
        //生成MD5通过盐值加密后的密码(其实,这个就是在我们进行注册用户的时候,密码就可以这样设置,
        // 所以上面就可以通过这样的方式再进行验证)
        Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }
}
第三步:修改多realm验证策略(这个是属于可选配置)

描述:这个设置主要是对于不同的需求的时候的不同验证详情进行设置,因为我们在开发中,有时候考虑到必须经过所有的realm验证才行,或者有时候我们又只需要验证通过其中的某一个验证即可。所以,通过下面的设置就能够实现这个效果。
在Spring容器中修改为如下信息:

<!--配置一个realm管理-->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <!--设置多个realm的验证策略,ModularRealmAuthenticator默认是使用的AtLeastOneSuccessFulStractegy,
        即只需要验证通过某一个realm即可就算验证通过-->
        <!--下面是设置为必须通过所有的realm验证,才算真正的验证-->
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy" />
        </property>
    </bean>

六:授权


样例分析:比如当成功登陆之后,进入了登陆页面,但是由于权限不同,里面很多标签显示都是针对不同的用户类型,那么就需要针对不同的角色,而实现不同的权限控制。所以:

步骤:

第一步:配置授权内容

登陆成功后的html代码:

<%--
  Created by IntelliJ IDEA.
  User: Administrator
  Date: 2018/1/9 0009
  Time: 16:19
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登陆成功</title>
</head>
<body>
<h1>登陆成功</h1>
<a href="/admin.jsp">管理员界面</a>
<a href="/user.jsp">用户界面</a>
<a href="/shiro/loginout">退出登陆</a>
</body>
</html>
在Spring容器配置文件中添加,授权认证:(这里就假设admin.jsp是需要admin权限,user.jsp需要customer权限)
 <property name="filterChainDefinitions">
            <value>
                <!--
                anon表示的是能够进行匿名访问的url,这里就只设置了login.jsp可以为通过url访问
                /** 表示的是通配符的情况,其他的都要进行权限验证处理
                -->
                /login.jsp = anon
                /shiro/login = anon
                /shiro/loginout=logout
                /admin.jsp = roles[admin]      <!--配置不同授权页面-->
                /user.jsp = roles[customer]
                /** = authc
            </value>
        </property>
如果通过上面的配置之后,你点击那些链接,你就会发生,你会自动跳转到之前设置好的没有授权的页面去了。
第二步:实现授权

知识点:因为,在之前的内容中,我们都没有讲到关于授权的内容,都只是说的认证过程,所以都是继承的AuthenticatingRealm这个类,那现在要实现授权,我们就需要继承的是AuthorizingRealm,这个类包含认证和授权方法,因为要实现这个类中的授权方法,而之前的那个类只有认证方法。

具体的授权类代码:

package com.hnu.scw.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import java.util.HashSet;
import java.util.Set;

/**
 * @author scw
 * @create 2018-01-09 11:30
 * @desc 实现realm接口,便于shiro框架
 **/
public class MyShiroRealm extends AuthorizingRealm {
    /**
     * 用于认证的方法
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("我是第一个realm!!!!!!!!!!!");
        //1:将token转为usernameandpaswordToken对象
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
        //2:获取到用户名和密码
        String username = usernamePasswordToken.getUsername();
        char[] password = usernamePasswordToken.getPassword();
        //3:进行数据库方法操作,获取到username对应的数据信息进行验证(这里就模拟进行了操作)
        System.out.println("我在进行获取用户操作");
        //4:如果用户不存在,则抛出异常
        if("unknow".equals(username)){
            throw new UnknownAccountException("用户不存在");
        }
        else if("master".equals(username)){
            throw new LockedAccountException("用户已锁定");
        }
        //5:根据用户的情况,来构建AuthenticationInfo对象,通常使用SimpleAuthenticationInfo对象
        Object principal = username;
        //6:设置登陆密码(这里只是进行模拟一下,实际情况是判断输入的和获取到的用户信息的对比)
        Object credentials = null;
        if("admin".equals(username)){
           credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
        }else if("customer".equals(username)){
           credentials = "53734d23e8a0c90aec94d0dbd2685675";
        }
        //设置盐值----这里必须要用一个唯一的值,我这里就假设用户名是唯一的,所以就传入username
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        //获取到真实名,这个直接调用父类的方法即可,其实获取到该类的绝对路径
        String realmName = getName();
        SimpleAuthenticationInfo simpleAuthenticationInfo = null;
        //如果要用盐值加密的话,就要用到下面的这个构造方法
        simpleAuthenticationInfo =  new SimpleAuthenticationInfo(principal ,credentials ,credentialsSalt , realmName);
        return simpleAuthenticationInfo;
    }
    /**
     * 通过盐值和MD5加密,生成密码
     */
    public static void main(String[] args){
        //设置加密的形式
        String hashAlgorithmName = "MD5";
        //设置加密的密码
        Object credentials = "123456";
        //设置加密的盐值
        Object salt = ByteSource.Util.bytes("customer");
        //设置MD5加密的次数
        int hashIterations = 1024;
        //生成MD5通过盐值加密后的密码(其实,这个就是在我们进行注册用户的时候,密码就可以这样设置,
        // 所以上面就可以通过这样的方式再进行验证)
        Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }

    /**
     * 进行授权的方法
     * @param principalCollection  这个就是我们在认证过程的时候,传送的参数
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1:获取认证传递的内容(注意一点:如果我们有配了多个realm验证,
        // 那么这里面是按照在我们配置文件中配置的realm验证顺序传递过来的内容,因为底层是一个LinkHashSet的集合,)
        Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
        //2:利用登录信息来判断该用户的角色或者权限信息
        // (这个可能是我们在认证的时候就传送过来,也有可能是再进行数据库查询操作,所以具体根据我们认证过程传送的内容判断)
        //因为我上面都是传的用户名信息,所以我这里也用用户名来判断了,记住这是根据你们认证传送的内容来的。
        Set<String> roles = new HashSet<String>();
        //为每个登陆进来的用户都给一个customer的权限
        roles.add("customer");
        //如果获取到的信息是admin,那么我这就给一个admin的权限认证信息,这样就相当于有了admin的权限
        if("admin".equals(primaryPrincipal)){
            roles.add("admin");
        }
        //3:构造一个SimpleAuthorizationInfo对象,用于传递授权信息
        SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo(roles);
        return simpleAuthenticationInfo;
    }
}
通过这样的方式之后,然后再运行,你就会发生,如果用用户名是"admin"的用户,那么可以登陆成功之后,就是可以进入管理员界面和用户界面的,而用"customer"账号登陆的,就只能进入用户界面。

咳咳,我们这样会发现,其实我们压根就可以在认证登陆之后,判断该用户认证信息的时候,就可以给不同的显示内容了,这样就可以不要授权的显示了呀。是的,其实这个是可以的,那么就可以通过shiro标签来实现,紧接着看下面:

知识点:上面用的是基于角色的授权认证,是否还记得我在第一篇文章中说到的,我们基于资源的授权比基于角色的授权的方式好很多,那么上面是怎么看出来是基于角色的呢?其实,从我们在配置文件中的shiroFilter中可以看出来,我们配置的URL中,是利用role这个来进行控制,所以很明显是基于角色;那么如何基于资源授权呢?

其实很简单,就是将role变成,ur了= perms[user:add:*],类似这样的方式即可,对于这中形式的格式为什么是这样,请看文章开始的第二点知识哦!!!然后,对于授权中的realm类中,其余的没什么变化,就是把对于role的变成添加对应的perms中的内容即可。。。。是不是很简单呢?所以,关键还是要看懂我上面写的内容!。

具体的可以看看下面的代码:(下面这个代码就是在开发过程中实际的情况处理代码了,而且看这个代码,最好再回头看看上面小点中关于认证的部分的实际开发代码,这样结合起来看,效果更好)

// 用于授权
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection principals) {
		
		//从 principals获取主身份信息
		//将getPrimaryPrincipal方法返回值转为真实身份类型(在上边的doGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo中身份类型),
		ActiveUser activeUser =  (ActiveUser) principals.getPrimaryPrincipal();
		
		//根据身份信息获取权限信息
		//从数据库获取到权限数据
		List<SysPermission> permissionList = null;
		try {
			permissionList = sysService.findPermissionListByUserId(activeUser.getUserid());
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//单独定一个集合对象 
		List<String> permissions = new ArrayList<String>();
		if(permissionList!=null){
			for(SysPermission sysPermission:permissionList){
				//将数据库中的权限标签 符放入集合
				permissions.add(sysPermission.getPercode());
			}
		}		
		//查到权限数据,返回授权信息(要包括 上边的permissions)
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		//将上边查询到授权信息填充到simpleAuthorizationInfo对象中
		simpleAuthorizationInfo.addStringPermissions(permissions);
		return simpleAuthorizationInfo;
	}

七:Shiro标签

<%--
  Created by IntelliJ IDEA.
  User: scw
  Date: 2018/1/9 0009
  Time: 16:19
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!--添加shiro标签的引用-->
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<html>
<head>
    <title>登陆成功</title>
</head>
<body>
<h1>登陆成功</h1>
<!--游客标签,即用户没有身份验证(没有调用subject.login方法)时候显示相应信息,即用于游客访问信息的显示-->
<shiro:guest>
    欢迎游客访问,<a href="/login.jsp">登陆</a>
</shiro:guest>
<br>

<!--是登陆的用户-->
<shiro:user>
    我是已经登陆的用户
</shiro:user>
<br>

<!--用户已经身份验证通过,即Subject.login登陆成功,不是通过记住我登陆的-->
<shiro:authenticated>
    用户:<shiro:principal/>通过身份验证
</shiro:authenticated>
<br>
<!--用户未进行身份验证通过,即没有调用Subject.login,包括记住我自动登陆的也属于未进行身份验证-->
<shiro:notAuthenticated>
    用户:<shiro:principal/>未进行过身份验证
</shiro:notAuthenticated>
<br>

<!--判断是否有设定的name属性的角色-->
<shiro:hasRole name="admin">
    <a href="/admin.jsp">管理员界面</a>
</shiro:hasRole>
<br>
<!--判断是否有设定的name属性的角色-->
<shiro:hasRole name="customer">
    <a href="/user.jsp">用户界面</a>
</shiro:hasRole>
<br>

<!--判断是否有任何的role角色都会显示内容-->
<shiro:hasAnyRoles name="admin,customer">
    <h4>你是一个有角色的用户</h4>
</shiro:hasAnyRoles>
<br>
<!--判断当然subject有权限将显示下面内容-->
<shiro:hasPermission name="admin:create">
    拥有权限
</shiro:hasPermission>
<br>
<!--判断当然subject没有权限将显示下面内容-->
<shiro:lacksPermission name="admin:create">
    用户您没有create权限
</shiro:lacksPermission>
<br>

<shiro:user>
    感谢用户:<shiro:principal />登陆
    <a href="/shiro/loginout">退出登陆</a>
</shiro:user>

</body>
</html>

八:权限注解

描述:我们都知道SpringMVC中有扫描@Controller注解,在Sping中有扫描@Service,@Entity等注解。那么对于Shiro来说,也可以有注解的形式的。

注解包括有:

1:@RequiresRoles({"XXX","YYY"})
表示:需要XXX角色和YYY角色
2:@RequiresPermissions("XXX","YYY")
表示:需要权限XXX和YYY
3:@RequiresGuest
表示:需要当前用户是以游客身份,即没有进行验证的用户
4:@RequiresAuthentication
表示:当前需要是已经经过验证,即通过了subject.isAuthenticated,返回true
5:@RequiresUser
表示:当前是已经通过身份验证或者通过已记住的用户

使用步骤:

controller层:

 /**
     * 测试shiro的注解使用
     */
    @RequestMapping(value = "/annotationshiro")
    public String shiroAnnotationTest(){
        shiroService.shiroTest();
        System.out.println("权限通过~!~!");
        return "hello" ;
    }
service层:
package com.hnu.scw.service.imp;

import com.hnu.scw.service.ShiroService;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.stereotype.Service;
import java.util.Date;

/**
 * @author scw
 * @create 2018-01-10 12:15
 * @desc
 **/
@Service
public class ShiroServiceImpl implements ShiroService{
    @RequiresRoles({"admin"})
    @Override
    public void shiroTest(){
        System.out.println(new Date() + "");
    }
}
OK,通过上面的形式的话,访问controller层的URL,就需要该用户有admin的角色,如果没有就会抛出异常;如果有admin角色,那么就能够正常的访问,从而实现了一种角色控制。

知识点:在上面,我们是将注解放到service中的方法的,而我们在开发中,很多情况都是将shiro注解放在controller的需要,如果有朋友试试上面的配置的话,就会发现,怎么shiro注解没有效果呢?但是放在service上面又有效果呢?

其实,原因很简单:我们都知道,在配置SpringMVC+Sping整合的时候,我们在web.xml中,有进行初始化他们加载的xml文件,因为它们两者文件从属不同的contextConfigLocation,(一个是springMVC的param,一个是spring的ContextLoaderListener监听),所以,如果按照上面的配置的话,我们就只能在service层中进行shiro注解了,而controller层是无法解析到处理的,那么怎么处理呢?好了,继续看下面:

方法一:

步骤:(1)首先将在Spring容器中的application.xml中,把关于shiro配置的内容放入到一个新的xml文件,比如spring-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd">
    <!--
           配置shiro的内容
       一:配置SecurityManager
       二:配置echche
       三:配置realm
       四:配置bean生命周期管理
       五:启用IOC容器的shiro注解
       六:配置ShiroFilter,其中的Id必须和web,xml中的shirofileter名字一样
       -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator"></property>
        <property name="realms">
            <list>
                <ref bean="jdbcRealm"></ref>
                <ref bean="jdbcRealm2"></ref>
            </list>
        </property>
    </bean>
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

    <!--配置一个realm管理-->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <!--设置多个realm的验证策略,ModularRealmAuthenticator默认是使用的AtLeastOneSuccessFulStractegy,
        即只需要验证通过某一个realm即可就算验证通过-->
        <!--下面是设置为必须通过所有的realm验证,才算真正的验证-->
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy" />
        </property>
    </bean>
    <!--配置一个realm实现类,该类实现realm接口,使用的是MD5加密-->
    <bean id="jdbcRealm" class="com.hnu.scw.realm.MyShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"></property>
                <!--设置MD5加密的次数,因为有时候一次过于简单,所以就多几次-->
                <property name="hashIterations" value="1024"></property>
            </bean>
        </property>
    </bean>
    <!--再配置一个realm实现类,该类实现realm接口,使用的是SHA1加密-->
    <bean id="jdbcRealm2" class="com.hnu.scw.realm.MyShiroRealm2">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="SHA1"></property>
                <!--设置MD5加密的次数,因为有时候一次过于简单,所以就多几次-->
                <property name="hashIterations" value="1024"></property>
            </bean>
        </property>
    </bean>
    <!--管理spring容器中的IOC的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>
    <!--配置shiro管理页面的控制过滤器 , 其中的Id必须和web,xml中的shiro中的fileter-name名字一样-->
    <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"/>
        <!--
        	配置哪些页面需要受保护.
        	以及访问这些页面需要的权限.
        	1). anon 可以被匿名访问
        	2). authc 必须认证(即登录)后才可能访问的页面.
        	3). logout 登出.
        	4). roles 角色过滤器
        -->
        <property name="filterChainDefinitions">
            <value>
                <!--
                anon表示的是能够进行匿名访问的url,这里就只设置了login.jsp可以为通过url访问
                /** 表示的是通配符的情况,其他的都要进行权限验证处理
                -->
                /login.jsp = anon
                /shiro/login = anon
                /shiro/loginout=logout
                /admin.jsp = roles[admin]
                /user.jsp = roles[customer]
                /** = authc
            </value>
        </property>
    </bean>

</beans>

(2)在application.xml文件中引入shiro的配置文件spring-shiro.xml

 <!--引入shiro配置文件-->
    <import resource="classpath:spring-shiro.xml"/>

(3)在springmvc.xml配置文件引入shiro的配置文件spring-shiro.xml(注意:将这个引入,放到springmvc配置文件的最开始的地方)

 <!--引入shiro配置文件-->
    <import resource="classpath:spring-shiro.xml"/>

方法二:

描述:我们都知道在spring中对于注解标签是通过AOP的代理来实现的,那么我们就可以想到,之所以放在controller层无法有效果,那就是对于springmvc中没有支持spring的AOP处理,所以,我们可以通过下面相对上面更加简单的方法:

步骤:在springmvc.xml文件中添加对于spring的aop的处理:

<!-- 开启aop,对类代理 -->
	<aop:config proxy-target-class="true"></aop:config>
	<!-- 开启shiro注解支持 -->
	<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
		<property name="securityManager" ref="securityManager" />
	</bean>

OK,通过上面这样配置之后,再将service的shiro注解放入到controller层的方法之后,就能够实现期望的效果啦。(注意:shiro的注解只能放在方法上,而不能直接放在controller类上面)

知识点:为什么通过shiro的jsp标签和shiro注解都可以实现权限控制的处理呢?具体的原理又是什么呢?其实对于这个的话,原理很简单,(1)对于Shiro的JSP标签,我们都知道,对于jsp的标签,其实具体的也是转化为servlet去处理,所以,当解析到shiro标签的时候,就会去调用我们编写的或者默认的realm类中的授权方法,去判断是否具有标签对应的权限。这个,我们可以通过在realm类中授权方法打断点的形式,可以看到,当进入有shiro标签的jsp的时候,就会去到该授权方法中。(2)对于shiro注解的原理,其实关键用到了spring的aop原理,当解析到方法名前有shiro注解的时候,就会通过aop去产生一个代理对象,然后又一样的形式去到realm类中去查询具体的授权方法,从而判断是否能够访问该方法。不管,是对于(1)和(2)的形式,我们都发现一个问题,就是会多次去访问授权方法,那不是多么的麻烦,每次验证都要去授权方法判断,所以,为了解决这个问题,就有了shiro的缓存原理,具体可以看下面的内容。

知识点:关于IOC注入的问题:如果在service层中,没有通过实现接口,那么在controller层的时候,需要使用@Qualifier("类名")来进行注入,如果采取@Resource或者@Autowired的形式,那么就会抛出代理异常;如果采取的是类实现接口,那么就用@Autowired注入。这是因为,sping中的jdk动态代理是必须通过接口,而如果要使用类注入的话,那么就要通过cglib的动态代理注入。

九:通过编写类,来对shiro的权限页面进行配置

步骤:(1)编写页面控制类--------详细的解析都在注释里面了

package com.hnu.scw.shirofactory;
import java.util.LinkedHashMap;
/**
 * @author scw
 * @create 2018-01-10 16:53
 * @desc 用于配置shiro中关于资源和权限的页面内容,
 * 以便不需要在shiro配置文件中对于什么页面需要什么权限都写到配置文件中,
 * 而只需要在这里进行配置即可
 **/
public class FilterChainDefinitionBuilder {
    /**
     * function:实现类似shiro配置文件中的下面的这个功能
     * 注意点:必须要返回一个linkhashmap,其实这个通过看filterChainDefinitionMap的源码就可以知道的
     * 它里面就是通过这个来实现的,所以再添加页面权限的时候,一定要注意顺序
     * @return  返回shiro页面控制内容
     */
    /**
    <property name="filterChainDefinitions">
        <value>
         /login.jsp = anon
         /shiro/login = anon
         /shiro/loginout=logout
         /admin.jsp = roles[admin]
         /user.jsp = roles[customer]
         /** = authc
         </value>
         </property>
     */
    public LinkedHashMap<String ,String> builderShiroPageContent(){
        LinkedHashMap<String , String> shiroPage = new LinkedHashMap<>();
        //通过下面这些,其实就是实现了配置文件上面那些内容
        shiroPage.put("/login.jsp" , "anon");
        shiroPage.put("/shiro/login" , "anon");
        shiroPage.put("/shiro/loginout" , "logout");
        shiroPage.put("/admin.jsp" , "roles[admin]");
        shiroPage.put("/user.jsp" , "roles[customer]");
        shiroPage.put("/**" , "authc");
        return shiroPage;
    }
}
(2)修改配置shiro.xml文件

<!--配置shiro管理页面的控制过滤器 , 其中的Id必须和web,xml中的shiro中的fileter-name名字一样-->
    <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"/>
        <!--设置权限页面的内容,通过编写的filterChainDefinitionMap的bean-->
        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap" />
        <!--
        	配置哪些页面需要受保护.
        	以及访问这些页面需要的权限.
        	1). anon 可以被匿名访问
        	2). authc 必须认证(即登录)后才可能访问的页面.
        	3). logout 登出.
        	4). roles 角色过滤器
        -->
        <!--<property name="filterChainDefinitions">
            <value>
                <!–
                anon表示的是能够进行匿名访问的url,这里就只设置了login.jsp可以为通过url访问
                /** 表示的是通配符的情况,其他的都要进行权限验证处理
                –>
                /login.jsp = anon
                /shiro/login = anon
                /shiro/loginout=logout
                /admin.jsp = roles[admin]
                /user.jsp = roles[customer]
                /** = authc
            </value>
        </property>-->
    </bean>
    <!---配置两个bean ,通过bean来进行shiro权限页面的控制,从而减少在配置文件中对页面进行控制
    这里是通过实例工厂的方法来进行的,当然还可以通过静态方法进行。
    -->
    <bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionBuilder" factory-method="builderShiroPageContent"/>
    <bean id="filterChainDefinitionBuilder" class="com.hnu.scw.shirofactory.FilterChainDefinitionBuilder" />
在上面的配置文件中,我将原来的配置方式和现在的配置方式都进行了保留,大家可以对比一下,哪种方法比较适合自己的需求。

十:缓存

知识点:在我们之前关于shiro的配置中,有配置一个cacheManager的bean,其实这就是一个对于shiro的缓存配置的内容。这到底有什么用呢?下面就简单的介绍一下:(关于如何配置的话,我就不多说,因为前面已经配置了的)

首先,我们看一下:

我们可以看到,上面就是配置了使用的cache缓存的路径和使用的形式。

另外,我们再看看ehcache.xml到底有些什么(在前面的知识中,进行了配置,可以翻上去看):

我这里主要讲解一下,这两个地方,我们从名字可以看出来,这里其实就是配置了shiro的验证缓存机制和授权机制(关于这两个知识点,上面介绍了哦!),那这两个到底有啥作用呢?我们来测试测试:

测试:这里我根据我之前上面的配置内容,进行测试,

这里就在上实现AuthorizingRealm的接口的授权方法中添加打印这么一句话:

OK,当我们运行项目之后,我们发现:

授权的方法打印了一次。那么我们将shiro配置文件中的cacheManage去掉,会发现什么呢?

打印了很多这条信息,所以,我们可以看出,配置的缓存是不是很有用呢?其实,当我们在项目中的时候,一般会考虑通过redis进行缓存shiro的信息。这个我在后面也会讲解的哦。

知识点:(1)如果用户正常退出,缓存自动清空

(2)如果yoghurt非正常退出,缓存自动清空

(3)如果修改了用户的权限,而用户不退出系统,修改的权限无法立即生效,需要手动进行编程实现

步骤:在权限修改后调用relm的clearCache方法清除缓存,这个一般都是放在对于权限修改的service方法中进行调用的,即当权限修改后,就调用realm中的该方法。

(1)在realm中,添加该清除缓存的方法,如下:

//清除缓存
	public void clearCached() {
		PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
		super.clearCache(principals);
	}

(2)在项目中的修改角色权限的service层中,调用该方法。比如下面的代码:

@Service
public class SystemPermissionServiceImpl {
	//注入realm
	@Autowired
	private CustomRealm customRealm;  //注入自定义的realm方法
		
	public voidclearShiroCache(){
		//.......进行相应的权限修改的mapper操作即可

		//清除缓存,将来正常开发要在service调用customRealm.clearCached()
		customRealm.clearCached();
	}
}

十一:session管理

描述:主要就是shiro对于session的一些操作管理,相比平常使用的HttpSession更加方便,因为可以在service层中获取到session的内容。
使用配置:在shiro.xml文件中,添加如下内容:
<!-- 会话管理器 -->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- session的失效时长,单位毫秒 -->
        <property name="globalSessionTimeout" value="600000"/>
        <!-- 删除失效的session -->
        <property name="deleteInvalidSessions" value="true"/>
    </bean>
然后在添加到securityManager的bean中,如下:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<!-- 注入session管理器 -->
		<property name="sessionManager" ref="sessionManager" />	
	</bean>

十二:RememberMe(记住我)

描述:我们在浏览很多网页的时候,都会发现,有时候会出现一个提示框,提示我们是否需要记住当前的用户信息,如果我们操作的话,那么我们以后重新浏览该网页的时候,就会自动进行登陆,而不需要进行身份验证了。那么对于shiro中,是怎么的一回事呢?

首先,说一下关于“认证”和“记住我”的区别:

“认证”是表示进行了subject.isAuthenticated(),是用户进行了身份验证登陆的,即subject.login();

“记住我”是表示进行了subject.isRemembered(),是用户是通过记住我登陆的,此时可能并不是真正的用户(可能是你朋友在操作你的电脑)进行访问。

对于这两点来说的话,他们之间只能二者选其一,不能同时出现。所以,对于访问一般的网页来说,我们通过@RequireUser这个拦截即可,即只需要用户进行了登陆;而对于特殊的网页,比如支付页面,订单页面,这些一般都是必须每次都要进行身份验证的,这样是为了安全起见(比如淘宝网就是这样的呢)。

其次:之所以能够进行“记住我”的这个操作,其实就是在本地写入了一个cookie值,所以,如果不想进行记住,或者说是想让记住的时间有自己设定的时间的话,那么可以进行如下的操作:

可以把这个设置为false,那么就不会 进行“记住”操作了。

另外的话,可以通过修改配置,来对“记住”的时间进行修改。

在securityManage的bean中添加:(这里设置的10秒的生命周期),这种是比较简单的方法

<property name="rememberMeManager.cookie.maxAge" value="10" />

对于上面的配置的话,还可以通过下面的形式,这样对于cookie的管理会更加的方便。

步骤:(1)在shiro.xml文件中添加rememberManager管理器

<!-- rememberMeManager管理器,写cookie,取出cookie生成用户信息 -->
	<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
		<property name="cookie" ref="rememberMeCookie" />
	</bean>
	<!-- 记住我cookie -->
	<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
		<!-- rememberMe是cookie的名字 -->
		<constructor-arg value="rememberMe" />
		<!-- 记住我cookie生效时间30天 -->
		<property name="maxAge" value="2592000" />
	</bean>

(2)在securityManager的bean中添加对于rememberManager的bean管理

<!-- securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<!-- 记住我 -->
		<property name="rememberMeManager" ref="rememberMeManager"/>	
	</bean>

可能,看了的话,还不是很了解这个的功能到底在哪里,那好,我通过一个实例来帮助大家理解,到底remember和认证与授权的关系,到底是个怎么回事。

步骤:

(1)登陆JSP代码:

<%--
  Created by IntelliJ IDEA.
  User: scw
  Date: 2018/1/9 0009
  Time: 13:27
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登陆</title>
</head>
<body>
<h1>欢迎登陆</h1>
<form action="/shiro/login" method="post">
    账号:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    <!--设置是否要记住登陆信息,其实就是保存信息到cookie设置到本地中,
    注意如果要使用shiro中的记住我的这个功能,
    那么这个checkbox的name属性就要为这个名字,
    当然可以通过配置文件进行修改,但是最好使用默认的这个name-->
    <input type="checkbox" name="rememberMe">自动登陆
    <input type="submit"  value="登陆">
</form>
</body>
</html>
(2)登陆的后台controller代码
 @RequestMapping(value = "/login")
    public String login(String username , String password ,String rememberMe) throws Exception{
        //获取subject对象
        Subject currentUser = SecurityUtils.getSubject();
        //判断是否已经有权限
        if(!currentUser.isAuthenticated()){
            //把用户名和密码封装成usernamepasswordToken对象
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            //设置是否记住token,即写入cookie到本地,这个是为了实现”记住我“的那个功能
            //这个功能主要是从登陆的前端的checkbox来判断是否需要添加自动登陆的功能
            if(rememberMe != null){
                //如果不是为null的话,那么就表示用户是需要进行自动登陆的,那么就把cookie写入到本地中
                //注意一点:就是通过这个方法的话,以后只能访问在shiro配置的user过滤器的页面和地址,其他需要权限的还是无法进行的
                token.setRememberMe(true);
            }
            try {
                currentUser.login(token);
            }catch (AuthenticationException e){
                throw new Exception("无法认证");
            }
        }
        return "success";
    }

请注意代码中的这一段:

(3)再shiro.xml文件中,配置一个页面,该页面的访问需要是user的,即要么是认证通过的,要么是通过“记住”功能,即本地有cookie的才能能够进行访问

(4)编写对应的JSP页面(随便就写个页面而已,对应上面配置的list.jsp)

<%--
  Created by IntelliJ IDEA.
  User: scw
  Date: 2018/1/9 0009
  Time: 13:27
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>列表</title>
</head>
<body>
<h1>列表</h1>
</body>
</html>

(5)进行测试演示效果:(注意分析)


情况一:不点击自动登陆

当输入了正确的账号和密码后(我这里是用的admin ,123456 )如果跟着我的步骤来的话,也用这个即可。当输入后,我们就可以看到进入了主页,


好了,重点来了~@@@@@

这时候,我们直接在地址上输入localhost:8080/list.jsp------------------也就是访问我们上面设定需要user的那个链接。

这时候,我们会发现,一切都正常,结果如下:


OK,这个确实没啥毛病,正常的嘛。。。。

好了,那么下面关闭浏览器,重新打开一个,然后直接输入:localhost:8080/list.jsp,我们会发现,自动弹到了登陆页面。

解析原理:这是因为,当关闭浏览器后,shiro会自动把我们之前的缓存全部清除,这就和logout的作用一样,所以,当我们再次访问的时候,因为这个list.jsp地址,是一个要么是认证过的,要么是“记住”的才能进行登陆,所以对于前面的一种方式,能够进行访问,就是因为我们之前是认证通过的,所以访问当然就没问题啦。所以,这就是认证。。。

情况二:点击了自动登陆的按钮

步骤还是和上面一样,输入 账号和密码之后,进入了主页,然后访问localhost:8080/list.jsp还是正常能访问(第一种情况也是可以的,是在同一个浏览器的时候哦~!)。重点,这时候,我们关闭浏览器,然后重新打开一个,这时候再直接在地址栏中输入localhost:8080/list.jsp ,哇塞,,,,这时候直接进去了耶(情况一的时候,是弹到登陆页面)。。。。

解析原理:其实,这就是remember“记住”的功能了!!!!很实用的哦,淘宝就有很多这样的功能~!~!

流程:其实就是因为,我们点击了那个复选框,然后再login的后台controller中的token.setRemember(true);这的作用(我在上面已经重点画了这个代码哦~),其实它就是在本地写入一个cookie,我们都知道cookie的作用的,所以,当我们重新打开浏览器的时候,就去检查cookie,有就使用,然后又因为shiro中配置的这个链接是一个可以通过“记住”来访问的,所以就访问成功了。

PS:我们可以在浏览器中,查看cookie


十三:更改Shiro中提供的过滤器,自定义过滤器操作

描述:(1)我们都知道,对于Shiro的用户名和密码及其“记住我功能”验证的时候,对应的JSP中的name分别是username和password及其rememberMe,如果,不是设置为这样的话,那么验证就会有问题,无法找到,这是为什么呢?我们可以看看对于表单验证的源代码:


那么,如果在JSP,我们想换name属性,如何再通过Shiro进行认证呢?

(2)在项目开发的时候,对于验证登陆的时候,一般都会加入验证码,那么我们可能会考虑,再进行用户名和密码匹配验证的时候,会先判断是否验证码输入正确,如果正确了,我们再进行用户名和密码匹配,那么如何在Shiro验证之前,进行验证码的自定义操作呢?

对于上面的两个问题,我们可以通过自定义表单验证过滤器也就是继承FormAuthenticationFilter这个类,来进行自定义操作。

我还是通过一个实例来进行说明问题:

实例描述:就是进行Shiro权限认证之前,先判断登陆中的验证码是否输入正确,如果正确,再进行用户名认证,否则直接返回错误信息。

步骤:(1)编写自定义的继承FormAuthenticationFilter类的

package com.hnu.scw.shiro;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

/**
 * 
 * <p>Title: CustomFormAuthenticationFilter</p>
 * <p>Description:自定义FormAuthenticationFilter,认证之前实现 验证码校验 </p>
 * @author	scw
 * @date	2018-1-13
 * @version 1.0
 */
public class CustomFormAuthenticationFilter extends FormAuthenticationFilter {
	//原FormAuthenticationFilter的认证方法
	@Override
	protected boolean onAccessDenied(ServletRequest request,
			ServletResponse response) throws Exception {
		//在这里进行验证码的校验	
		//从session获取正确验证码
		HttpServletRequest httpServletRequest = (HttpServletRequest) request;
		HttpSession session =httpServletRequest.getSession();
		//取出session的验证码(正确的验证码)
		String validateCode = (String) session.getAttribute("validateCode");	
		//取出页面的验证码
		//输入的验证和session中的验证进行对比 
		String randomcode = httpServletRequest.getParameter("randomcode");
		if(randomcode!=null && validateCode!=null && !randomcode.equals(validateCode)){
			//如果校验失败,将验证码错误失败信息,通过shiroLoginFailure设置到request中
			httpServletRequest.setAttribute("shiroLoginFailure", "randomCodeError");
			//拒绝访问,不再校验账号和密码 
			return true; 
		}
		return super.onAccessDenied(request, response);
	}	
}
通过这个,就可以解决上面描述中的第二个问题。

(2)在shiro.xml文件中,进行添加自定义的表单过滤器类

<!-- 自定义form认证过虑器 -->
<!-- 基于Form表单的身份验证过滤器,不配置将也会注册此过虑器,表单中的用户账号、密码及loginurl将采用默认值,建议配置 -->
	<bean id="formAuthenticationFilter" 
	class="com.hnu.scw.shiro.CustomFormAuthenticationFilter ">
		<!-- 表单中账号的input名称 -->
		<property name="usernameParam" value="username" />  
		<!-- 表单中密码的input名称 -->
		<property name="passwordParam" value="password" />
		<!-- 记住我input的名称 -->
		<property name="rememberMeParam" value="rememberMe"/>
 </bean>
通过这个,就可以解决描述中的第一个问题。如果,需要匹配JSP自定义的name属性,那么就只需要修改上面代码中的对应的value值即可。
(3)实现案例。。。。。其实,这个验证码验证的问题,我们在实际开发中,一般只需要放在对应的登陆的controller最开始进行判断即可,所以,上面这个实例并不是十分实用,但是主要就是说明一个问题,就是对于shiro中的过滤器,我们可以进行自定义的编写,我们可以进行自己所需要的操作的哦。。。

总结:

     好了,基本的Shiro的知识,我主要通过两篇文章进行了讲解,所以,如果认真的一步步跟着我的知识点来学习的话,我觉得还是没有什么问题的,剩下的就是自己多熟悉熟悉,另外,还有关于Spring-security的一个权限框架,其实,主要是因为它过度依赖于Spring,所以相对Shiro就有一点依赖问题,但是它也是非常有用的一个框架。我们在开发的时候,根据我们自身的需要来进行使用就好了。。。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值