如果没有看第一篇关于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"; }
请注意代码中的这一段:
(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,如果,不是设置为这样的话,那么验证就会有问题,无法找到,这是为什么呢?我们可以看看对于表单验证的源代码:
(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就有一点依赖问题,但是它也是非常有用的一个框架。我们在开发的时候,根据我们自身的需要来进行使用就好了。。。