shiro讲解之 SpringMVC 集成Shiro
本章节将通过实例来学习下SpringMVC+Spring+Shiro如何集成并用一个精简的例子说明。
整合
新建一个完整的Spring+SpringMVC 框架
- 关于Spring整个SpringMVC的例子已在SpingMVC模块有分享,可移步至SpringMVC。
项目目录
整合 Shiro 步骤
下载 Shiro 及相关jar包
pom.xml新增以下依赖
<!-- Shiro --> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-all --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.3.2</version> </dependency> <!-- Shiro end --> <!-- Ehcache --> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-ehcache --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.4</version> </dependency> <!-- Ehcahche end -->
配置web.xml
Shiro web.xml配置信息参考 Shiro Web App。这里我们的整合将直接参考Spring Example中的web.xml的配置信息。
<!-- 配置Shiro Filter --> <!-- Shiro Filter is defined in the spring application context: --> <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>
web.xml文件中配置Shiro 的DelegatingFilterProxy,该Filter的核心作用点就是用户拦截和过滤请求的URL,之于如何处理URL将在Shiro配置文件的 shiroFilter中有具体的说明。
新建ehcache.xml缓存
在整合Shiro的过程中我们在使用shiro的缓存机制时会使用到缓存,这一点在pom.xml的依赖有提现,另外在后续的篇章中我们也将详细说明Shiro的缓存机制,这里做初步了解。
在classpath(一般建议,也可以自定义)下新建一个ehcache.xml,配置信息如下,如有必要可全部复制<?xml version="1.0" encoding="UTF-8"?> <ehcache> <diskStore path="java.io.tmpdir" /> <!-- 缓存存放目录(此目录为放入系统默认缓存目录),也可以是”D:/cache“ java.io.tmpdir --> <!-- 登录记录缓存 锁定10分钟 --> <cache name="passwordRetryCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authorizationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authenticationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" maxElementsOnDisk="10000000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU" /> <!-- name:Cache的唯一标识 maxElementsInMemory:内存中最大缓存对象数 maxElementsOnDisk:磁盘中最大缓存对象数,若是0表示无穷大 eternal:Element是否永久有效,一但设置了,timeout将不起作用 overflowToDisk:配置此属性,当内存中Element数量达到maxElementsInMemory时,Ehcache将会Element写到磁盘中 timeToIdleSeconds:设置Element在失效前的允许闲置时间。仅当element不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大 timeToLiveSeconds:设置Element在失效前允许存活时间。最大时间介于创建时间和失效时间之间。仅当element不是永久有效时使用,默认是0.,也就是element存活时间无穷大 diskPersistent:是否缓存虚拟机重启期数据 diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒 diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区 memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用) --> </ehcache>
在ApplicationContext.xml中配置Shiro相关信息
在ApplicationContext.xml文件中配置Shiro的必要信息,这里我们的整合将直接参考Spring Example中的applicationContext.xml的配置信息。基本配置如下(在讲解后续功能时会不断完善该配置)<!-- 1.配置SecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager" /> <!-- Shiro 多Realm认证策略 --> <!-- <property name="authenticator" ref="authenticator"></property> --> <property name="realms"> <list> <ref bean="shiroRealm"></ref> </list> </property> </bean> <!-- 2.配置 cacheManager --> <!-- 2.1 自定义ehcache 此处需要加入ehcache jar 及其配置文件 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property> </bean> <!-- 3.配置Realm --> <bean id="shiroRealm" class="com.shiro.example.interceptor.realm.ShiroRealm"> <!-- <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="SHA1"></property> <property name="hashIterations" value="1024"></property> </bean> </property> --> </bean> <!-- <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <property name="authenticationStrategy"> 多个Reaml认证中有一个认证成功即成功策略 <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean> 第一个Reaml认证策略 <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy"></bean> 必须所有Reaml都成功认证策略 <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean> </property> </bean> --> <!-- 4.配置lifecycleBeanPostProcessor,可以自动的调用Spring IOC 容器中shiro bean的生命周期方法 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <!-- 5.启用ICO 容器中使用Shiro 注解,但必须在配置了lifecycleBeanPostProcessor之后方可使用 --> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor" /> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager" /> </bean> <!-- 6.配置ShiroFilter 6.1 id必须和web.xml中的DelegatingFilterProxy的 FilterName一致 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/login/toLogin" /> <property name="successUrl" value="/example/index" /> <property name="unauthorizedUrl" value="/example/unauthorized" /> <property name="filterChainDefinitions"> <value> /login/toLogin = anon /login/loginVal = anon /login/logout = logout /** = authc </value> </property> </bean>
一个Shiro例子
以上配置好以后我们将做一个Shiro 认证的登录例子。
新建Form表单,本例子中的login.jsp
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path; %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Insert title here</title> </head> <body> <form action="<%=basePath %>/login/loginVal" method="POST"> username: <input type="text" name="username" /> <br> <br> password: <input type="password" name="password" /> <br> <br> <input type="submit" value="Submit" /> </form> </body> </html>
实现Controller方法
package com.shiro.example.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; import org.springframework.web.bind.annotation.RequestParam; @Controller @RequestMapping(value = "/login") public class LoginProcess { @RequestMapping("/loginVal") public String login(@RequestParam("username") String username, @RequestParam("password") String password) throws Exception { System.out.println("username: " + username + "+ password: " + password); Subject subject = SecurityUtils.getSubject(); // 如果当前用户并未经过认证登录的 /*if (!subject.isAuthenticated()) {*/ UsernamePasswordToken token = new UsernamePasswordToken(username, password); token.setRememberMe(true); try { subject.login(token); } catch (Exception e) { System.out.println("登录失败: " + e.getMessage()); return "erro"; } /*} System.out.println("用户尚未被认证");*/ return "index"; } @RequestMapping("/toLogin") public String toLogin() throws Exception { return "login"; } }
实现自定义Realm方法
package com.shiro.example.interceptor.realm; import java.util.HashSet; import java.util.Set; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; 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 com.shiro.example.entity.SubjectEntity; public class ShiroRealm extends AuthorizingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("[FirstRealm] doGetAuthenticationInfo"); // 1. 把 AuthenticationToken 转换为 UsernamePasswordToken UsernamePasswordToken upToken = (UsernamePasswordToken) token; // 2. 从 UsernamePasswordToken 中来获取 username String username = upToken.getUsername(); // 3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录 SubjectEntity principals = new SubjectEntity("123456", "Dustyone"); // 4. 若用户不存在, 则可以抛出 UnknownAccountException 异常 if ("unknown".equals(username)) { throw new UnknownAccountException("用户不存在!"); } // 5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常. if ("monster".equals(username)) { throw new LockedAccountException("用户被锁定"); } // 6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: // SimpleAuthenticationInfo // 以下信息是从数据库中获取的. // 1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象. Object principal = principals.getUsername(); // 2). credentials: 密码. Object credentials = principals.getPassword(); // "fc1709d0a95a6be30bc5926fdb7f22f4"; // 3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 String realmName = getName(); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName); /* * //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 String * realmName = getName(); //4). 盐值. ByteSource credentialsSalt = * ByteSource.Util.bytes(username); * * SimpleAuthenticationInfo info = null; //new * SimpleAuthenticationInfo(principal, credentials, realmName); info = * new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, * realmName); */ return info; }
新建Subject Entity
package com.shiro.example.entity; import java.io.Serializable; public class SubjectEntity implements Serializable{ /** * Subject Entity */ private static final long serialVersionUID = 1L; public SubjectEntity() { super(); } @Override public String toString() { return "SubjectEntity [username=" + username + ", password=" + password + "]"; } public SubjectEntity(String password, String username) { super(); this.username = username; this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } private String username; private String password; }
Example 文件
``` package com.shiro.example.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/example") public class Example { @RequestMapping(value = "/index") public String example() throws Exception { return "example"; } @RequestMapping(value="/unauthorized") public String unauthorized(){ return "unauthorized"; } } ```
本例子涉及的核心功能点
Shiro 认证(登录验证)
- 由我们自定义的Realm代码中可知我们指定登录的Subject的usernam=Dustyone password=123456。唯有此principal(实体)组合才能通过Shiro认证(暂时不涉及加密)。
正确登录
错误principal登录
Shiro 授权
在shiroFilter的 filterChainDefinitions我们做了访问权限声明即除了 /login/toLogin 和 /login/loginVal 可以被匿名访问意外其他的任何url请都将被 shiro filetr收集并进行Subject 授权(即用户是否有权限访问**, 此处的两个星号标识资源。)。以 /example/index请求为例,在用户未登录的情况下访问该路径将直接跳转至登录页面。只有在登录成功的情况下才能访问 /example/index
未登录的情况下访问 /example/index 将直接跳至登录页面
已登录的情况下访问 /example/index 直接可以访问该资源
Shiro 缓存
用户登录成功之后(特质在启用了shiro的缓存机制之后),shiro 缓存将对最近一个Subject的认证或授权信息写入缓存。当该Subject 再次访问需要认证或授权的资源时,shiro将从缓存中(本例子中特指ehcache.xml)读取Subject的相关信息而不再此对该Subject进行认证或授权处理(这里的认证或授权特指我们自定义的Reaml中的 doGetAuthenticationInfo 和 doGetAuthorizationInfo 两个方法)。Shiro 登出
在访问logout 之后shiro将清除所有缓存信息并跳至登录页面。