将Shiro集成到Web应用中,只需要在web.xml中配置ContextListener和Filter.
对于Shiro来说,集成到Web应用中,需要解决以下问题:
- SecurityManager 等Shiro用到的组件与ServletContext的绑定。
- 将请求的URI与权限对应。
为了方便,我们首先使用基于*.ini配置文件的方式来进行集成。
web.xml
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
<init-param>
<param-name>configPath</param-name>
<param-value>classpath:shiro.ini</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>hello.shiro.demo3.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>UserServlet</servlet-name>
<servlet-class>hello.shiro.demo3.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UserServlet</servlet-name>
<url-pattern>/user</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>AdminServlet</servlet-name>
<servlet-class>hello.shiro.demo3.AdminServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AdminServlet</servlet-name>
<url-pattern>/admin</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>AdminCreateServlet</servlet-name>
<servlet-class>hello.shiro.demo3.AdminCreateServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AdminCreateServlet</servlet-name>
<url-pattern>/admin/create</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
shiro.ini
[main]
#定义身份认证失败后的请求url映射
authc.loginUrl=/login
#定义用户和角色
[users]
user=123,user
admin=123,admin
#定义不同角色的权限
[roles]
user=user:*
admin=admin:*
[urls]
#/login,不需要权限(anon)
/login=anon
#/user,需要身份认证(authc)
/user=authc
#/admin,需要角色认证(admin)
/admin=roles[admin]
#/admin/create,需要权限认证(admin:create)
/admin/create=perms[admin:create]
LoginServlet
public class LoginServlet extends HttpServlet{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("login");
String username = request.getParameter("username");
String password = request.getParameter("password");
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
currentUser.login(token);
System.out.println("认证成功");
request.getSession().setAttribute("username", username);
if(currentUser.hasRole("user")){
response.sendRedirect("/user");
}else if(currentUser.hasRole("admin")){
response.sendRedirect("/admin");
}
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("认证失败");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
}
UserServlet
public class UserServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("message", "欢迎您:"+request.getSession().getAttribute("username"));
request.getRequestDispatcher("/success.jsp").forward(request, response);
}
}
AdminServlet
public class AdminServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("message", "欢迎您,管理员:"+request.getSession().getAttribute("username"));
request.getRequestDispatcher("/success.jsp").forward(request, response);
}
}
AdminCreateServlet
public class AdminCreateServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("message", "创建管理员");
request.getRequestDispatcher("/success.jsp").forward(request, response);
}
}
login.jsp
<body>
<form action="${request.contextPath }/login" method="post">
username:<input type="text" name="username"/><br>
password:<input type="password" name="password"/><br>
<input type="submit" value="登陆">
</form>
</body>
success.jsp
<body>
${message }
</body>
接下来运行并访问 /user,/admin, /admin/create ,发现跳转到 /login。
使用user账户登陆后,访问/user正常显示, 访问 /admin, /admin/create报401错误,没有权限访问
使用admin账户登陆后,所有请求都可以正常访问
EnvironmentLoaderListener
用于初始化Shiro,用于创建并初始化 MutableWebEnvironment
ContextListener的容器初始化
public void contextInitialized(ServletContextEvent sce) {
initEnvironment(sce.getServletContext());
}
public WebEnvironment initEnvironment(ServletContext servletContext) throws IllegalStateException {
//......
long startTime = System.currentTimeMillis();
try {
创建一个 WebEnvironment ,并绑定到ServletContext中
WebEnvironment environment = createEnvironment(servletContext);
servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, environment);
return environment;
}
//......
}
protected WebEnvironment createEnvironment(ServletContext sc) {
//获取在web.xml中通过 shiroEnvironmentClass 参数指定的 WebEnviroment的实现类,
//如果没有指定,默认使用 基于 *.ini配置文件的 IniWebEnvironment
Class<?> clazz = determineWebEnvironmentClass(sc);
//实现类必须实现了 MutableWebEnvironment 接口,否则抛出异常
if (!MutableWebEnvironment.class.isAssignableFrom(clazz)) {
throw new ConfigurationException("Custom WebEnvironment class [" + clazz.getName() +
"] is not of required type [" + MutableWebEnvironment.class.getName() + "]");
}
//通过 shiroConfigLocations 参数在web.xml配置的配置文件路径
String configLocations = sc.getInitParameter(CONFIG_LOCATIONS_PARAM);
//配置文件路径为空,即没有指定配置文件
boolean configSpecified = StringUtils.hasText(configLocations);
//如果指定的 WebEnvironment类型实现了ResourceConfigurable 接口,则必须指定配置文件路径
if (configSpecified && !(ResourceConfigurable.class.isAssignableFrom(clazz))) {
String msg = "WebEnvironment class [" + clazz.getName() + "] does not implement the " +
ResourceConfigurable.class.getName() + "interface. This is required to accept any " +
"configured " + CONFIG_LOCATIONS_PARAM + "value(s).";
throw new ConfigurationException(msg);
}
//创建具体的 “环境” 实例
MutableWebEnvironment environment = (MutableWebEnvironment) ClassUtils.newInstance(clazz);
environment.setServletContext(sc);
if (configSpecified && (environment instanceof ResourceConfigurable)) {
((ResourceConfigurable) environment).setConfigLocations(configLocations);
}
customizeEnvironment(environment);
//初始化 “环境”
LifecycleUtils.init(environment);
return environment;
}
MutableWebEnvironment
Shiro的运行环境,在Web应用启动时,EnvironmentLoaderListener会创建并初始化 EnvironmentLoaderListener
在上面的案例中我们使用的是基于*.ini配置文件的方式, 因此会使用默认的 IniWebEnvironment 类
我们看看IniWebEnvironment的初始化方法:
public void init() {
//...... 省略,上面一大段都是根据配置文件创建 Ini 实例的
configure();
}
protected void configure() {
//清空已经存储的对象,
this.objects.clear();
//通过WebIniSecurityManagerFactory创建一个 DefaultWebSecurityManager
WebSecurityManager securityManager = createWebSecurityManager();
setWebSecurityManager(securityManager);
//通过IniFilterChainResolverFactory创建一个PathMatchingFilterChainResolver
//在此过程中会创建 并初始化 FilterChainManager
FilterChainResolver resolver = createFilterChainResolver();
if (resolver != null) {
setFilterChainResolver(resolver);
}
}
接下来查看 IniFilterChainResolverFactory是如何创建 FilterChainManager的
protected FilterChainResolver createInstance(Ini ini) {
//创建 FilterChainResolver 与 FilterChainManager
FilterChainResolver filterChainResolver = createDefaultInstance();
if (filterChainResolver instanceof PathMatchingFilterChainResolver) {
PathMatchingFilterChainResolver resolver = (PathMatchingFilterChainResolver) filterChainResolver;
FilterChainManager manager = resolver.getFilterChainManager();
//构建 FilterChainManager
buildChains(manager, ini);
}
return filterChainResolver;
}
protected FilterChainResolver createDefaultInstance() {
FilterConfig filterConfig = getFilterConfig();
//创建 PathMatchingFilterChainResolver 时会创建 DefaultFilterChainManager
if (filterConfig != null) {
return new PathMatchingFilterChainResolver(filterConfig);
} else {
return new PathMatchingFilterChainResolver();
}
}
//
protected void buildChains(FilterChainManager manager, Ini ini) {
//配置文件中的配置
Ini.Section section = ini.getSection(FILTERS);
Map<String, Object> defaults = new LinkedHashMap<String, Object>();
//开始合并各处的Filter
//FilterChainManager 已经有的默认的Filter , 具体可查看 DefaultFilter
Map<String, Filter> defaultFilters = manager.getFilters();
if (!CollectionUtils.isEmpty(defaultFilters)) {
defaults.putAll(defaultFilters);
}
//创建工程时指定的一些对象
if (!CollectionUtils.isEmpty(this.defaultBeans)) {
defaults.putAll(this.defaultBeans);
}
//将defaults中的非Filter对象去除,并添加 ini中配置的Filter
Map<String, Filter> filters = getFilters(section, defaults);
registerFilters(filters, manager);
//生成各url对应的过滤器链
section = ini.getSection(URLS);
createChains(section, manager);
}
//将Filter绑定到 FilterChainManager
protected void registerFilters(Map<String, Filter> filters, FilterChainManager manager) {
if (!CollectionUtils.isEmpty(filters)) {
boolean init = getFilterConfig() != null;
for (Map.Entry<String, Filter> entry : filters.entrySet()) {
String name = entry.getKey();
Filter filter = entry.getValue();
manager.addFilter(name, filter, init);
}
}
}
//根据url生成对应的NamedFilterList
protected void createChains(Map<String, String> urls, FilterChainManager manager) {
if (CollectionUtils.isEmpty(urls)) {
if (log.isDebugEnabled()) {
log.debug("No urls to process.");
}
return;
}
if (log.isTraceEnabled()) {
log.trace("Before url processing.");
}
for (Map.Entry<String, String> entry : urls.entrySet()) {
String path = entry.getKey();
String value = entry.getValue();
manager.createChain(path, value);
}
}
ShiroFilter
ShiroFilter是Shiro对请求进行拦截的入口,在这个过滤器中,
//此方法用于权限验证
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
//根据请求 从 FilterChainResolver 中获取对应的 FilterChain。
//如果获取不到,返回原来的 origChain
FilterChain chain = getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}
//根据请求 从 FilterChainResolver 中获取对应的 FilterChain。如果获取不到,返回原来的 origChain
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
return origChain;
}
FilterChain resolved = resolver.getChain(request, response, origChain);
if (resolved != null) {
chain = resolved;
}
return chain;
}
FilterChainResolver
过滤器链解析器,用于根据请求解析出对应的过滤器链。
它只有一个实现类:PathMatchingFilterChainResolver,用于根据URI从FilterChainManager中获取对应的过滤器链
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
String requestURI = getPathWithinApplication(request);
//the 'chain names' in this implementation are actually path patterns defined by the user. We just use them
//as the chain name for the FilterChainManager's requirements
for (String pathPattern : filterChainManager.getChainNames()) {
// If the path does match, then pass on to the subclass implementation for specific checks:
if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " +
"Utilizing corresponding filter chain...");
}
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
FilterChainManager
过滤器链管理器,用于管理请求对应的过滤器链
它主要有两个方法:
//获取请求对应的过滤器集合
public NamedFilterList getChain(String chainName) {
return this.filterChains.get(chainName);
}
//对过滤器链进行封装,返回Shiro自定义的过滤器链 ProxiedFilterChain
public FilterChain proxy(FilterChain original, String chainName) {
NamedFilterList configured = getChain(chainName);
if (configured == null) {
String msg = "There is no configured chain under the name/key [" + chainName + "].";
throw new IllegalArgumentException(msg);
}
return configured.proxy(original);
}
ProxiedFilterChain
Shiro封装的过滤器链,在请求到达时, ShiroFilter会将Servlet容器的FilterChain封装称 ProxiedFilterChain
查看ProxiedFilterChain源码可以看到 请求将 依次通过Shiro提供的各种Filter,然后再交给ShiroFilter后面的Filter进行处理
public class ProxiedFilterChain implements FilterChain {
private FilterChain orig; //ShiroFilter中原来的过滤器链
private List<Filter> filters; //Shiro根据请求从 FilterChainManager 中获取的过滤器集合
private int index = 0;
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.filters == null || this.filters.size() == this.index) {
//we've reached the end of the wrapped chain, so invoke the original one:
if (log.isTraceEnabled()) {
log.trace("Invoking original filter chain.");
}
this.orig.doFilter(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Invoking wrapped filter at index [" + this.index + "]");
}
this.filters.get(this.index++).doFilter(request, response, this);
}
}
}
最后列出Shiro内置过滤器:
anon(AnonymousFilter.class)
匿名认证,永远认证通过
authc(FormAuthenticationFilter.class)
身份认证,需要登陆
authcBasic(BasicHttpAuthenticationFilter.class)
Basic HTTP身份验证拦截器
logout(LogoutFilter.class)
退出拦截器,退出成功后重定向到redirectUrl
noSessionCreation(NoSessionCreationFilter.class)
禁止创建Session.
如果一个Subject在这个过滤器被调用时还没有Session,调用Subject.getsession()和Subject.getsession(true)将抛出一个异常。
但是,如果这个主题在调用这个过滤器之前已经有了一个关联的Session,那么这个过滤器就没有效果。
perms(PermissionsAuthorizationFilter.class)
权限授权拦截器,验证用户是否拥有所有权限
port(PortFilter.class)
端口拦截器,可以指定请求通过的端口;
如果用户访问该页面不是通过 port属性指定的端口,将自动将请求端口改为指定端口,并重定向到指定端口
rest(HttpMethodPermissionFilter.class)
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”权限字符串进行权限匹配(所有都得匹配,isPermittedAll)
roles(RolesAuthorizationFilter.class)
角色认证
如果拥有指定角色,则通过认证。否则拒绝访问
ssl(SslFilter.class)
SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样
user(UserFilter.class)
允许已知的用户访问资源,包括已经登陆的或者通过“记住我”记住的用户
如果没有,则跳转到loginUrl