如果没有看第一篇关于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为post,get,delete等。
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没有参数表示必须存在用户, 身份认证通过或通过记住我认证通过的可以访问,当登入操作时不做检查
注:
anon,authcBasic,auchc,user是认证过滤器,
perms,roles,ssl,rest,port是授权过滤器
这是搭建项目的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管理
<!-- 会话管理器 -->
<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";
}
请注意代码中的这一段:
(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,如果,不是设置为这样的话,那么验证就会有问题,无法找到,这是为什么呢?我们可以看看对于表单验证的源代码:
(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就有一点依赖问题,但是它也是非常有用的一个框架。我们在开发的时候,根据我们自身的需要来进行使用就好了。。。