Shiro和Spring的集成,涉及到很多相关的配置,涉及到shiro的filer机制以及它拥有的各种默认filter,涉及到shiro的权限判断标签,权限注解,涉及到session管理等等方面。
1. 配置
首先需要在web.xml中专门负责接入shiro的filter:
<!-- shiro 安全过滤器 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <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>
并且需要放在所有filter中靠前的位置,比如需要放在siteMesh的过滤器之前。
DelegatingFilterProxy 表示这是一个代理filter,它会将实际的工作,交给spring配置文件中 id="shiroFilter" 的bean来处理:
public class DelegatingFilterProxy extends GenericFilterBean { private String contextAttribute; private WebApplicationContext webApplicationContext; private String targetBeanName; private boolean targetFilterLifecycle = false; private volatile Filter delegate; private final Object delegateMonitor = new Object(); @Override protected void initFilterBean() throws ServletException { synchronized (this.delegateMonitor) { if (this.delegate == null) { // If no target bean name specified, use filter name. if (this.targetBeanName == null) { this.targetBeanName = getFilterName(); } // Fetch Spring root application context and initialize the delegate early, // if possible. If the root application context will be started after this // filter proxy, we‘ll have to resort to lazy initialization. WebApplicationContext wac = findWebApplicationContext(); if (wac != null) { this.delegate = initDelegate(wac); } } } }
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean { @Override public final void init(FilterConfig filterConfig) throws ServletException { Assert.notNull(filterConfig, "FilterConfig must not be null"); if (logger.isDebugEnabled()) { logger.debug("Initializing filter ‘" + filterConfig.getFilterName() + "‘"); } this.filterConfig = filterConfig; // Set bean properties from init parameters. try { PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment)); initBeanWrapper(bw); bw.setPropertyValues(pvs, true); } catch (BeansException ex) { String msg = "Failed to set bean properties on filter ‘" + filterConfig.getFilterName() + "‘: " + ex.getMessage(); logger.error(msg, ex); throw new NestedServletException(msg, ex); } // Let subclasses do whatever initialization they like. initFilterBean(); if (logger.isDebugEnabled()) { logger.debug("Filter ‘" + filterConfig.getFilterName() + "‘ configured successfully"); } }
// Let subclasses do whatever initialization they like.
initFilterBean();
Filter 接口的 init 方法调用 initFilterBean(), 而该方法在子类中进行实现,它先获得 this.targetBeanName = getFilterName(); bean的名称,也就是id,然后对其进行初始化:this.delegate = initDelegate(wac); 其实就是从bean工厂中根据bean的名称找到bean.
protected Filter initDelegate(WebApplicationContext wac) throws ServletException { Filter delegate = wac.getBean(getTargetBeanName(), Filter.class); if (isTargetFilterLifecycle()) { delegate.init(getFilterConfig()); } return delegate; }
而 shiroFilter在spring中的配置如下:
<!-- Shiro的Web过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/"/> <property name="unauthorizedUrl" value="/unauthorized"/> <property name="filters"> <util:map> <entry key="authc" value-ref="passThruAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /reg/** = anon <!-- 注册相关 --> /login = authc /logout = logout /authenticated = authc /loginController = anon /js/** = anon /css/** = anon /img/** = anon /html/** = anon /font-awesome/** = anon <!-- /** = anon /user/modifyPassword = perms["user:update", "user:select"] --> /** = user </value> </property> </bean>
上面的shiroFilter的配置又引出了 securityManager 和 shiro 的filter机制和他自带的一些filter.
2. securityManager 级相关配置
在上一篇文章 Java 权限框架 Shiro 实战一:理论基础 中我们知道securityManager是shiro的顶层对象,它管理和调用其它所有子系统,负责系统的安全。我们知道shiro有两个类型的securityManager:一个是JavaSE环境,默认是DefaultSecurityManager;一个是web环境,默认是DefaultWebSecurityManager。所以我们web环境肯定应该使用后者。我们从顶层对象一层一层向下配置。先看securityManager如何配置:
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) --> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/> <property name="arguments" ref="securityManager"/> </bean>
上面的配置相当于调用SecurityUtils.setSecurityManager(securityManager) ,来注入了下面配置的 securityManager(DefaultWebSecurityManager) :
<!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm"/> <property name="cacheManager" ref="cacheManager"/> <property name="rememberMeManager" ref="rememberMeManager"/> </bean>
它默认使用的session管理器是 ServletContainerSessionManager,所以上面没有配置,所以就使用默认值。配置了就会覆盖下面的默认值:
public DefaultWebSecurityManager() { super(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator()); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); }
显然 securityManager 最重要的工作就是用户登录认证和获得用户的权限等相关信息,所以 realm 是其最重要的依赖:
<!-- Realm实现 --> <bean id="userRealm" class="com.ems.shiro.UserRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"/> <property name="cachingEnabled" value="false"/> </bean>
要理解上面userRealm的配置,就的先理解 UserRealm 的继承体系:
UserRealm 继承 AuthorizingRealm 显然是为了获取权限信息,对用户进行访问控制;继承AuthenticatingRealm显然是为了获得用户的认证信息,对用户进行认证。而 credentialsMatcher 就是 AuthenticatingRealm 使用来进行密码验证的依赖的组件:
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {/** * Credentials matcher used to determine if the provided credentials match the credentials stored in the data store. */ private CredentialsMatcher credentialsMatcher;
再看其credentialsMatcher bean的配置:
<!-- 凭证匹配器(验证登录密码是否正确) --> <bean id="credentialsMatcher" class="com.ems.shiro.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager"/> <property name="hashAlgorithmName" value="SHA-256"/> <property name="hashIterations" value="2"/> <property name="storedCredentialsHexEncoded" value="true"/> </bean>
配置就是 hash加密的相关参数:hash算法,hash迭代次数等。到这里 shiro 登录验证的配置就完了。至于获取用户信息和用户的权限的信息,都在userRealm中实现了:
public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String userName = (String)principals.getPrimaryPrincipal(); User user = userService.getUserByUserName (userName ); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(userService.findRolesByUserId(user.getId())); authorizationInfo.setStringPermissions(userService.findPermissionsByUserId(user.getId())); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String userName = (String)token.getPrincipal(); User user = userService.getUserByUserName(userName); if(user == null) { throw new UnknownAccountException();//没找到账户 } if(user.getLocked() == 0) { throw new LockedAccountException(); //帐号锁定 } if(user.getLocked() == 2){ throw new AuthenticationException("account was inactive"); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user.getUserName(), user.getPassword(), // 密码 ByteSource.Util.bytes(user.getCredentialsSalt()), // salt=no+salt getName() // realm name ); return authenticationInfo; }
securityManager会在需要的时候回调上面 的 doGetAuthorizationInfo 和 doGetAuthenticationInfo 方法,从realm中获得登录认证信息和用户权限信息。至于 rememberMeManager 主要是实现使用cookie表示我已经登录过了,下次不需要重新登录,这一个功能,也就是“记住我”登录过这一功能:
<!-- rememberMe管理器 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <!-- rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)--> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode(‘9FvVhtFLUs0KnA3Kprsdyg==‘)}"/> <property name="cookie" ref="rememberMeCookie"/> </bean> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="2592000"/><!-- 30天 --> </bean>
还有cacheManager的配置:
<!--ehcache--> <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache/ehcache.xml"/> </bean> <bean id="springCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManager" ref="ehcacheManager"/> </bean> <!-- 缓存管理器 --> <bean id="cacheManager" class="com.ems.shiro.SpringCacheManagerWrapper"> <property name="cacheManager" ref="springCacheManager"/> </bean>
使用的是 EhCache.
3. Shiro 的filter机制和自带的filter
Shiro的filter是基于Servlet的Filter接口实现的。我们通过Shiro提供的form登录filter:FormAuthenticationFilter 和 ShiroFilter 看看其实现:
继承中的每一层都实现了一些功能:
1> NameableFilter:实现给filter取名的功能(Allows a filter to be named via JavaBeans-compatible)
/** * Allows a filter to be named via JavaBeans-compatible*/ public abstract class NameableFilter extends AbstractFilter implements Nameable { /** * The name of this filter, unique within an application. */ private String name;
2> OncePerRequestFilter : 保证对于同一个request,fiter只执行一次(Filter base class that guarantees to be just executed once per request)
/** * Filter base class that guarantees to be just executed once per request, * on any servlet container. It provides a {@link #doFilterInternal} * method with HttpServletRequest and HttpServletResponse arguments.*/ public abstract class OncePerRequestFilter extends NameableFilter {
3> AdviceFilter: SpringMVC风格的过滤器(就是preHandle, postHandle,afterCompletion 三接口的过滤器)
/** * A Servlet Filter that enables AOP-style "around" advice for a ServletRequest via * preHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse), * postHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse), * and afterCompletion(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Exception)hooks. */public abstract class AdviceFilter extends OncePerRequestFilter { protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { return true; } @SuppressWarnings({"UnusedDeclaration"}) protected void postHandle(ServletRequest request, ServletResponse response) throws Exception { } @SuppressWarnings({"UnusedDeclaration"}) public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception { }
4> PathMatchingFilter:该过滤器仅仅处理指定的路径(比如上面的配置:/js/** = anon,表示对 /js/ 目录和其子目录的请求,交给anon过滤器处理)
/** * <p>Base class for Filters that will process only specified paths and allow all others to pass through.</p>*/ public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {
5> AccessControlFilter: 实现提供对资源的访问控制,没有权限时,重定向到登录页面,登录之后跳转到原来的那个页面
/** * Superclass for any filter that controls access to a resource and may redirect the user to the login page * if they are not authenticated. This superclass provides the method * saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse) * which is used by many subclasses as the behavior when a user is unauthenticated.*/ public abstract class AccessControlFilter extends PathMatchingFilter {
6> AuthenticationFilter: 实现对访问用户的认证要求,也就是必须登录了才能访问
/** * Base class for all Filters that require the current user to be authenticated. This class encapsulates the * logic of checking whether a user is already authenticated in the system while subclasses are required to perform * specific logic for unauthenticated requests.*/ public abstract class AuthenticationFilter extends AccessControlFilter {
7> AuthenticatingFilter: 实现判断用户是否有权限访问某资源。
/** * An AuthenticationFilter that is capable of automatically performing an authentication attempt * based on the incoming request.*/ public abstract class AuthenticatingFilter extends AuthenticationFilter {
8> FormAuthenticationFilter:shiro提供的用于实现用户登录功能,如果我们打算自己实现登录,那么我们应用 PassThruAuthenticationFilter 来替代
/** * Requires the requesting user to be authenticated for the request to continue, and if they are not, forces the user * to login via by redirecting them to the setLoginUrl(String) you configure. * If you would prefer to handle the authentication validation and login in your own code, consider using the * PassThruAuthenticationFilter instead, which allows requests to the loginUrl to pass through to your application‘s code directly.*/ public class FormAuthenticationFilter extends AuthenticatingFilter {
9> PassThruAuthenticationFilter : 用于我们自己在controller中实现登录逻辑时替代FormAuthenticationFilter
/** * An authentication filter that redirects the user to the login page when they are trying to access * a protected resource. However, if the user is trying to access the login page, the filter lets * the request pass through to the application code. * The difference between this filter and the FormAuthenticationFilter is that * on a login submission (by default an HTTP POST to the login URL), the FormAuthenticationFilter filter * attempts to automatically authenticate the user by passing the username and password request parameter values to * Subject.login(AuthenticationToken) directly. * Conversely, this controller always passes all requests to the loginUrl through, both GETs and POSTs. * This is useful in cases where the developer wants to write their own login behavior, which should include a * call to Subject.login(AuthenticationToken) at some point. For example, if the developer has their own custom MVC * login controller or validator, this PassThruAuthenticationFilter may be appropriate.*/ public class PassThruAuthenticationFilter extends AuthenticationFilter { protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { return true; } else { saveRequestAndRedirectToLogin(request, response); return false; } } }
10> Shiro 自带的filter:
Shiro自身提供了很多的默认filter 来供我们使用,主要分为两种:一是 登录认证相关的filter;一是权限访问控制相关的filter;
登录认证相关的filter有:
1)filter名称: anon, 实现类org.apache.shiro.web.filter.authc.AnonymousFilter,主要用于静态资源的访问,表示无需登录就可以访问;
2)filter名称: authc, 实现类org.apache.shiro.web.filter.authc.FormAuthenticationFilter,主要用于表单登录,没有登录则跳转登录url;
3)filter名称: user, 实现类org.apache.shiro.web.filter.authc.UserFilter,主要用于要求用户已经登录或者通过“记住我”功能登录了也行。
4)filter名称: logout, 实现类org.apache.shiro.web.filter.authc.LogoutFilter,主要用于用户登出
5)filter名称: authcBasic,authc的简化形式,略。
权限访问控制相关的filter有:
1)filter名称: roles, 实现类org.apache.shiro.web.filter.authc.RolesAuthorizationFilter,主要用于验证用户必须拥有某角色,才能继续访问;
2)filter名称: perms, 实现类org.apache.shiro.web.filter.authc.PermissionsAuthorizationFilter,主要用于验证用户必须拥有某权限,才能继续访问;
3)filter名称: ssl, 实现类org.apache.shiro.web.filter.authc.SslFilter,主要用于要求访问协议是https才能访问,不然跳转到https的443短裤;
4)filter名称: port rest noSessionCreation,略。
我们上面的shiroFilter的配置中,已经使用过了上面这些自带的filter:
/reg/** = anon <!-- 注册相关 --> /login = authc /logout = logout /authenticated = authc /loginController = anon /js/** = anon /css/** = anon /img/** = anon /html/** = anon /font-awesome/** = anon /** = user
我们看到 /reg/** 注册相关的,/js/**静态资源都是使用的 anon匿名过滤器,不要求用户已经登录就可以访问。
/** = user 放在最后是要求除了上面那些 url 之外的访问路径,都需要登录认证过或者通过记住我登录认证过。因为路径比较是从上面开始列出来的先开始比较的,匹配了就走该过滤器,不会继续下面的过滤器了。
4. shiro的权限标签
对双方都