准备
简介
- Apache Shiro 是 Java 的一个安全(权限)框架。
- Shiro 不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
- Shiro 可以完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。
Shiro 官网:https://shiro.apache.org | 官网下载 shiro-1.3.2 源码包 | 百度云下载 shiro-1.3.2 源码包(包含jar包)(提取码:f7zm)
功能
Apache Shiro是一个具有许多功能的综合应用程序安全框架,基本的功能如下:
- Authentication:身份验证。
- Authorization:授权,即访问控制的过程,指定“谁”可以访问“什么资源”。
- Session Management:会话管理,即使在非 Web 或 EJB 应用程序中,也可以管理特定于用户的会话
- Cryptography:加密,使用加密算法保持数据安全,同时仍然易于使用。
快速开始
下面代码是 shiro 提供的快速开始示例,这里对其注释及提示做了中文替换易于理解,路径为 shiro-root-\samples\quickstart 。
1、使用 maven 创建 java 工程,导入如下依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.zze.shiro</groupId> <artifactId>shiro_test</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.6.2</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.6.2</version> </dependency> </dependencies> </project>
pom.xml
2、引入配置文件:
[users] root = secret, admin guest = guest, guest presidentskroob = 12345, president darkhelmet = ludicrousspeed, darklord, schwartz lonestarr = vespa, goodguy, schwartz [roles] admin = * schwartz = lightsaber:* goodguy = winnebago:drive:eagle5
shiro.ini
log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n log4j.logger.org.apache=WARN log4j.logger.org.springframework=WARN log4j.logger.org.apache.shiro=INFO log4j.logger.org.apache.shiro.util.ThreadContext=WARN log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
log4j.properties
3、示例代码:
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Quickstart { private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { // 加载 shiro.ini 创建 SecurityManagerFactory Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); // 获取 SecurityManager 实例 SecurityManager securityManager = factory.getInstance(); // 通过 SecurityUtils 操作 SecurityManager 实例 SecurityUtils.setSecurityManager(securityManager); // 获得 Subject,相当于获取到当前用户 Subject currentUser = SecurityUtils.getSubject(); // 获得 Session Session session = currentUser.getSession(); // Session 中存值 session.setAttribute("someKey", "aValue"); // Session 中取值 String value = (String) session.getAttribute("someKey"); // 判断是否取到值 if (value.equals("aValue")) { log.info("获取到正确的值:[" + value + "]"); } // 通过 Subject.isAuthenticated() 判断当前用户是否以认证 if (!currentUser.isAuthenticated()) { // 如果未认证 // 创建用户名密码Token,与 shiro.ini 中 users 下配置比对 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); // 记住我 token.setRememberMe(true); try { // 执行登陆操作 currentUser.login(token); } catch (UnknownAccountException uae) {// 用户名不存在 log.info("没有这个用户名的用户: " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { // 密码错误 log.info(token.getPrincipal() + "的密码错误"); } catch (LockedAccountException lae) { // 账户已锁定 log.info("用户名为" + token.getPrincipal() + "账户已锁定"); } catch (AuthenticationException ae) { // 上面三个异常的父类 } } // 如果已认证,currentUser.getPrincipal() 不为空 if (currentUser.getPrincipal() != null) { log.info("用户 [" + currentUser.getPrincipal() + "] 登录成功"); // 通过 Subject.hasRole() 方法判断当前用户是否拥有某个角色 if (currentUser.hasRole("schwartz")) { log.info("你有[schwartz]这个角色"); } else { log.info("你没有[schwartz]这个角色"); } // 判断当前用户是否拥有某个权限 // schwartz 角色对应的权限为 lightsaber:*,即可以对 lightsaber 做任何操作 if (currentUser.isPermitted("lightsaber:weild")) { log.info("你可以对[lightsaber]使用[weild]"); } else { log.info("对不起,[lightsaber]权限只有[schwartz]拥有"); } // 更细粒度的判断当前用户是否拥有某个权限 if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("你可以[drive]标识为[eagle5]的[winnebago]"); } else { log.info("对不起,你不允许[drive]标识未[eagle5]的[winnebago]"); } } // 登出 currentUser.logout(); System.exit(0); } }
Quickstart
拦截器
我们在上面已经使用了 Shiro 默认提供的拦截器 anon 和 authc,它提供的所有拦截器可以在下面这个枚举类中看到:
package org.apache.shiro.web.filter.mgt; import org.apache.shiro.util.ClassUtils; import org.apache.shiro.web.filter.authc.*; import org.apache.shiro.web.filter.authz.*; import org.apache.shiro.web.filter.session.NoSessionCreationFilter; import javax.servlet.Filter; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import java.util.LinkedHashMap; import java.util.Map; /** * Enum representing all of the default Shiro Filter instances available to web applications. Each filter instance is * typically accessible in configuration the {@link #name() name} of the enum constant. * * @since 1.0 */ public enum DefaultFilter { anon(AnonymousFilter.class), authc(FormAuthenticationFilter.class), authcBasic(BasicHttpAuthenticationFilter.class), logout(LogoutFilter.class), noSessionCreation(NoSessionCreationFilter.class), perms(PermissionsAuthorizationFilter.class), port(PortFilter.class), rest(HttpMethodPermissionFilter.class), roles(RolesAuthorizationFilter.class), ssl(SslFilter.class), user(UserFilter.class); private final Class<? extends Filter> filterClass; private DefaultFilter(Class<? extends Filter> filterClass) { this.filterClass = filterClass; } public Filter newInstance() { return (Filter) ClassUtils.newInstance(this.filterClass); } public Class<? extends Filter> getFilterClass() { return this.filterClass; } public static Map<String, Filter> createInstanceMap(FilterConfig config) { Map<String, Filter> filters = new LinkedHashMap<String, Filter>(values().length); for (DefaultFilter defaultFilter : values()) { Filter filter = defaultFilter.newInstance(); if (config != null) { try { filter.init(config); } catch (ServletException e) { String msg = "Unable to correctly init default filter instance of type " + filter.getClass().getName(); throw new IllegalStateException(msg, e); } } filters.put(defaultFilter.name(), filter); } return filters; } }
org.apache.shiro.web.filter.mgt.DefaultFilter
默认拦截器名 | 拦截器类 | 说明(括号中表示默认值) |
---|---|---|
身份验证相关 | ||
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
基于表单的拦截器:如 "/**=authc",如果没有登录会跳转到相应的登录页。 主要属性: usernameParam:表单提交的用户名参数名(username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的记住我参数名(rememberMe); loginUrl:登录页面地址(/login.jsp); successUrl:登录成功后默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure) |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
Basic HTTP 身份验证拦截器。 主要属性: applicationName:弹出登录框显示的信息(application)。 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter |
注销/注销拦截器。 主要属性: redirectUrl:退出成功后重定向地址(/)。例:"/logout=logout"。 |
user | org.apache.shiro.web.filter.authc.UserFilter | 用户拦截器,用户已身份验证/记住我登录都可。例:"/**=user"。 |
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 匿名拦截器,即不需要登录即可访问,一般用于静态资源过滤。例:"/static/**=anon"。 |
授权相关 | ||
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
角色授权拦截器,验证用户是否拥有某角色。例:"/admin=rols[admin]" 主要属性: loginUrl:登录页面地址(/login.jsp); unauthorizedUrl:验证未授权后重定向的地址; |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
权限授权拦截器,验证用户是否拥有某权限,属性与 roles 拦截器相同。例:"/user/**=perms['user:create']" |
port | org.apache.shiro.web.filter.authz.PortFilter |
端口拦截器。 主要属性: port(80):可以通过的端口。 例:"/test=port[80]",如果访问的该页面是通过非 80 端口,将自动将请求端口改为 80,并重定向到该 80 端口,其它参数(路径等)都不变。 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | rest风格拦截器,自动根据请求方法构建权限字符串(GET=read,POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串。例:"/users=rest[user]",会自动拼出 "user:read,user:create,user:update,user:delete" 权限字符串进行权限匹配。 |
ssl | org.apache.shiro.web.filter.authz.SslFilter | SSL 拦截器,只有请求协议是 https 才能通过,否则自动跳转 https 端口(443);其它和 port 拦截器一样; |
其它 | ||
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常; |
集成Spring
简单示例
1、使用 maven 创建一个 web 工程,导入如下依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.zze.shiro</groupId> <artifactId>shiro_spring</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <dependencies> <!--Spring--> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>aopalliance</groupId> <artifactId>aopalliance</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-core-asl</artifactId> <version>1.9.11</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.2.4.RELEASE</version> </dependency> <!--servlet--> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <!--Shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.6.2</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.6.2</version> </dependency> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> <version>2.4.3</version> <type>pom</type> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8080</port> <path>/</path> </configuration> </plugin> </plugins> </build> </project>
pom.xml
2、配置 Spring 核心监听器、SpringMVC 核心 Servlet、Shiro 过滤器:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!--配置 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> <!--配置 Spring 核心监听器--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!--配置 SpringMVC 核心 Servlet--> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
WEB-INF/web.xml
3、定义一个 Realm,需要实现 org.apache.shiro.realm.Realm :
package com.zze.shiro.realms; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.realm.Realm; public class MyRealm implements Realm { public String getName() { return null; } public boolean supports(AuthenticationToken authenticationToken) { return false; } public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { return null; } }
com.zze.shiro.realms.MyRealm
4、引入 ECache 配置文件:
<ehcache> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> <cache name="sampleCache1" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true" /> <cache name="sampleCache2" maxElementsInMemory="1000" eternal="true" timeToIdleSeconds="0" timeToLiveSeconds="0" overflowToDisk="false" /> </ehcache>
ehcache.xml
5、引入 log4j 属性文件:
log4j.rootLogger = debug,stdout ### 输出信息到控制抬 ### log4j.appender.stdout = org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target = System.out log4j.appender.stdout.layout = org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n ### 输出DEBUG 级别以上的日志到=E://logs/error.log ### log4j.appender.D = org.apache.log4j.DailyRollingFileAppender log4j.appender.D.File = E://logs/log.log log4j.appender.D.Append = true log4j.appender.D.Threshold = DEBUG log4j.appender.D.layout = org.apache.log4j.PatternLayout log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n ### 输出ERROR 级别以上的日志到=E://logs/error.log ### log4j.appender.E = org.apache.log4j.DailyRollingFileAppender log4j.appender.E.File =E://logs/error.log log4j.appender.E.Append = true log4j.appender.E.Threshold = ERROR log4j.appender.E.layout = org.apache.log4j.PatternLayout log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
log4j.properties
5、引入 Spring 配置文件,并在 Spring 配置文件中配置 Shiro:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 1、配置安全管理器 SecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager"/> <property name="realm" ref="jdbcRealm"/> </bean> <!-- 2、配置缓存管理器 CacheManager a、需加入 ehcache jar 和 配置文件 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> </bean> <!-- 3、配置实现了 org.apache.shiro.realm.Realm 接口的 Realm --> <bean id="jdbcRealm" class="com.zze.shiro.realms.MyRealm"/> <!-- 4、配置 lifecycleBeanPostProcessor,可以自动调用配置在 Spring IOC 容器中 Shiro bean 的生命周期方法 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- 5、启用 IOC 容器中使用 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 a、id 必须和 web.xml 中配置的 DelegatingFilterProxy 的 filter-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"/> <!-- 配置哪些页面需要受保护 以及 访问这些页面需要的权限 a、anon 可以匿名访问 b、authc 必须认证(登陆)后才可以访问 --> <property name="filterChainDefinitions"> <value> /login.jsp = anon /** = authc </value> </property> </bean> </beans>
applicationContext.xml
6、引入 SpringMVC 核心配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.zze.shiro"/> <mvc:annotation-driven/> <mvc:default-servlet-handler/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
springmvc.xml
7、创建测试 jsp 页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h3>Login Page</h3> </body> </html>
login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h3>List Page</h3> </body> </html>
list.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h3>Unauthorized Page</h3> </body> </html>
unauthorized.jsp
8、测试,启动项目,效果是只有 login.jsp 可以匿名访问,访问其它页面都会自动重定向到 login.jsp 。
注意:在 Spring 中配置 org.apache.shiro.spring.web.ShiroFilterFactoryBean 的时候,id 必须和 WEB-INF/web.xml 中的过滤器 org.springframework.web.filter.DelegatingFilterProxy 的 filter-name 一致,否则会在程序启动时抛出异常。
为什么它们的名称必须一致呢?可以从源码中看到,查看 Shiro 的入口过滤器源码:
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { Filter delegateToUse = this.delegate; if (delegateToUse == null) { synchronized (this.delegateMonitor) { if (this.delegate == null) { WebApplicationContext wac = findWebApplicationContext(); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: " + "no ContextLoaderListener or DispatcherServlet registered?"); } this.delegate = initDelegate(wac); } delegateToUse = this.delegate; } } invokeDelegate(delegateToUse, request, response, filterChain); }
org.springframework.web.filter.DelegatingFilterProxy#doFilter
查看第 14 行 initDelegate 方法:
protected Filter initDelegate(WebApplicationContext wac) throws ServletException { Filter delegate = wac.getBean(getTargetBeanName(), Filter.class); if (isTargetFilterLifecycle()) { delegate.init(getFilterConfig()); } return delegate; }
org.springframework.web.filter.DelegatingFilterProxy#initDelegate
在第 2 行可以看到,这里从 Spring 的 IOC 容器中获取一个名称为 getTargetBeanName() 值的实例,查看该方法:
/** * Set the name of the target bean in the Spring application context. * The target bean must implement the standard Servlet Filter interface. * <p>By default, the {@code filter-name} as specified for the * DelegatingFilterProxy in {@code web.xml} will be used. */ public void setTargetBeanName(String targetBeanName) { this.targetBeanName = targetBeanName; } /** * Return the name of the target bean in the Spring application context. */ protected String getTargetBeanName() { return this.targetBeanName; }
org.springframework.web.filter.DelegatingFilterProxy#getTargetBeanName
从第 4 行的注释中可以看到, targetBeanName 设置的值默认为 WEB-INF/web.xml 中 DelegatingFilterProxy 的 filter-name 值。
即:在 initDelegate 方法中从 Spring IoC 容器中获取的 Bean 的名称就是这个 filter-name 值,所以我们也可以通过设置 targetBeanName 的值来自定义 Spring IoC 容器中配置的 ShiroFilterFactoryBean 的名称,如下:
<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> <init-param> <param-name>targetBeanName</param-name> <param-value>shiroFilter222</param-value> </init-param> </filter>
WEB-INF/web.xml
过滤配置
这里的过滤配置即在 Spring 配置中 org.apache.shiro.spring.web.ShiroFilterFactoryBean 下 filterChainDefinitions 属性的配置。具有如下规则:
匹配格式
- 格式: url=拦截器名称[参数] 。
- 如果当前请求路径匹配了某个 url,那么将会执行其对应的拦截器。
- anon (anonymous):该拦截器表示可匿名访问,即不需要登录即可访问。
- authc (authentication):该拦截器表示需要身份认证通过后才能访问。
匹配模式
这里的 url 使用的是 Ant 风格路径。
Ant 路径通配符支持 '?'、'*'、'**',注意通配符匹配不包括目录分隔符 '/':
- ?:匹配一个字符,如 /admin? 将匹配 /admin1 ,但不匹配 /admin 或 /admin/ 。
- *:匹配零个或多个字符串,如 /admin* 将匹配 /admin 、 /admin123 ,但不匹配 /admin/1 。
- **:匹配路径中零个或多个路径,如 /admin/** 将匹配 /admin/a 或 /admin/a/b 。
匹配顺序
url 采取第一次匹配优先的方式,即从头开始使用第一个匹配的 url 模式对应的拦截器链。
如:
<property name="filterChainDefinitions"> <value> /bb/**=filter1 /bb/aa=filter2 /**=filter3 </value> </property> <!--如果请求的 url 是 "/bb/aa",因为按照声明顺序进行匹配,那么将使用 filter1 进行拦截。-->
上述这种配置方式其实有一个很明显的弊端,当我们有很多过滤规则都放在配置文件中显然不合适,常常我们所需要的方式是能将权限数据保存在数据库,这样我们就可以通过操作数据库的权限数据动态的更改权限配置。那这种方式我们该如何实现呢?
因为上述配置方式实际上是将一个字符串通过 org.apache.shiro.spring.web.ShiroFilterFactoryBean#setFilterChainDefinitions 方法注入,查看这个方法:
public void setFilterChainDefinitions(String definitions) { Ini ini = new Ini(); ini.load(definitions); Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS); if (CollectionUtils.isEmpty(section)) { section = ini.getSection(Ini.DEFAULT_SECTION_NAME); } setFilterChainDefinitionMap(section); }
org.apache.shiro.spring.web.ShiroFilterFactoryBean#setFilterChainDefinitions
可以看到,2-7 行将我们传入的过滤配置字符串进行了解析并且包装为 org.apache.shiro.config.Ini.Section 对象(继承了 java.util.Map<String,Stirng> )最终调用了 setFilterChainDefinitionMap(section) :
public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) { this.filterChainDefinitionMap = filterChainDefinitionMap; }
org.apache.shiro.spring.web.ShiroFilterFactoryBean#setFilterChainDefinitionMap
很明显,上述就是将我们配置的过滤字符串转换为一个 Map 对象最终赋值给 org.apache.shiro.spring.web.ShiroFilterFactoryBean#filterChainDefinitionMap 属性,所以我们只要动态的构建一个 Map 对象赋值给这个属性就实现了权限的动态过滤配置。这里要注意的我们的过滤规则是有顺序的,所以我们可以构建一个 java.util.LinkedHashMap 对象,实现如下:
1、构建 Map 工厂类:
package com.zze.shiro.utils; import java.util.LinkedHashMap; public class FilterChainDefinitionMapBuilder { public LinkedHashMap<String, String> buildFilterChainDefinitionMap() { LinkedHashMap<String, String> map = new LinkedHashMap<String, String>(); // 数据可以从数据库中获取 map.put("/bb/**", "filter1"); map.put("/bb/aa", "filter2"); map.put("/**", "filter3"); return map; } }
com.zze.shiro.utils.FilterChainDefinitionMapBuilder
2、通过 Spring 的工厂方式注入 filterChainDefinitionMap 属性:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/list.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/> </bean> <bean id="filterChainDefinitionMapBuilder" class="com.zze.shiro.handlers.FilterChainDefinitionMapBuilder"/> <bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"/>
登录功能的实现
思路
- 获取当前用户即 Subject,调用 SecurityUtils.getSubject() 。
- 判断当前用户是否已经被认证,即是否已登录。调用 Subject.isAuthenticated() 。
- 若没有被认证,则把用户名和密码封装为 UsernamePasswordToken 对象。
a.创建一个表单页面。
b.把请求提交到 SpringMVC 的 Handler。
c.获取用户名和密码。
- 执行登录,调用 Subject.login(AuthenticationToken) 方法。
- 自定义认证 Realm,从数据库中获取对应用户的记录,返回给 Shiro。
a.实际上需要继承 org.apache.shiro.realm.AuthenticatingRealm 类。
b.实现 org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo 方法。
- 由 Shiro 完成用户名和密码的校验。
自定义认证 Realm 为什么要继承 org.apache.shiro.realm.AuthenticatingRealm 类实现 doGetAuthenticationInfo 方法?
登录操作是通过 Subject.login(AuthenticationToken) 方法实现的,查看该方法的实现:
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject) subject; //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals: principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } if (principals == null || principals.isEmpty()) { String msg = "Principals returned from securityManager.login( token ) returned a null or " + "empty value. This value must be non null and populated with one or more elements."; throw new IllegalStateException(msg); } this.principals = principals; this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken) token).getHost(); } if (host != null) { this.host = host; } Session session = subject.getSession(false); if (session != null) { this.session = decorate(session); } else { this.session = null; } }
org.apache.shiro.subject.support.DelegatingSubject#login
继续走到第 3 行的 securityManager.login(this, token) 方法:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = authenticate(token); } catch (AuthenticationException ae) { try { onFailedLogin(token, ae, subject); } catch (Exception e) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an " + "exception. Logging and propagating original AuthenticationException.", e); } } throw ae; } Subject loggedIn = createSubject(token, info, subject); onSuccessfulLogin(token, info, loggedIn); return loggedIn; }
org.apache.shiro.mgt.DefaultSecurityManager#login
再进到第 4 行的 authenticate(token) 方法:
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); }
org.apache.shiro.mgt.AuthenticatingSecurityManager#authenticate
接着进到第 2 行的 this.authenticator.authenticate(token) 方法的实现:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { if (token == null) { throw new IllegalArgumentException("Method argument (authentication token) cannot be null."); } log.trace("Authentication attempt received for token [{}]", token); AuthenticationInfo info; try { info = doAuthenticate(token); if (info == null) { String msg = "No account information found for authentication token [" + token + "] by this " + "Authenticator instance. Please check that it is configured correctly."; throw new AuthenticationException(msg); } } catch (Throwable t) { AuthenticationException ae = null; if (t instanceof AuthenticationException) { ae = (AuthenticationException) t; } if (ae == null) { //Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more //severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate: String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " + "error? (Typical or expected login exceptions should extend from AuthenticationException)."; ae = new AuthenticationException(msg, t); if (log.isWarnEnabled()) log.warn(msg, t); } try { notifyFailure(token, ae); } catch (Throwable t2) { if (log.isWarnEnabled()) { String msg = "Unable to send notification for failed authentication attempt - listener error?. " + "Please check your AuthenticationListener implementation(s). Logging sending exception " + "and propagating original AuthenticationException instead..."; log.warn(msg, t2); } } throw ae; } log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info); notifySuccess(token, info); return info; }
org.apache.shiro.authc.AbstractAuthenticator#authenticate
继续进到第 11 行的 doAuthenticate(token) 方法:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate
目前我们只使用一个 Realm,所以接着进到第 5 行的 doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); 方法:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is " + "configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } return info; }
org.apache.shiro.authc.pam.ModularRealmAuthenticator#doSingleRealmAuthentication
继续看 realm.getAuthenticationInfo(token) 方法:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo
我们自己编写的认证 Realm 继承了 org.apache.shiro.realm.AuthenticatingRealm ,所以这里第 5 行的 doGetAuthenticationInfo(token) 方法的实现实际上就是我们自己定义 Realm 中的实现,最后拿到该方法的返回值交给 Shiro 完成校验。
通过上述过程可以知道登录是否成功的关键其实就在于返回值。回头看 org.apache.shiro.authc.AbstractAuthenticator#authenticate 方法的 12-16 行,当 info==null 即我们实现的 doGetAuthenticationInfo 方法的返回值为空时,会抛出 AuthenticationException 异常,即登陆失败。如果不为空,接着在 org.apache.shiro.subject.support.DelegatingSubject#login 方法中就会继续完成对 Subject 即当前用户的授权,即登录成功。
编码
1、创建登录控制器:
package com.zze.shiro.web.controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class LoginController { private static final transient Logger log = LoggerFactory.getLogger(LoginController.class); @RequestMapping("login") public String login(String username, String password) { Subject currentUser = SecurityUtils.getSubject(); if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); token.setRememberMe(true); try { currentUser.login(token); } catch (AuthenticationException ae) { System.out.println(ae.getMessage()); } } return "redirect:list.jsp"; } @RequestMapping("logout") public String logout() { Subject currentUser = SecurityUtils.getSubject(); currentUser.logout(); return "redirect:login.jsp"; } }
com.zze.shiro.web.controller.LoginController
2、创建登录 Realm:
package com.zze.shiro.realms; import org.apache.shiro.authc.*; import org.apache.shiro.realm.AuthenticatingRealm; public class LoginRealm extends AuthenticatingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // authenticationToken 实际就是 Controller 中通过 Subject.Login 方法传入的 token,保存了前台输入的密码 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; // 获取页面传入的用户名 String username = usernamePasswordToken.getUsername(); // 根据用户名从数据库获取密码,假如获取到的是 123456 String dbPassword = "123456"; // 假如 unknown 用户名不存在 if ("unknown".equals(username)) { throw new UnknownAccountException("用户名不存在"); } // 假如 monster 用户名已经被锁定 if ("monster".equals(username)) { throw new LockedAccountException("用户已被锁定"); } // 根据用户情况,构建 AuthenticationInfo 对象返回,通常使用的实现类为 SimpleAuthenticationInfo // a. principal : 认证的实体信息,可以是 username,也可以是用户对应的实体对象 Object principal = username; // b. credentials : 密码 Object credentials = dbPassword; // c. realmName : 当前 realm 对象的 name,调用父类的 getName() 方法获得 String realmName = getName(); SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, realmName); return simpleAuthenticationInfo; // 返回认证信息,交给 Shiro 完成比对 } }
com.zze.shiro.realms.LoginRealm
3、修改登录页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Login Page</title> </head> <body> <form action="/login" method="post"> <table> <tr> <td>username:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>password:</td> <td><input type="password" name="password"></td> </tr> <tr> <td colspan="2" style="text-align: center"> <input type="submit" value="submit"> </td> </tr> </table> </form> </body> </html>
login.jsp
4、注意要修改 Spring 中的 Shiro 拦截配置,允许登录控制器匿名访问:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 1、配置安全管理器 SecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager"/> <property name="realm" ref="jdbcRealm"/> </bean> <!-- 2、配置缓存管理器 CacheManager a、需加入 ehcache jar 和 配置文件 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> </bean> <!-- 4、配置 lifecycleBeanPostProcessor,可以自动调用配置在 Spring IOC 容器中 Shiro bean 的生命周期方法 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- 5、启用 IOC 容器中使用 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> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/list.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /logout = anon /login* = anon /** = authc </value> </property> </bean> <bean id="jdbcRealm" class="com.zze.shiro.realms.LoginRealm"/> </beans>
applicationContext.xml
Shiro 是如何完成密码的比对的呢?
我们已经知道,从页面传入的 username 及 password 已经被封装在 UsernamePasswordToken 实例中通过 Subject.login(AuthenticationToken) 方法交给了 Shiro 传递到了 LoginRealm,而我们在 LoginRealm 中返回的校验信息是正确的用户名及密码信息。可以推断,Shiro 所做的比对其实就是将页面传入的 UsernamePasswordToken 实例和我们返回的正确的校验信息 AuthenticationInfo 实例中的数据进行比对,所以我们可以在返回 SimpleAuthenticationInfo 实例时打个断点,跟着进入源码,肯定能找到 Shiro 是如何完成比对。
下一步,进到调用 doGetAuthenticationInfo 的方法中:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //otherwise not cached, perform the lookup: info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo
可以看到,在第 6 行拿到我们返回的 SimpleAuthenticationInfo 实例,接着将 UsernamePasswordToken 实例和 SimpleAuthenticationInfo 实例一起传入第 16 行的 assertCredentialsMatch(token, info) 方法:
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { CredentialsMatcher cm = getCredentialsMatcher(); if (cm != null) { if (!cm.doCredentialsMatch(token, info)) { String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " + "credentials during authentication. If you do not wish for credentials to be examined, you " + "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }
org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch
再看到第 4 行 cm.doCredentialsMatch(token, info) 方法,通过方法名就可以看出这是个凭证匹配的方法,依旧是将 UsernamePasswordToken 实例和 SimpleAuthenticationInfo 实例一起传入了:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenCredentials = getCredentials(token); Object accountCredentials = getCredentials(info); return equals(tokenCredentials, accountCredentials); }
org.apache.shiro.authc.credential.SimpleCredentialsMatcher#doCredentialsMatch
一目了然, 拿到页面传入的密码和正确的密码进行比对,返回布尔值。接着回到 org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch 方法中,如果比对失败,则抛出 org.apache.shiro.authc.AuthenticationException 异常。
总结:Shiro 是通过 org.apache.shiro.authc.credential.CredentialsMatcher 类实例的 doCredentialsMatch 方法来完成页面传入的密码和实际的密码的比对。
密码的加密
如果我们手动将密码加密,那么保存在数据库的密码就是加密后的密码,这时我们在 Realm 中从数据库获取的密码也就是加密后的密码,返回给 Shiro 进行比对的时候 Shiro 如果还是使用默认页面输入的原始密码与数据库中加密后的密码比对,那么肯定是不可行的。通过上面登录功能的实现我们已经知道 Shiro 是通过 org.apache.shiro.authc.credential.CredentialsMatcher 类实例的 doCredentialsMatch 方法来完成页面传入的密码和实际的密码的比对。显然,该实例的 doCredentialsMatch 方法已经满足不了我们的要求。通过查看源码我们已经知道, org.apache.shiro.authc.credential.CredentialsMatcher 类实例实际上是我们自定义认证类父类中的一个属性,所以我们可以通过给该属性注入一个满足我们要求的 org.apache.shiro.authc.credential.CredentialsMatcher 类的子类实例来重写 doCredentialsMatch 。当然我们可以手动定义子类继承它对原来的 doCredentialsMatch 方法进行重写,但其实 Shiro 已经给我们提供了一些加密类,我们只需要配置一下注入就行。如下:
1、在上面登录示例的基础上修改 Spring 配置文件,给 LoginRealm 父类属性 credentialsMatcher 的注入我们需要的 CredentialsMatcher 实例,我这里使用 MD5 加密:
<bean id="jdbcRealm" class="com.zze.shiro.realms.LoginRealm"> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <!--加密方式--> <property name="hashAlgorithmName" value="MD5"/> <!--加密次数--> <property name="hashIterations" value="1024"/> </bean> </property> </bean>
此时再启动项目,比对密码的方法就是下面这个方法了:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenHashedCredentials = hashProvidedCredentials(token, info); Object accountCredentials = getCredentials(info); return equals(tokenHashedCredentials, accountCredentials); }
org.apache.shiro.authc.credential.HashedCredentialsMatcher#doCredentialsMatch
通过第 2 行可以看到,现在获取的 tokenHashedCredentials 即页面输入的密码是通过 hashProvidedCredentials(token, info) 加密后的密码,查看该方法:
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) { Object salt = null; if (info instanceof SaltedAuthenticationInfo) { salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt(); } else { //retain 1.0 backwards compatibility: if (isHashSalted()) { salt = getSalt(token); } } return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations()); }
org.apache.shiro.authc.credential.HashedCredentialsMatcher#hashProvidedCredentials(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo)
继续看到第 11 行的 hashProvidedCredentials(token.getCredentials(), salt, getHashIterations()) 方法:
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) { String hashAlgorithmName = assertHashAlgorithmName(); return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); }
org.apache.shiro.authc.credential.HashedCredentialsMatcher#hashProvidedCredentials(java.lang.Object, java.lang.Object, int)
可以看到,加密工作其实是通过第三行的 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations) 来完成的。
// hashAlgorithmName : 加密方式 // credentials : 被加密的对象 // salt : 盐值 // hashIterations :加密次数 new org.apache.shiro.crypto.hash.SimpleHash(String hashAlgorithmName, Object credentials, Object salt, int hashIterations)
2、修改 LoginRealm 为如下:
package com.zze.shiro.realms; import org.apache.shiro.authc.*; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.util.ByteSource; public class LoginRealm extends AuthenticatingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // authenticationToken 实际就是 Controller 中通过 Subject.Login 方法传入的 token,保存了前台输入的密码 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; // 获取页面传入的用户名 String username = usernamePasswordToken.getUsername(); // 根据用户名从数据库获取密码,假如获取到的是 123456 加密后的密码 Object credentials = "123456"; // 要加密的对象 String hashAlgorithmName = "MD5"; // 加密方式 ByteSource salt = ByteSource.Util.bytes(username); // 以用户名当做盐值 int hashIterations = 1024; // 加密次数,与 Spring 中配置一致 credentials = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); // 假如 unknown 用户名不存在 if ("unknown".equals(username)) { throw new UnknownAccountException("用户名不存在"); } // 假如 monster 用户名已经被锁定 if ("monster".equals(username)) { throw new LockedAccountException("用户已被锁定"); } // 根据用户情况,构建 AuthenticationInfo 对象返回,通常使用的实现类为 SimpleAuthenticationInfo // a. principal : 认证的实体信息,可以是 username,也可以是用户对应的实体对象 Object principal = username; // b. credentials : 密码 // c. realmName : 当前 realm 对象的 name,调用父类的 getName() 方法获得 String realmName = getName(); SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, salt, realmName); return simpleAuthenticationInfo; // 返回认证信息,交给 Shiro 完成比对 } }
com.zze.shiro.realms.LoginRealm
多Realm认证
实现
其实在上面【登录功能实现】这节查看源码的过程中我们可以看到这个方法:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate
在第 3 行实际上是从当前 ModularRealmAuthenticator 实例中获取一个 realms 属性,而这个属性是一个集合,该属性是用来存放我们所注册的 Realm 实例。
显然,Realm 实例是可以有多个的,我们只需要将它们注入给 ModularRealmAuthenticator 实例,然后将 ModularRealmAuthenticator 实例交给安全管理器即可。如下:
1、创建第二个 Realm 类,密码使用 SHA1 加密:
package com.zze.shiro.realms; import org.apache.shiro.authc.*; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.util.ByteSource; public class SecondRealm extends AuthenticatingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // authenticationToken 实际就是 Controller 中通过 Subject.Login 方法传入的 token,保存了前台输入的密码 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; // 获取页面传入的用户名 String username = usernamePasswordToken.getUsername(); // 根据用户名从数据库获取密码,假如获取到的是 123456 加密后的密码 Object credentials = "123456"; // 要加密的对象 String hashAlgorithmName = "SHA1"; // 加密方式 ByteSource salt = ByteSource.Util.bytes(username); // 以用户名当做盐值 int hashIterations = 1024; // 加密次数,与 Spring 中配置一致 credentials = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); // 假如 unknown 用户名不存在 if ("unknown".equals(username)) { throw new UnknownAccountException("用户名不存在"); } // 假如 monster 用户名已经被锁定 if ("monster".equals(username)) { throw new LockedAccountException("用户已被锁定"); } // 根据用户情况,构建 AuthenticationInfo 对象返回,通常使用的实现类为 SimpleAuthenticationInfo // a. principal : 认证的实体信息,可以是 username,也可以是用户对应的实体对象 Object principal = username; // b. credentials : 密码 // c. realmName : 当前 realm 对象的 name,调用父类的 getName() 方法获得 String realmName = getName(); SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, salt, realmName); return simpleAuthenticationInfo; // 返回认证信息,交给 Shiro 完成比对 } }
com.zze.shiro.realms.SecondRealm
2、在 Spring 核心配置文件中配置多 Realm :
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 1、配置安全管理器 SecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager"/> <property name="realm" ref="jdbcRealm"/> <property name="authenticator" ref="authenticator"/> </bean> <!-- 2、配置缓存管理器 CacheManager a、需加入 ehcache jar 和 配置文件 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> </bean> <!-- 4、配置 lifecycleBeanPostProcessor,可以自动调用配置在 Spring IOC 容器中 Shiro bean 的生命周期方法 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- 5、启用 IOC 容器中使用 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> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/list.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /logout = anon /login* = anon /** = authc </value> </property> </bean> <bean id="jdbcRealm" class="com.zze.shiro.realms.LoginRealm"> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <!--加密方式--> <property name="hashAlgorithmName" value="MD5"/> <!--加密次数--> <property name="hashIterations" value="1024"/> </bean> </property> </bean> <bean id="secondRealm" class="com.zze.shiro.realms.SecondRealm"> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <!--加密方式--> <property name="hashAlgorithmName" value="SHA1"/> <!--加密次数--> <property name="hashIterations" value="1024"/> </bean> </property> </bean> <!--配置多 Realm--> <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <property name="realms"> <list> <ref bean="jdbcRealm"/> <ref bean="secondRealm"/> </list> </property> </bean> </beans>
applicationContext.xml
此时,我们再登录就会使用多 Realm 认证。
认证策略
上面我们已经实现了多 Realm 认证,但肯定还会有一个疑问。我们现在使用的是多个 Realm 认证,那如何才算认证成功呢?对于这个问题的解决,Shiro 为我们提供了认证策略,要使用认证策略我们需要了解下面几个类,这几个类都实现了 org.apache.shiro.authc.pam.AuthenticationStrategy 接口:
- org.apache.shiro.authc.pam.FirstSuccessfulStrategy :只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其它的忽略。
- org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy :只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有 Realm 身份验证成功的认证信息。
- org.apache.shiro.authc.pam.AllSuccessfulStrategy :所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。
org.apache.shiro.authc.pam.ModularRealmAuthenticator 默认使用的是 org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy 策略。
那我们如何配置认证策略呢,还是从源码看。使用多 Realm 认证时会经过这个方法:
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { AuthenticationStrategy strategy = getAuthenticationStrategy(); AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); if (log.isTraceEnabled()) { log.trace("Iterating through {} realms for PAM authentication", realms.size()); } for (Realm realm : realms) { aggregate = strategy.beforeAttempt(realm, token, aggregate); if (realm.supports(token)) { log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm); AuthenticationInfo info = null; Throwable t = null; try { info = realm.getAuthenticationInfo(token); } catch (Throwable throwable) { t = throwable; if (log.isWarnEnabled()) { String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:"; log.warn(msg, t); } } aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); } else { log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); } } aggregate = strategy.afterAllAttempts(token, aggregate); return aggregate; }
org.apache.shiro.authc.pam.ModularRealmAuthenticator#doMultiRealmAuthentication
可以看到在第三行 getAuthenticationStrategy() 就是在获取认证策略,而认证策略也是 ModularRealmAuthenticator 的一个属性,所以我们只需要将我们要使用的认证策略类注入给认证器 ModularRealmAuthenticator 实例即可:
<!--认证器配置多 Realm--> <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <!--配置多 Realm--> <property name="realms"> <list> <ref bean="jdbcRealm"/> <ref bean="secondRealm"/> </list> </property> <!--配置认证策略--> <property name="authenticationStrategy"> <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/> </property> </bean>
在上面我们是把 realms 属性配置给 ModularRealmAuthenticator ,但我们通常是直接把 realms 直接配置给安全管理器,如下:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager"/> <property name="authenticator" ref="authenticator"/> <!--配置多 Realm--> <property name="realms"> <list> <ref bean="jdbcRealm"/> <ref bean="secondRealm"/> </list> </property> </bean> <!--认证器配置多 Realm--> <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <!--配置认证策略--> <property name="authenticationStrategy"> <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/> </property> </bean>
但我们知道 org.apache.shiro.authc.pam.ModularRealmAuthenticator#doMultiRealmAuthentication 方法中是要通过 ModularRealmAuthenticator 实例的属性拿到 realms 使用的。为什么这样配置依然好使呢?查看 DefaultWebSecurityManager 的 setRealms 方法:
public void setRealms(Collection<Realm> realms) { if (realms == null) { throw new IllegalArgumentException("Realms collection argument cannot be null."); } if (realms.isEmpty()) { throw new IllegalArgumentException("Realms collection argument cannot be empty."); } this.realms = realms; afterRealmsSet(); }
org.apache.shiro.mgt.RealmSecurityManager#setRealms
接着查看 9 行的 afterRealmsSet() 方法:
protected void afterRealmsSet() { super.afterRealmsSet(); if (this.authorizer instanceof ModularRealmAuthorizer) { ((ModularRealmAuthorizer) this.authorizer).setRealms(getRealms()); } }
org.apache.shiro.mgt.AuthorizingSecurityManager#afterRealmsSet
看到这里就可以明了,其实在把 realms 注入给安全管理器 RealmSecurityManager 时,安全管理器同时也把 realms 的引用给了 ModularRealmAuthenticator 的 realms 属性。
授权
介绍
授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面、编辑数据、页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
- 主体(Subject):访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。
- 资源(Resource):在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只有授权后才能访问。
- 权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源。如:访问用户列表页面查看、新增、修改、删除用户数据(即很多时候都是 CRUD 式权限控制)等。权限代表了用户有没有操作某个资源的权力,即反映了在某个资源上的操作允不允许。
- 角色(Role):角色实际上就是权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。
Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(如操作某个用户的权限,实例级别)。
授权方式
Shiro 中支持三种方式的授权:
- 编程式:通过写 if/else 授权代码块完成。
if(subject.hasRole("admin")){ // 有权限 }else{ // 无权限 }
例:
- 注解式:通过在执行的 Java 方法上放置相应的注解完成,没有权限将抛出相应的异常。
@RequiresRoles("admin") // 有权限时才进入方法,无权限时抛出异常 public void hello(){ // 有权限 }
例:
- JSP 标签:在 JSP 页面通过相应的标签完成。
<shiro:hasRole name="admin"> // 有权限 </shiro:hasRole>
例:
配置
有两个页面 admin.jsp 和 user.jsp,如果我们需要控制访问 admin.jsp 需要用户拥有 admin 角色、访问 user.jsp 拥有 user 角色,则可如下配置:
<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"/> <!-- 配置哪些页面需要受保护 以及 访问这些页面需要的权限 a、anon 可以匿名访问 b、authc 必须认证(登陆)后才可以访问 --> <property name="filterChainDefinitions"> <value> /admin.jsp=roles[admin] /user.jsp=roles[user] /logout=logout /login*=anon /** = authc </value> </property> </bean>
配置后即便我们已经登录,访问 admin.jsp 和 user.jsp 都会跳转到 unauthorized.jsp,因为当前登录用户并没有 admin 和 user 这两个角色。
授权Realm
- 授权 Realm 需要继承 org.apache.shiro.realm.AuthorizingRealm 类,并实现其 doGetAuthorizationInfo 方法。
- AuthorizingRealm 类继承自 org.apache.shiro.realm.AuthenticatingRealm ,但没有实现 AuthenticatingRealm 中的 doGetAuthenticationInfo 方法,所以要完成认证和授权只需要继承 AuthorizingRealm 类,同时实现它的两个抽象方法即可。
看一下授权操作的执行流程,在上面的快速开始 Demo 中我们已经知道,校验当前用户是否拥有某个角色是通过 Subject.hasRole 方法:
public boolean hasRole(String roleIdentifier) { return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier); }
org.apache.shiro.subject.support.DelegatingSubject#hasRole
接着看到 securityManager.hasRole(getPrincipals(), roleIdentifier) 方法:
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { return this.authorizer.hasRole(principals, roleIdentifier); }
org.apache.shiro.mgt.AuthorizingSecurityManager#hasRole
接着是第 2 行的 this.authorizer.hasRole(principals, roleIdentifier) 方法,当是单个授权 Realm 时:
public boolean hasRole(PrincipalCollection principal, String roleIdentifier) { AuthorizationInfo info = getAuthorizationInfo(principal); return hasRole(roleIdentifier, info); }
org.apache.shiro.realm.AuthorizingRealm#hasRole(org.apache.shiro.subject.PrincipalCollection, java.lang.String)
重点就在第 2 行的 getAuthorizationInfo(principal) :
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { if (principals == null) { return null; } AuthorizationInfo info = null; if (log.isTraceEnabled()) { log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]"); } Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache(); if (cache != null) { if (log.isTraceEnabled()) { log.trace("Attempting to retrieve the AuthorizationInfo from cache."); } Object key = getAuthorizationCacheKey(principals); info = cache.get(key); if (log.isTraceEnabled()) { if (info == null) { log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]"); } else { log.trace("AuthorizationInfo found in cache for principals [" + principals + "]"); } } } if (info == null) { // Call template method if the info was not found in a cache info = doGetAuthorizationInfo(principals); // If the info is not null and the cache has been created, then cache the authorization info. if (info != null && cache != null) { if (log.isTraceEnabled()) { log.trace("Caching authorization info for principals: [" + principals + "]."); } Object key = getAuthorizationCacheKey(principals); cache.put(key, info); } } return info; }
org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo
看到第 32 行,我们自定义的授权 Realm 继承了 AuthorizingRealm ,所以这里调的 doGetAuthorizationInfo 方法实际上就是需要我们自己实现的 doGetAuthorizationInfo 方法,最后返回该方法的返回值。回到 org.apache.shiro.realm.AuthorizingRealm#hasRole(org.apache.shiro.subject.PrincipalCollection, java.lang.String) 方法,通过 hasRole(roleIdentifier, info) 方法来检验用户是否拥有某角色:
protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) { return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier); }
org.apache.shiro.realm.AuthorizingRealm#hasRole(java.lang.String, org.apache.shiro.authz.AuthorizationInfo)
而如果是多授权 Realm 的情况下,在 org.apache.shiro.mgt.AuthorizingSecurityManager#hasRole 方法的第二行的 this.authorizer.hasRole(principals, roleIdentifier) 方法就会走这个方法:
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { assertRealmsConfigured(); for (Realm realm : getRealms()) { if (!(realm instanceof Authorizer)) continue; if (((Authorizer) realm).hasRole(principals, roleIdentifier)) { return true; } } return false; }
org.apache.shiro.authz.ModularRealmAuthorizer#hasRole
可以看到,只要有一个授权 Realm 返回的信息包含了指定角色,校验就通过了。
所以示例中的 LoginRealm 就可以修改如下:
package com.zze.shiro.realms; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import java.util.HashSet; import java.util.Set; public class LoginRealm extends AuthorizingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // authenticationToken 实际就是 Controller 中通过 Subject.Login 方法传入的 token,保存了前台输入的密码 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; // 获取页面传入的用户名 String username = usernamePasswordToken.getUsername(); // 根据用户名从数据库获取密码,假如获取到的是 123456 String dbPassword = "123456"; // 假如 unknown 用户名不存在 if ("unknown".equals(username)) { throw new UnknownAccountException("用户名不存在"); } // 假如 monster 用户名已经被锁定 if ("monster".equals(username)) { throw new LockedAccountException("用户已被锁定"); } // 根据用户情况,构建 AuthenticationInfo 对象返回,通常使用的实现类为 SimpleAuthenticationInfo // a. principal : 认证的实体信息,可以是 username,也可以是用户对应的实体对象 Object principal = username; // b. credentials : 密码 Object credentials = dbPassword; // c. realmName : 当前 realm 对象的 name,调用父类的 getName() 方法获得 String realmName = getName(); SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, realmName); return simpleAuthenticationInfo; // 返回认证信息,交给 Shiro 完成比对 } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 1、获取登录用户的信息 Object primaryPrincipal = principals.getPrimaryPrincipal(); // 2、利用登录用户信息来获取当前用户的角色或权限(可能需要查询数据库) Set<String> roles = new HashSet<String>(); roles.add("user"); if ("admin".equals(primaryPrincipal)) { roles.add("admin"); } // 3、创建 SimpleAuthorizationInfo 实例,并设置其 reles 属性 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roles); return simpleAuthorizationInfo; } }
com.zze.shiro.realms.LoginRealm
上述代码中给任何已认证的用户都分配了 user 角色,给 admin 用户分配了 admin 角色。此时以 user 用户登录就可以访问 user.jsp ,但不可以访问 admin.jsp ,以 admin 用户登录就可以访问 user.jsp 和 admin.jsp 。
Shiro标签
Shiro 提供了 JSTL 标签库用于 JSP 页面进行权限控制,如根据登录用户显示相应的页面按钮。
- <shiro:guest> :用户没有身份验证时显示相应信息,即游客访问信息。
<shiro:guest> 欢迎游客访问,<a href='/login.jsp'>登录 </shiro:guest>
例:
- <shiro:user> :用户已经经过认证或通过“记住我”登录后显示相应的信息。
<shiro:user> 欢迎[<shiro:principal/>]访问,<a href='/logout'>退出</a> </shiro:user>
例:
- <shiro:authenticated> :用户已经身份验证通过,即通过 Subject.login 方法登录成功,非记住我登录。
<shiro:authenticated> 用户[<shiro:principal/>]身份验证已通过 </shiro:authenticated>
例:
- <shiro:notAuthenticated> :用户未进行身份验证,即没有通过 Subject.login 进行登录,包括通过“记住我”自动登录的也属于未进行身份验证。
<shiro:notAuthenticated> 未进行身份验证(包括记住我) </shiro:notAuthenticated>
例:
- <shiro:principal> :显示用户身份信息,默认调用 Subject.getPrincipal 方法获取,即 Primary Principal 。
<shiro:principal property="username"/>
例:
- <shiro:hasRole> :如果当前 Subject 有指定角色将显示标签内的内容。
<shiro:hasRole name="admin"> 用户[<shiro:principal/>]拥有角色 admin </shiro:hasRole>
例:
- <shiro:hasAnyRoles> :如果当前 Subject 有任意一个角色,则显示标签内内容。
<shiro:hasAnyRoles name="admin,user"> 用户[<shiro:principal/>]拥有角色 admin 或 user </shiro:hasAnyRoles>
例:
-
<shiro:lacksRole> :如果当前 Subject 没有指定角色将显示标签内内容。
<shiro:lacksRole name="admin"> 用户[<shiro:principal/>]没有角色 admin </shiro:lacksRole>
例:
-
<shiro:hasPermission> :如果当前 Subject 有指定权限将显示标签内内容。
<shiro:hasPermission name="user:create"> 用户[<shiro:principal/>]拥有权限 user:create </shiro:hasPermission>
例:
- <shiro:acksPermission> :如果当前 Subject 没有指定权限将显示标签内内容。
<shiro:lacksPermission name="org:create"> 用户[<shiro:principal/>]没有权限 org:create </shiro:lacksPermission>
例:
只需在 jsp 中引入标签库即可使用:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
权限注解
介绍
权限注解的作用就是修饰一个方法,让这个方法只有在当前 Subject 满足一定的条件时才能访问。
- @RequiresAuthentication :当前 Subject 已经通过 login 进行了身份验证,即 Subject.isAuthenticated 方法返回 true 时。
- @RequiresUser :当前 Subject 已经身份验证或通过“记住我”自动登录。
- @RequiresGuest :当前 Subject 没有身份验证(包含通过“记住我”登录),为游客身份时。
- @RequiresRoles(value={"admin","user"},logical=Logical.AND) :当前 Subject 需要拥有角色 admin 和 user。
- @RequiresPermissions(value={"user:a","user.b"},logical=Logical.OR) :当前 Subject 需要=拥有权限 user:a 或 user:b。
示例
1、新建测试使用的 Controller 并使用 Shiro 注解:
package com.zze.shiro.controller; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class TestController { @RequestMapping("testHasRole") @RequiresRoles({"admin"}) // 只有拥有 admin 角色的用户才可以访问 public String testHasRole() { System.out.println("from testHasRole"); return "redirect:list.jsp"; } }
com.zze.shiro.controller.TestController
2、修改 list.jsp :
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <html> <head> <title>Title</title> </head> <body> Welcome 【<shiro:principal/>】 <br> <h3>List Page</h3> <a href="/user.jsp">user</a> <br> <a href="/admin.jsp">admin</a><br> <a href="/logout">logout</a><br> <a href="/testHasRole">testHasRoleAnno</a> </body> </html>
list.jsp
3、测试访问:
以 admin 用户登录,访问 localhost:8080/testHasRole ,正常访问。以 user 用户登录,访问 localhost:8080/testHasRole ,抛出如下异常:
此时我们就可以通过 SpringMVC 的全局异常处理来捕获这个异常,给用户返回友好的页面。
如注解失效,需要将下面配置移到 SpringMVC 核心配置文件中:
<!-- 配置 lifecycleBeanPostProcessor,可以自动调用配置在 Spring IOC 容器中 Shiro bean 的生命周期方法 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- 启用 IOC 容器中使用 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>
会话管理
相关API
- Subject.getSession() :即刻获取会话,等价于 Subject.getSession(true) ,即如果当前没有创建 Session 对象则会立即创建一个;而 Subject.getSession(false) 则是如果当前没有 Session 则返回 null 。
- session.getId() :获取当前会话的唯一标识。
- session.getHost() :获取当前会话的主机地址。
- session.getTimeout()&session.setTimeout(毫秒) :获取/设置当前 Session 的过期时间。
- session.getStartTimestamp()&session.getLastAccessTime() :获取会话的启动时间及最后访问时间。如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是 web 应用,每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。
- session.touch()&session.stop() :更新会话最后访问时间及销毁会话。当执行 Subject.logout() 时会自动调用 session.stop() 方法来销毁会话。如果在 web 应用程序中,调用 HttpSession.invalidate() 也会自动调用 Shiro 的 session.stop() 来销毁 Shiro 的会话。
- session.setAttribute(key,val)&session.getAttribute(key)&session.removeAttribute(key) :设置/获取/删除会话属性。
Shiro 提供的 Session 有一个特点就是非侵入式,看如下示例:
package com.zze.shiro.web.controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.session.Session; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpSession; @Controller public class TestController { @RequestMapping("testSession") public void test(HttpSession httpSession){ httpSession.setAttribute("key","from HttpSessionValue"); Session shiroSession = SecurityUtils.getSubject().getSession(); Object key = shiroSession.getAttribute("key"); System.out.println(key); //输出 from HttpSessionValue } }
com.zze.shiro.web.controller.TestController
在上面代码中,我们往当前的 HttpSession 实例中存放了一个键值对,接着我们从 Shiro 提供的 Session 中获取到了之前存放在 HttpSession 中的键值对。这个现象说明了我们可以在当前会话中的任意位置获取到 Session 中的数据,例如 service 中。
会话监听器
会话监听器用于监听会话的创建、过期即停止事件。需要实现 SessionListener 接口:
package org.apache.shiro.session; public interface SessionListener { // 会话开始 void onStart(Session session); // 会话销毁 void onStop(Session session); // 会话过期 void onExpiration(Session session); }
使用如下:
1、自定义一个会话监听器类:
package com.zze.shiro.web.listener; import org.apache.shiro.session.Session; import org.apache.shiro.session.SessionListener; public class ShiroSessionListener implements SessionListener { public void onStart(Session session) { System.out.println("session 创建"); } public void onStop(Session session) { System.out.println("session 销毁"); } public void onExpiration(Session session) { System.out.println("session 过期"); } }
com.zze.shiro.web.listener.ShiroSessionListener
2、在 Spring 核心配置文件中配置会话管理器,注入自定义会话监听器,接着把会话管理器注入给安全管理器:
<!-- 自定义session监听器 --> <bean id="shiroSessionListener" class="com.zze.shiro.web.listener.ShiroSessionListener"/> <!-- 会话管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="sessionListeners"> <list> <ref bean="shiroSessionListener"/> </list> </property> </bean> <!-- 配置安全管理器 SecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager"/> <property name="authenticator" ref="authenticator"/> <!--配置多 Realm--> <property name="realms"> <list> <ref bean="jdbcRealm"/> <ref bean="secondRealm"/> </list> </property> <property name="sessionManager" ref="sessionManager"/> </bean>
SessionDao
介绍
通过 SessionDao 可以把 Session 保存在我们想要保存的地方,比如 MySQL,也可以是 Redis,甚至是一个文件,我们可以自定义它的 CRUD 操作。下面是 Shiro 提供的几个 SessionDao 管理实现:
- org.apache.shiro.session.mgt.eis.AbstractSessionDAO :提供了 org.apache.shiro.session.mgt.eis.SessionDAO 的基本实现,如生成会话 ID 等。
- org.apache.shiro.session.mgt.eis.CachingSessionDAO :提供了对开发者透明的会话缓存功能,需要设置相应的 CacheManager。
- org.apache.shiro.session.mgt.eis.MemorySessionDAO :直接在内存中进行会话维护。
- org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO :提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。
示例
下面是一个将 Session 保存在 MySQL 中的一个示例。
1、引入对象序列化工具类:
package com.zze.shiro.web.utils; import org.apache.shiro.codec.Base64; import org.apache.shiro.session.Session; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class SerializableUtils { public static String serialize(Session session) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(session); return Base64.encodeToString(bos.toByteArray()); } catch (Exception ex) { throw new RuntimeException("serialize session error", ex); } } public static Session deSerialize(String sessionStr) { try { ByteArrayInputStream bis = new ByteArrayInputStream(Base64.decode(sessionStr)); ObjectInputStream ois = new ObjectInputStream(bis); return (Session) ois.readObject(); } catch (Exception ex) { throw new RuntimeException("deserialize session error", ex); } } }
com.zze.shiro.web.utils.SerializableUtils
2、自定义 SessionDao 的实现,重写对 Session CRUD 的方法:
package com.zze.shiro.web.utils; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.ValidatingSession; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; import org.springframework.jdbc.core.JdbcTemplate; import java.io.Serializable; import java.util.List; public class MySessionDao extends EnterpriseCacheSessionDAO { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override protected Serializable doCreate(Session session) { Serializable sessionId = super.doCreate(session); String sql = "insert into sessions(id,session) values(?,?)"; jdbcTemplate.update(sql, sessionId, SerializableUtils.serialize(session)); return session.getId(); } @Override protected Session doReadSession(Serializable sessionId) { String sql ="select session from sessions where id=?"; List<String> sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId); if(sessionStrList.size()==0){ return null; } return SerializableUtils.deSerialize(sessionStrList.get(0)); } @Override protected void doUpdate(Session session) { if(session instanceof ValidatingSession && !((ValidatingSession)session).isValid()){ return; } String sql = "update sessions set session=? where id=?"; jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId()); } @Override protected void doDelete(Session session) { String sql = "delete from sessions where id=?"; jdbcTemplate.update(sql, session.getId()); } }
com.zze.shiro.web.utils.MySessionDao
3、配置 SessionDao:
<!--配置数据源--> <bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///test"/> <property name="username" value="root"/> <property name="password" value="root"/> </bean> <!--JDBC 模板--> <bean name="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!--SessionId 生成器--> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/> <!--sessionDao--> <bean id="sessionDao" class="com.zze.shiro.web.utils.MySessionDao"> <!--缓存名称,对应 ehcache.xml 中的缓存名称--> <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/> <!--指定 SessionId 生成器--> <property name="sessionIdGenerator" ref="sessionIdGenerator"/> <!--注入 jdbc 模板--> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <!--会话管理器--> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="sessionDAO" ref="sessionDao"/> <!-- Session 失效时长,单位毫秒 --> <property name="globalSessionTimeout" value="1800000"/> <!-- 删除失效的 Session --> <property name="deleteInvalidSessions" value="true"/> <!-- 是否定期检查 Session 的有效性 --> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionListeners"> <list> <ref bean="shiroSessionListener"/> </list> </property> </bean>
Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将会停止会话。出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的,但如果在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器 org.apache.shiro.session.mgt.SessionValidationScheduler ,也提供了使用 Quartz 的会话验证调度器: org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler 。
缓存
CacheManagerAware接口
Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如 Realm)是否实现了 CacheManagerAware 并自动注入相应的 CacheManager。
Realm缓存
Shiro 提供了 CachingRealm ,其实现了 CacheManagerAware 接口,提供了缓存的一些基础实现。AuthenticatingRealm 及 AuthorizingRealm 也分别提供了对 AuthenticationInfo 和 AuthorizationInfo 信息的缓存。
Session缓存
如 SecurityManager 继承了 SessionSecurityManager,其会判断 SessionSecurityManager 是否实现了 CacheManagerAware 接口,如果实现了会把 CacheManager 设置给它。
SessionManager 也会判断相应的 SessionDAO (如继承自 CachingSessionDAO)是否实现了 CacheManagerAware,如果实现了会把 CacheManager 设置给它。
设置了缓存的 SessionManager,查询时会先查缓存,如果找不到才查数据库。
记住我
介绍
Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器,下次再打开还能记住你是谁,下次访问时无需再登录即可访问,通过调用 org.apache.shiro.authc.UsernamePasswordToken#setRememberMe(true) 方法实现,基本流程如下:
- 首先在登录页面选中 RememberMe 然后登录成功,如果是浏览器登录,一般会把 RememberMe 的 Cookie 写到客户端并保存下来。
- 关闭浏览器再重新打开,会发现浏览器还是记住你的。
- 访问一般的网页服务端还是知道你是谁,且能正常访问。
- 但是比如我们在淘宝查看我的订单或进行支付时,此时还是需要再次进行身份认证的,以确保当前的用户还是你。
认证和记住我
- Subject.isAuthenticated :是否进行了身份验证,即是否是通过了 Subject.login 进行访问。
- Subject.IsRemembered :表示用户是通过记住我登录的,此时可能并不是真正的你(如你的朋友在使用你的电脑或你的 cookie 被窃取)访问。
- 二者同时只有一个为真。
- 访问一般网页:如个人在主页之类的网页时,我们使用 user 拦截器即可,user 拦截器只需要用户是非游客状态,即 Subject.isRemembered()||Subject.isAuthenticated() 为真时即可。
- 访问特殊网页:如我的订单,提交订单页面,我们可使用 authc 拦截器,authc 拦截器会判断用户是否是通过 Subject.login 方法登录的即 Subject.isAuthenticated() 状态为 true 。