digdeep

凡是过去,皆是序幕。Read the fucking manual and source code.

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

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
                getName()  // realm name
        );        
        return authenticationInfo;
    }

securityManager会在需要的时候回调上面 的 doGetAuthorizationInfodoGetAuthenticationInfo 方法,从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 &quot;around&quot; 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的权限标签

Shiro提供了相应的权限标签,用来实现根据用户的角色和权限来显示它相应的菜单和按钮。首先需要导入shiro标签库:

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

标签库的定义位于:shiro-web.jar 包中的META-INF/shiro.tld文件中:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!--
  ~ Licensed to the Apache Software Foundation (ASF) under one
  ~ or more contributor license agreements.  See the NOTICE file
  ~ distributed with this work for additional information
  ~ regarding copyright ownership.  The ASF licenses this file
  ~ to you under the Apache License, Version 2.0 (the
  ~ "License"); you may not use this file except in compliance
  ~ with the License.  You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing,
  ~ software distributed under the License is distributed on an
  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  ~ KIND, either express or implied.  See the License for the
  ~ specific language governing permissions and limitations
  ~ under the License.
  -->
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
  "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">
<taglib>
  <tlib-version>1.1.2</tlib-version>
  <jsp-version>1.2</jsp-version>
  <short-name>Apache Shiro</short-name>
  <uri>http://shiro.apache.org/tags</uri>
  <description>Apache Shiro JSP Tag Library.</description>
  <tag>
    <name>hasPermission</name>
    <tag-class>org.apache.shiro.web.tags.HasPermissionTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current Subject (user)
      'has' (implies) the specified permission (i.e the user has the specified ability).
    </description>
    <attribute>
      <name>name</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>
  <tag>
    <name>lacksPermission</name>
    <tag-class>org.apache.shiro.web.tags.LacksPermissionTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current Subject (user) does
      NOT have (not imply) the specified permission (i.e. the user lacks the specified ability)
    </description>
    <attribute>
      <name>name</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>
  <tag>
    <name>hasRole</name>
    <tag-class>org.apache.shiro.web.tags.HasRoleTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current user has the specified role.</description>
    <attribute>
      <name>name</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>
  <tag>
    <name>hasAnyRoles</name>
    <tag-class>org.apache.shiro.web.tags.HasAnyRolesTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current user has one of the specified roles from a
      comma-separated list of role names.
    </description>
    <attribute>
      <name>name</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>
  <tag>
    <name>lacksRole</name>
    <tag-class>org.apache.shiro.web.tags.LacksRoleTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current user does NOT have the specified role
      (i.e. they explicitly lack the specified role)
    </description>
    <attribute>
      <name>name</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>
  <tag>
    <name>authenticated</name>
    <tag-class>org.apache.shiro.web.tags.AuthenticatedTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current user has successfully authenticated
      _during their current session_. It is more restrictive than the 'user' tag.
      It is logically opposite to the 'notAuthenticated' tag.
    </description>
  </tag>
  <tag>
    <name>notAuthenticated</name>
    <tag-class>org.apache.shiro.web.tags.NotAuthenticatedTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current user has NOT succesfully authenticated
      _during their current session_. It is logically opposite to the 'authenticated' tag.
    </description>
  </tag>
  <tag>
    <name>user</name>
    <tag-class>org.apache.shiro.web.tags.UserTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current Subject has a known identity, either
      from a previous login or from 'RememberMe' services. Note that this is semantically different
      from the 'authenticated' tag, which is more restrictive. It is logically
      opposite to the 'guest' tag.
    </description>
  </tag>
  <tag>
    <name>guest</name>
    <tag-class>org.apache.shiro.web.tags.GuestTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays body content only if the current Subject IS NOT known to the system, either
      because they have not logged in or they have no corresponding 'RememberMe' identity. It is logically
      opposite to the 'user' tag.
    </description>
  </tag>
  <tag>
    <name>principal</name>
    <tag-class>org.apache.shiro.web.tags.PrincipalTag</tag-class>
    <body-content>JSP</body-content>
    <description>Displays the user's principal or a property of the user's principal.</description>
    <attribute>
      <name>type</name>
      <required>false</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
    <attribute>
      <name>property</name>
      <required>false</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
    <attribute>
      <name>defaultValue</name>
      <required>false</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>
</taglib>
shiro.tld

