Apache Shiro 的Web支持
配置
将Shiro集成到web
项目中最简单的方式就是在web.xml
中配置一个Servlet
的ContextListener
和过滤器,让Web项目知道如何读取Shiro的INI
配置文件。关于INI配置文件的内容,在前面有描述,这里将会讲述一些Web相关的小节
如果使用了
Spring
框架,将不需要考虑这一步,详情请看Spring
集成Shiro的相关文档
web.xml
Shiro1.2及以后
在Shiro1.2及之后
的版本中,标准的Web
应用程序集成Shiro,就是将下列的xml内容添加到web.xml
中:
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
...
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
如果没有显式配置INI配置文件的位置,Shiro将会按照下面的顺序找配置文件,使用第一个找到的配置文件:
/WEB-INF/shiro.ini
类路径的根路径下的shiro.ini
以上的配置其实是做了以下的事:
-
EnvironmentLoaderListener
会初始化一个Shiro的WebEnvironment
实例(该实例包含了Shiro需要的所有东西,包括SecurityManager
),并将这个实例设置到了ServletContext
中使其可用。如果需要获取这个WebEnvironment
实例,可以通过WebUtils.getRequiredWebEnvironment(servletContext)
获得 -
ShiroFilter
将会通过WebEnvironment
来执行所有的安全相关操作 - 最后,
filter-mapping
定义确保了所有的请求都能经过ShiroFilter
的过滤
通常需要把
ShiroFilter
的filter-mapping
定义在所有的filter-mapping之前
,确保Shiro也能作用在其他过滤器之中
ShiroFilter
是一个标准的servlet
过滤器,它的默认编码是ISO-8859-1
,然而客户端可以通过使用请求头的Content-Type头部的charset属性
来指定编码
自定义 WebEnvironment 类
EnvironmentLoaderListener
将默认创建的是IniWebEnvironment
实例,顾名思义是通过INI
配置Shiro。如果有需要,可以在web.xml
中为ServletContext
指定一个context-param
参数,来指定WebEnvironment
<context-param>
<param-name>shiroEnvironmentClass</param-name>
<param-value>com.foo.bar.shiro.MyWebEnvironment</param-value>
</context-param>
这种方式可以让我们自定义Shiro配置的格式,并将其解析为一个WebEnvironment
实例,可以继承IniWebEnvironment
来继续使用ini
的配置格式,或者完全自定义配置格式
配置文件路径
IniWebEnvironment
类需要读取INI
配置文件才能配置Shiro,默认情况下,这个类将会自动的从以下两个位置顺序的寻找配置文件,只要找到了配置文件就会使用它(如果同时在两个位置都有配置文件,则会使用第一个/WEB-INF/shiro.ini
)
/WEB-INF/shiro.ini
classpath:shiro.ini
可以在web.xml
中配置一个context-param
来指定配置文件的位置
<context-param>
<param-name>shiroConfigLocations</param-name>
<param-value>YOUR_RESOURCE_LOCATION_HERE</param-value>
</context-param>
默认情况下,这个参数值会按照ServletContext.getResource()
方法来解析,如"/WEB-INF/some/path/shiro.ini"
。Shiro的ResourceUtils
类也支持"url:"
,"file:"
这样的资源路径前缀来指定路径,下面的路径也是可以使用的
file:/home/foobar/myapp/shiro.ini
classpath:com/foo/bar/shiro.ini
url:http://confighost.mycompany.com/myapp/shiro.ini
Shiro1.1及之前
Web应用集成Shiro1.1
及之前版本,最简单的就是定义IniShiroFilter
及其相关filter-mapping
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
</filter>
...
<!-- Make sure any request you want accessible to Shiro is filtered. /* catches all -->
<!-- requests. Usually this filter mapping is defined first (before all others) to -->
<!-- ensure that Shiro works in subsequent filters in the filter chain: -->
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
以上定义要求INI
配置,且INI
配置文件在类路径的根路径下,即classpath:shiro.ini
。注意这里放在/WEB-INF/
下面无效
配置文件路径
如果不想让INI
配置文件放在classpath:shiro.ini
,也可以指定一个资源文件路径。需要为ShiroFilter
添加一个configPath
参数
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
<init-param>
<param-name>configPath</param-name>
<param-value>classpath:anotherFile.ini</param-value>
</init-param>
</filter>
...
请注意,ServletContext
资源路径是在Shiro1.2
及之后可用的,在1.1及之前
的版本中,所有的configPath
定义都需要指定classpath:
,file:
或者url:
前缀,也就是说,这里不能写/WEB-INF/shiro.ini
,就算写了也会没有效果。官网写的有点模糊,试了下,应该是这样
在web.xml文件中嵌入Shiro配置
可以使用名为config
的init-param
的参数设置,将INI
配置嵌入到web.xml
中,这样就不需要使用额外的ini
配置文件了
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
<init-param><param-name>config</param-name><param-value>
# INI Config Here
</param-value></init-param>
</filter>
...
这种配置方式比较适用于小的,简单的应用,但出于以下原因,将其放到专门的shiro.ini
文件中通常更为方便:
- 你可能会频繁修改安全的配置,这会为
web.xml
增加一个版本控制噪声 - 你可能想要将安全相关的配置从
web.xml
中分离出来 - 你可能需要写很多
Shiro
的相关配置,如果写了太多,反而会让web.xml
文件过于臃肿 - 相同的
Shiro
配置需要用在多个部分时
Web相关的INI配置项
[main]
,[users]
,[roles]
小节前面已经讲述过了,这里重点介绍Web项目特有的[urls]
小节
# [main], [users] and [roles] above here
...
[urls]
...
[urls]
小节允许你为你的应用程序中任何匹配的URL
路径定义专门的过滤器链的能力,这比在web.xml
中定义过滤器链的方式更加灵活、强大和简洁,即使只使用这个特性,它也值得一用
[urls]
URL
路径表达式
[urls]
小节的每一行遵从以下格式:
_URL_Ant_Path_Expression_ = _Path_Specific_Filter_Chain_
例如:
...
[urls]
/index.html = anon
/user/create = anon
/user/** = authc
/admin/** = authc, roles[administrator]
/rest/** = authc, rest
/remoting/rpc/** = authc, perms["remote:invoke"]
等号左边的是一个Ant
风格的路径表达式,注意,所有的路径表达式都是从应用程序环境中的根目录开始匹配的,也就是说,如果你的部署从www.somehost.com/myapp
换成了www.anotherhost.com
,配置不需要更改,依然有效。所有的路径都是相对于HttpServletRequest.getContextPath()
的值
到来的请求会按照
urls
定义的顺序来匹配,第一个匹配的配置项则为该路径需要通过的过滤器链。假如有如下配置/account/** = ssl, authc /account/signup = anon
则请求路径
/account/signup
将永远不会被正确的处理(匿名可访问),应为第一条配置项/account/**
已经匹配了这个请求,后面的就没用了
过滤器链定义
等号右边就是以逗号风格的过滤器链,它的格式如下
filter1[optional_config1], filter2[optional_config2], ..., filterN[optional_configN]
-
filterN
是Filter
在[main]
小节中定义的名字 -
[optional_config]
是一个带括号的可选字符串,它只对部分路径的部分过滤器有意义,如果过滤器不需要这个特定的配置,可以放弃方括号,这样filter[]
就变成了filter
请求通过过滤链的顺序就是它们在配置文件中定义的顺序。
响应也会通过这个过滤器链,如果没有满足必要条件(例如执行重定向、响应带有HTTP错误代码、直接呈现等),每个过滤器都可以自由地处理响应。否则,它将允许请求继续通过链到达最终目标视图
通过
[optional_configN]
能够对特定路径配置做出反应,这是Shiro的独特特性,如果你想要创建一个能做到这些功能的javax.servlet.Filter
实现,需要确保其继承了org.apache.shiro.web.filter.PathMatchingFilter
可用的过滤器
在[main]
小节中可以定义需要使用的过滤器,其中定义的名字就是在过滤器链的定义中需要使用的名字,如下所示
[main]
...
myFilter = com.company.web.some.FilterImplementation
myFilter.property1 = value1
...
[urls]
...
/some/path/** = myFilter
默认过滤器
当运行一个web应用的时候,Shiro默认会创建一些有用的过滤器实例,我们可以在[main]
小节中直接使用这些实例,也可以在[main]
小节中对它们进行配置,就和其他定义的bean
一样
[main]
...
# 我们并没有定义FormAuthenticationFilter ('authc'),这是Shiro自动定义好的
authc.loginUrl = /login.jsp
...
[urls]
...
# 如果用户没有经过认证,则防止这个URL时将会重定向到authc.loginUrl配置的路径,之后如果用户通过了认证,则将会回到原来的页面
/account/** = authc
...
以下是Shiro自动定义好的过滤器及其在main中可用的名字
名字 | 类 |
---|---|
anno | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
authcBearer | org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter |
invalidRequest | org.apache.shiro.web.filter.InvalidRequestFilter |
logout | org.apache.shiro.web.filter.authc.LogoutFilter |
noSessionCreation | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
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.zuthc.UserFilter |
开启和关闭过滤器
在过滤器链中开启或关闭一个过滤器最直接的方式,就是通过将其从过滤器链定义中添加或删除其名字。但是在Shiro1.2之后
新增加了一个特性,可以不用通过前面那种方式就能开启或关闭过滤器,可以通过一个配置的属性来判断是否开启过滤器,甚至也可以通过每一个请求来判断
Shiro提供了一个OncePerRequestFilter
的抽象类,所有的继承它的过滤器都可以在不将其从过滤器链中删除的情况下控制其是否生效,我们也可以继承这个类来实现这个功能
一般的开启和关闭
OncePerRequestFilter
的开关可以对所有的请求生效,也可以对基于某一个请求生效。设置enabled
属性为true或false
就可以设置它的开关,该属性的默认值为true
,因此大多数过滤器都是开启状态的
[main]
...
# 配置ssl过滤器失效
ssl.enabled = false
[urls]
...
/some/path = ssl, authc
/another/path = ssl, roles[admin]
...
绝大多数情况下,URL
都是配置了SSL
的,如果在开发时还开启了SSL
,那是很难受的,所以可以先将其关闭,在生产环境下再把它开起来
根据请求
OncePerRequestFilter
还可以通过它的isEnable(request, response)
方法来对每一个请求进行检查,判断是否需要经过该过滤器过滤。该方法的默认实现是直接返回enable
属性,也就是说,本质上OncePerRequestFilter
就是通过isEnable
方法来判断是否开启该过滤器的。我们可以通过重写这个isEnable(request, response)
来进行更细致的检查
根据路径
Shiro的PathMatchingFilter
(OncePerRequestFilter
的子类)拥有对特定路径的请求进行过滤的能力,也就是说,可以对一个请求的路径来判断是否开启过滤器。如果你的过滤器需要这种能力,则可以重写PathMatchingFilter
的isEnabled(request, response, path, pathConfig)
方法
全局过滤器
从Shiro1.6
开始,可以定义一个一条全局过滤器链,它将会作用于所有的路由,包括哪些没有配置过滤器链的路由。默认情况下,这个全局过滤器链包含了invalidRequest
过滤器(上面的表格中有),这个过滤器是负责过滤掉无效请求的,可以阻挡一些恶意攻击
全局过滤器链的配置和关闭如下
[main]
...
# 关闭全局过滤器链
filterChainResolver.globalFilters = null
定义一条全局过滤器链
[main]
...
filterChainResolver.globalFilters = invalidRequest, port
invalidRequest
过滤器会阻塞使用非ascii字符、分号和反斜杠的请求,可以单独禁用其中的每一个,以便于向后兼容
[main]
...
invalidRequest.blockBackslash = true
invalidRequest.blockSemicolon = true
invalidRequest.blockNonAscii = true
...
如果你的应用程序允许在
URL
中重写jsessionid
,那么必须将blockSemicolon
设置为false
HSTS
SslFilter
及其所有的子类,支持HSTS
的概念,我们可以开启或关闭HSTS
[main]
...
# 配置ssl过滤器开启HSTS
ssl.enabled = true
ssl.hsts.enabled = true
ssl.hsts.includeSubDomains = true
[urls]
...
/some/path = ssl, authc
/another/path = ssl, roles[admin]
...
会话管理
Servlet容器会话
在Web环境下,Shiro的默认会话管理器是ServletContainerSessionManager
,Shiro会把所有的会话管理都委托给Servlet容器
(包括会话集群,如果Servlet容器
支持的话)。这是一个Shiro的SessionAPI
与Servlet容器
的桥梁
这种方式的好处就是,应用程序可以使用Servlet容器
的配置,如会话超时,容器的集群设置等等。缺点就是应用程序和Servlet容器
绑定了,比如如果开发环境使用的是Jetty,生产环境使用Tomcat,那么一些特定于容器的配置将会需要更改
Servlet容器会话的超时时间
如果使用Servlet容器
来管理会话的话,则可以在web.xml
中配置会话超时时间
<session-config>
<!-- 配置会话超时时间为30分钟 -->
<session-timeout>30</session-timeout>
</session-config>
原生会话
如果想要会话的配置能够跨Servlet容器
生效的话,应该使用Shiro的原生会话管理。“原生”意味着使用Shiro自己的企业会话管理,这样可以完全地支持Subject
和HttpServletRequest
会话,完全绕过Servlet容器
。请放心,Shiro直接实现了Servlet规范的相关部分,因此任何现有的Web/HTTP
相关代码都可以按预期工作,对于它们来说,Shiro是透明的
DefaultWebSessionManager
在web应用中开启Shiro的原生会话管理,只需要为SecurityManager
配置一个DefaultWebSessionManager
实例即可,如下
[main]
...
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
# 如果需要的话,在这里配置属性
# 使用配置好的原生会话管理器
securityManager.sessionManager = $sessionManager
可以为DefaultWebSessionManager
实例配置所有原生会话的配置选项,如超时时间和集群配置等
原生会话的超时时间
在配置了DefaultWebSessionManager
实例后,可以像前面介绍会话管理那样设置会话超时时间
会话Cookie
DefaultWebSessionManager
有两个Web相关的配置属性:
-
sessionIdCookieEnabled
——一个布尔属性 -
sessionIdCookie
,一个Cookie
实例
sessionIdCookie
属性是一个Cookie
实例,它作为一个模板,这个模板会被用来在运行时使用适当的会话ID值设置实际的HTTP ‘Cookie
'头。
会话Cookie配置
DefaultWebSessionManager
的sessionIdCookie
的默认是一个SimpleCookie
实例,它是一个JavaBean
的形式来设置相关属性值
[main]
...
securityManager.sessionManager.sessionIdCookie.domain = foo.com
查看SimpleCookie
的Java文档查看更多信息
根据Servlet规范,cookie
的默认名是JSESSIONID
,Shiro还支持HttpOnly和SameSite
标识,为了安全性,sessionIdCookie
默认将会设置HttpOnly
为true
,设置SameSite
为LAX
关闭会话的Cookie
如果不想使用会话Cookie
,可以通过sessionIdCookieEnabled
属性值,将其设置为false
[main]
...
securityManager.sessionManager.sessionidCookieEnabled = false
“记住我”功能
如果AuthenticationToken
实现了org.apache.shiro.authc.RememberMeAuthenticationToken
接口,那Shiro将会根据接口的方法来判断是否提供”记住我“服务
boolean isRememberMe();
如果这个方法返回true
,那Shiro将会跨会话的记住终端用户的标识
UsernamePasswordToken
和 记住我最常用的
UsernamePasswordToken
已经实现了RememberMeAuthenticationToken
接口,支持”记住我“服务
编程式配置
可以通过将登陆时使用的RememberMeAuthenticationToken
或其子类的rememberMe
属性值设置为true
来开启”记住我“服务,如下
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
SecurityUtils.getSubject().login(token);
...
基于表单的登录
在Web应用程序中,authc
过滤器默认是一个FormAuthenticationFilter
,它能够读取请求参数中的rememberMe
参数,默认情况下参数名就是rememberMe
[main]
authc.loginUrl = /login.jsp
[urls]
# 登陆页面
login.jsp = authc
# 这样设置真的不会陷入死循环吗?
在你的Web表单中,添加一个名为rememberMe
的checkbox
<form ...>
Username: <input type="text" name="username"/> <br/>
Password: <input type="password" name="password" />
...
<input type="checkbox" name="rememberMe" value="true"/> Remember Me?
...
</form>
默认情况下,FormAuthenticationFilter
将会寻找名为username
,password
和rememberMe
的请求参数,可以配置FormAuthenticationFilter
来自定义参数名字
[main]
...
authc.loginUrl=/whatever.jsp
authc.usernameParam=somethingOtherThanUsername
authc.passwordParam=somethingOtherThanPassword
authc.rememberMeParam=somethingOtherThanRememberMe
...
Cookie配置
还可以设置rememberMe
的cookie如何
生效
[main]
...
securityManager.rememberMeManager.cookie.name = foo
securityManager.rememberMeManager.cookie.maxAge = blah
...
详情可以看CookieRememberMemanager
和SimpleCookie
的JavaDoc
自定义RememberMeManager
如果默认的RememberMeManager
实现不能满足你的需求,也可以自定义
[main]
...
rememberMeManager = com.my.impl.RememberMeManager
securityManager.rememberMeManager = $rememberMeManager
...
JSP/GSP
标签库
Apache Shiro提供了一个Subject
可以感知的JSP/GSP
标记库,允许根据当前Subject
状态控制JSP,JSTL或GSP
页面输出。 对于基于当前用户查看网页的特定用户的身份和授权状态,这对于个性化视图非常有用
标签库配置
JSP/GSP
的标签库描述文件(TLD)是在shiro-web.jar
中的META-INF/shiro.tld
的,在JSP
页面中添加下面这一行来使用标签
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
我们将会使用shiro
前缀最为Shiro标签库的命名空间,当然可以使用自己喜欢的前缀
下面来介绍一些标签
guest
guest
包裹的内容,只会展示给那些未登录的用户
<shiro:guest>
这里是未登录的游客才能看到的东西
</shiro:guest>
guest
标签的对立面即为user
标签
user
与guest
相反,user
标签包裹的内容只会展示给那些登录的用户,其中包括在之前的会话中被记住的用户,注意这个标签和authenticated
标签是不一样的,后者更为严格
<shiro:user>
这里是展示给登录的用户的
</shiro:user>
该标签和guest
标签相反
authenticated
只有当前Subject
是已认证的,才会展示该标签包裹的内容。注意它和user
标签的区别,具体参考之前描述的已认证和已记住两个状态
<shiro:authenticated>
这里是已认证的用户才能看到的
</shiro:authenticated>
该标签和notAuthenticated
是相反的
notAuthenticated
该标签包裹的内容只会展示给那些未认证的Subject
<shiro:notAuthenticated>
这里是未认证的用户才能看到的
</shiro:notAuthenticated>
该标签和authenticated
相反
principal
principal
标签将会输出Subject
的principal
信息(即用户的身份信息)或者其属性。如果没有指定输出哪个标签属性,该标签将会输出principal
的toString()
方法,如
<shiro:principal/>
和下面的输出结果是一样的
<%= SecurityUtils.getSubject().getPrincipal().toString() %>
根据类型选择principal
principal
标签默认是输出subject.getPrincipal()
值,如果想要输出的不是primary principal
,而是Subject
的其他principal
,可以通过principal
的类型来选择输出
<principal type="java.lang.Integer" />
上面的标签和下面的输出是一样的
<%= SecurityUtils.getSubject().getPrincipals().oneByType(Integer.class).toString() %>
Principal的属性
如果principal
是一个复杂类型的数据,而不是一个简单的字符串,我们还可以通过property
属性来指定输出principal
的哪个属性值
<shiro:principal property="firstName" />
下面是它的等效结果
<%= SecurityUtils.getSubject().getPrincipal().getFirstName().toString() %>
还可以结合Principal
的类型进行输出
<shiro:principal type="com.foo.User" property="firstname"/>
它和下面的输出是一样的
<%= SecurityUtils.getSubject().getPrincipals().oneByType(com.foo.User.class).getFirstName().toString() %>
hasRole
该标签包裹的内容只会展示给具有特定角色的用户
<shiro:hasRole name="administrator">
只有拥有administrator角色的用户才能看到
</shiro:hasRole>
该标签和lacksRole
标签相反
lacksRole
该标签包裹的内容只会展示给不具有特定角色的用户
<shiro:lacksRole name="administrator">
只有没有administrator角色的用户才能看到
</shiro:lacksRole>
该标签与hasRole
标签相反
hasAnyRole
该标签包裹的内容,只有那些具有指定角色集合的其中一个角色才能看到
<shiro:hasAnyRoles name="developer, project manager, administrator" >
需要具有developer,project manager,administrator至少一个角色的用户的才能看到
</shiro:hasAnyRoles>
该标签没有逻辑上相反的标签
hasPermission
参考hasRole
<shiro:hasPermission name="user:create">
内容
</shiro:hasPermission>
lacksPermission
参考lacksRole
<shiro:lacksPermission name="user:delete">
内容
</shiro:lacksPermission>