其中最重要的标签是关于角色和权限的:

<shiro:hasAnyRoles name="student,teacher"></shiro:hasAnyRoles>

<shiro:hasPermission name="user:delete"></shiro:hashPermission>

其它还有关于登录与否的标签:

<shiro:guest></shiro:guest> 未登录可以显示的信息;

<shiro:user></shiro:user> 用户已经登录或者通过记住我登录后显示的信息;

<shiro:authenticated></shiro:authenticated> 必须是实际登录,不是通过记住我登录的

其它标签参考 shiro.tld文件。

shiro标签使用示例:

<!--sidebar-menu-->
<div id="sidebar"><a href="javascript:;" class="visible-phone"><i class="icon icon-home"></i> Dashboard</a>
  <ul>
  <shiro:hasAnyRoles name="student,teacher">
      <li id="li_queryScore"><a href="${ctx}/user/queryScore"><i class="icon icon-home"></i><span>查询成绩</span></a></li>
  </shiro:hasAnyRoles>
  <shiro:hasAnyRoles name="teacher,admin">
      <li id="li_showStudentInfo"><a href="${ctx}/student/showStudentInfo"><i class="icon icon-home"></i><span>查询学生信息</span></a></li>
  </shiro:hasAnyRoles>
  <shiro:hasAnyRoles name="admin">
    <li id="li_showTeacherInfo"><a href="${ctx}/teacher/showTeacherInfo"><i class="icon icon-home"></i><span>查询教师信息</span></a></li>
  </shiro:hasAnyRoles> 
  <shiro:hasAnyRoles name="admin">
    <li id="li_getStatistic"><a href="${ctx}/statistics/getStatistic"><i class="icon icon-th"></i><span>统计</span></a></li>
  </shiro:hasAnyRoles>    
  <shiro:hasAnyRoles name="student,teacher,admin">
    <li id="li_password"><a href="${ctx}/user/password"><i class="icon icon-inbox"></i><span>密码修改</span></a></li>
  </shiro:hasAnyRoles>  
  <shiro:hasRole name="admin">
      <li id="li_showPrivilege"><a href="${ctx}/priv/showPrivilege"><i class="icon icon-fullscreen"></i><span>权限设置</span></a></li>
  </shiro:hasRole>
  <shiro:hasAnyRoles name="teacher">
      <li id="li_scoreRatio"><a href="${ctx}/set/scoreRatio"><i class="icon icon-tint"></i><span>成绩比例设置</span></a></li>
  </shiro:hasAnyRoles> 
  <shiro:hasAnyRoles name="admin">
      <li id="li_getSetting"><a href="${ctx}/set/getSetting"><i class="icon icon-tint"></i><span>成绩录入时间设置</span></a></li>
  </shiro:hasAnyRoles> 
  <shiro:hasAnyRoles name="student,teacher">
      <li id="li_queryReExam"><a href="${ctx}/user/queryReExam"><i class="icon icon-pencil"></i><span>补考名单</span></a></li>
    <li id="li_queryReLearn"><a href="${ctx}/user/queryReLearn"><i class="icon icon-pencil"></i><span>重修名单</span></a></li>
  </shiro:hasAnyRoles>  
  </ul>
</div>
<!--sidebar-menu-->
View Code

效果是根据用户拥有的角色,来显示左侧有哪些菜单项。

5. shiro 权限注解的使用

shiro对权限的控制,除了前面给出的在 shiroFilter这个bean中配置的过滤器:

<property name="filterChainDefinitions">
            <value>
                /reg/** = anon    <!-- 注册相关  -->
                /login = authc
                /logout = logout
                /loginController = anon
                /js/** = anon  
                /css/** = anon   
                /img/** = anon  
                /html/** = anon
                /font-awesome/** = anon  
                /** = user
            </value>
        </property>

之外,最重要的就是使用注解的方式来进行访问控制的实现了。shiro权限注解可以达到方法级别的细腻控制,可以控制具有某些权限或者某些角色的用户才能访问某个方法(某个url)。先要开启shiro权限注解功能,开启方法参见文档:http://shiro.apache.org/spring.html

Here is how to enable these annotations. Just add these two bean definitions to applicationContext.xml:

<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- Enable Shiro Annotations for Spring-configured beans.  Only run after -->
<!-- the lifecycleBeanProcessor has run: -->
<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>

开启shiro权限注解的方法二

    <aop:config />
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

<aop:config /> 表示开启spring注解,而 DefaultAdvisorAutoProxyCreator 表示会自动创建代理。但是二者最好不要同时使用。

AuthorizationAttributeSourceAdvisor 通过其依赖的 securityManager 来获取用户的角色和权限信息,进而可以进行权限判断。

支持的shiro注解有:

@SuppressWarnings({"unchecked"})
public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {
    private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);
    private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
            new Class[] {
                    RequiresPermissions.class, RequiresRoles.class,
                    RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
            };
    protected SecurityManager securityManager = null;
    public AuthorizationAttributeSourceAdvisor() {
        setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
    }

RequiresPermissions, RequiresRolesRequiresUser, RequiresGuest, RequiresAuthentication

主要是通过: AopAllianceAnnotationsAuthorizingMethodInterceptor 类来实现的:

public class AopAllianceAnnotationsAuthorizingMethodInterceptor extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor{
    public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
        List<AuthorizingAnnotationMethodInterceptor> interceptors = new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
        //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
        //raw JDK resolution process.
        AnnotationResolver resolver = new SpringAnnotationResolver();
        //we can re-use the same resolver instance - it does not retain state:
        interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
        interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
        interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
        interceptors.add(new UserAnnotationMethodInterceptor(resolver));
        interceptors.add(new GuestAnnotationMethodInterceptor(resolver));

        setMethodInterceptors(interceptors);
    }

上面注入了注解的拦截器实现。具体的拦截判断权限过程实现如下:

public class RoleAnnotationHandler extends AuthorizingAnnotationHandler {
    public RoleAnnotationHandler() {
        super(RequiresRoles.class);
    }
    public void assertAuthorized(Annotation a) throws AuthorizationException {
        if (!(a instanceof RequiresRoles)) return;
        RequiresRoles rrAnnotation = (RequiresRoles) a;
        String[] roles = rrAnnotation.value();
        if (roles.length == 1) {
            getSubject().checkRole(roles[0]);
            return;
        }
        if (Logical.AND.equals(rrAnnotation.logical())) {
            getSubject().checkRoles(Arrays.asList(roles));
            return;
        }
        if (Logical.OR.equals(rrAnnotation.logical())) {
            // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
            boolean hasAtLeastOneRole = false;
            for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true;
            // Cause the exception if none of the role match, note that the exception message will be a bit misleading
            if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]);
        }
    }
}

主要是上面的方法 assertAuthorized(Annotation a) 中来实现对用户是否拥有某些角色进行判断的。其实还是很简单的。

shiro权限注解使用方法如下所示:

    @RequiresPermissions(value={"user:update", "user:select"}, logical= Logical.AND)
    @RequestMapping(value="/modifyPassword", method=RequestMethod.POST)
    @ResponseBody
    public Map<String, String> modifyPassword(String oldPassword, String newPassword, HttpSession session) {
        Map<String, String> map = new HashMap<>();
        if(oldPassword == null || newPassword == null || newPassword.length() < 8 || newPassword.length() > 32){
            map.put("result", "error");
            map.put("msg", "密码必须在8到20位之间");
            return map;
        }        
        User user = (User)session.getAttribute(ConstantConfig.LONGIN_USER);
        if(user != null){
            PasswordHelper ph = new PasswordHelper();
            if(!ph.checkPasswordAndEncryptPassword(oldPassword, user)){    // 判断输入的 oldPassword是否正确
                map.put("result", "error");
                map.put("msg", "密码错误");
                return map;
            }else{
                user.setPassword(newPassword);
                ph.encryptPassword(user);
                int result = this.userService.updateUserById(user);
                if(result > 0){
                    map.put("result", "ok");
                    map.put("msg", "密码修改成功,请重新登录");
                }else{
                    map.put("result", "error");
                    map.put("msg", "密码修改失败");
                }
                return map;
            }
        }
        return map;
    }

@RequiresPermissions(value={"user:update", "user:select"}, logical= Logical.AND)

表示必须有 对 user 表的同时拥有 查询和更新权限,才能修改密码。

posted on 2015-07-04 18:23  digdeep  阅读(10122)  评论(0编辑  收藏  举报
不懂数据库和Web安全的架构师不是一个好的程序员。