SpringSecurity

依赖包

基础包

<!-- 核心框架和类 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>${springSecurity.version}</version>
</dependency>

<!-- 支持web层 -->
<dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>${springSecurity.version}</version>
</dependency>    

<!-- 整体配置支持 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${springSecurity.version}</version>
</dependency>

<!-- 支持JSP标签库 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>${springSecurity.version}</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

额外支持包

  1. 支持CAS
     <!-- Addition: 支持CAS-->
     <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-cas</artifactId>
       <version>${springSecurity.version}</version>
     </dependency>
     <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-cas-client</artifactId>
       <version>${springSecurity.version}</version>
     </dependency>
    
  2. 支持LDAP
     <!-- Addition:支持LDAP -->
     <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-ldap</artifactId>
       <version>${springSecurity.version}</version>
     </dependency>
    
  3. 支持ACL,加入spring-security-acl-nnn.jar
  4. 支持OpenID ,加入spring-security-openid-nnn.jar

装载SpringSecurity

配置web.xml文件

  1. context-param
     <context-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>classpath*:beans.xml,classpath*:security.xml</param-value>
     </context-param>
    
  2. listener
    • 加载Spring监听器 (因为下面的过滤器Filter中要使用到此Bean,所以使用Listener的方式加载spring)
      <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
      
    • 加载Security Session生命周期事件监听器
      <!--设置session 超时时间间隔:以分钟为单位,值必须为整数(如果值为零或负数,则表示会话将永远不会超时)-->
      <session-config>
        <session-timeout>60</session-timeout>
      </session-config>
      <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
      </listener>
      
  3. filter
    <filter>
     <filter-name>springSecurityFilterChain</filter-name>
     <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
     <filter-name>springSecurityFilterChain</filter-name>
     <url-pattern>/*</url-pattern>
    </filter-mapping>
    

PS:

  1. DelegatingFilterProxy将过滤处理委托给一个由Security注入Spring容器的Filter (我们无法对注册在web.xml中的Servlet Filter进行Bean注入, 但通过Spring的DelegatingFilterProxy,使得我们可以在外部配置文件中配置实际工作的Filter)
  2. 这个filter-namespringSecurityFilterChain对SpringSecurity是有意义的, 用于在Spring上下文中查找Filter Bean

装载过程说明

  1. 容器创建一个ServletContext(上下文)
    • 这个WEB项目所有部分都将共享这个上下文
    • 容器将<context-param>转化为键值对,交给ServletContext
  2. Spring监听器ContextLoaderListener,初始化Spring容器(调用 contextInitialized(ServletContextEvent event)
    • ServletContext sc= event.getServletContext();
    • this.contextLoader.initWebApplicationContext(sc);
      • 获取<context-param>contextConfigLocation指定的配置文件
        context-param的值 = sc.getInitParameter("context-param的键");
        
      • 交给WebApplicationContext,解析并注入配置文件中定义的Bean
  3. 解析Security配置文件(security.xml)中的标签,创建注入Security Bean到Spring容器
    • Security解析包为:spring-security-config.jar,包下META-INF中有两个文件:
      • spring.schemas:标签的规范、约束
      • spring.handlers:定义真正解析自定义标签的类
        • 内容为:http\://www.springframework.org/schema/security=org.springframework.security.config.SecurityNamespaceHandler
        • 即spring security的标签解析由SecurityNamespaceHandler来处理
    • 注意:Security配置文件需添加Spring Security命名空间):
      <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:sec="http://www.springframework.org/schema/security"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
      http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
        ...
      </beans>
      
  4. 加载Security代理过滤器DelegatingFilterProxy,截获/*所有请求
    • DelegatingFilterProxy
      • 位于spring-web.jar包,本身是和springSecurity无关
      • 通过registerBeanComponent方法将 FilterChainProxy注册到Spring容器中,并控制注册的别名为springSecurityFilterChain
      • 其实该类仅仅是初始化一个FilterChainProxy,然后把所有拦截的Request交给Spring容器中的FilterChainProxy处理 DelegatingFilterProxy
    • FilterChainProxy
      • 是一个特殊的Servlet Filter,它可以链接任意一个或多个其他的过滤器
      • package org.springframework.security.web filterchain

SecurityFilters

对于Web资源,我们大约可以只用6个过滤器来保护我们的应用系统: filters

Spring Security依赖一系列的Servlet Filter来提供不同的安全特性 (不需要显示的声明springSecurityFilterChain以及它所链接在一起的其他Filters)

  • Spring Security会根据在xml中配置的标签,自动创建所需要的Bean
  • 通过使用Java EE的servlet过滤器链按顺序组合起来

PS: Spring Security中默认Filter顺序,通过enum SecurityFilters 定义

  • Value [Integer.MIN_VALUE,100~2100,Integer.MAX_VALUE],每个间隔100
  • 对于customer配置的Filter,有afterbeforeposition三种顺序 (+-1)

100 CHANNEL_FILTER

ChannelProcessingFilter :通道处理过滤器

  • 只要intercept-url标签中包含requires-channel属性,该过滤器就被创建,并把决策权交给ChannelDecisionManager决策处理器处理
  • requires-channel 值有以下三种:

    含义 说明
    any 任何通道都支持 决策管理器不做处理
    https 只支持安全通道 决策管理器把决策任务交给ChannelProcessor列表循环处理
    (若以http方式访问,则重定向到https通道,以SSL方式访问)
    http 只支持http 决策管理器把决策任务交给ChannelProcessor列表循环处理
    (若以https方式访问,则重定向到http通道,以不安全的方式访问)

200 SECURITY_CONTEXT_FILTER

SecurityContextPersistenceFilter:持久化SecurityContext

interface SecurityContext

  • 用于存放认证实体Authentication
  • 方法:
    • Authentication getAuthentication(); 获取认证实体
    • void setAuthentication(Authentication authentication); 设置认证实体
  • 实际存储在:
    • Session中,可通过 getAttribute("SPRING_SECURITY_CONTEXT") 获取
    • SecurityContextHolder中,可通过 SecurityContextHolder.getContext() 获取,有三种保存策略(SecurityContextHolderStrategy):
      • ThreadLocal 默认 (不能跨越多个request存在,即一次request有效)
      • InheritableThreadLocal
      • Global

300 CONCURRENT_SESSION_FILTER

ConcurrentSessionFilter:Session并发控制

  • 从session缓存中获取当前session信息
  • 若发现过期了,则跳转到expired-url配置的url,或者响应session失效提示信息

配置:

<sec:session-management>
    <sec:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" expired-url= "/xxx"/>
</sec:session-management>
属性 说明
max-sessions Session最大并发数:当前认证实体的已注册的最大session数
expired-url 跳转到指定url
若未配置,则提示:This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
error-if-maximum-exceeded true:若max-sessions为1,则第二次登录不了
false(默认) :若max-sessions为1,则第二次是能够登录的,但第一次登录的账号再次发起的请求,会跳转到expired-url
session-registry-ref 默认使用:SessionRegistryImpl,完成对发布的Session的生命周期事件的处理

400 LOGOUT_FILTER

LogoutFilter:登出过滤器

监控一个实际为退出功能的URL(默认为/j_spring_security_logout),

  • 监控处理filterProcessesUrl请求(退出功能的URL,默认为/j_spring_security_logout
  • 循环List<LogoutHandler> 每个LogoutHandlerlogout()方法
  • 成功后交由有logoutSuccessHandler处理(跳转到指定URL)

配置:

<sec:logout
    logout-success-url=""
    success-handler-ref=""
    logout-url=""
    invalidate-session="false"
    deleteCookies=“XXX,XXX”
/>
属性 说明
logout-url 默认为: /j_spring_security_logout
对应filterProcessesUrl字段,设置要监控的退出URL
logout-success-url 默认为: /
会使用SimpleUrlLogoutSuccessHandler实现页面跳转
success-handler-ref 指向一个LogoutSuccessHandler的实现类
用于完成退出系统后的后续动作
invalidate-session 用于配置SecurityContextHandlerinvalidateHttpSession属性

PS:

logout-success-urlsuccess-handler-ref 只能设置其中之一

  • 使用logout-success-url
      <sec:logout logout-success-url="/index"/>
    
  • 使用success-handler-ref
    <sec:logout success-handler-ref="myLogoutSuccessHandler"/>
    <!--
    myLogoutSuccessHandler:自定义的LogoutSuccessHandler
    功能:为LogoutURL加入动态时间参数,以防止被浏览器缓存
    -->
    <bean id="myLogoutSuccessHandler" class="com.cj.security.core.auth.MyLogoutSuccessHandler">
      <property name="logoutSuccessUrl" value="https://sso.seagate.com/logout.html?doc="/>
    </bean>
    

500 X509_FILTER

X509AuthenticationFilter: X509认证过滤器

600 PRE_AUTH_FILTER

RequestHeaderAuthenticationFilter:预认证过滤器(需手动注入)

  • 从RequestHeader中获取预认证用户信息,默认从header attribute SM_USER中获取用户名
  • extends AbstractPreAuthenticatedProcessingFilter
  • 使用PreAuthenticatedAuthenticationProvider认证器处理PreAuthenticatedAuthenticationToken获取认证实体
    • 可以使用PreAuthenticatedGrantedAuthoritiesUserDetailsService从Request中获取权限列表
    • 也可使用UserDetailsByNameServiceWrapper<T extends Authentication>从其他地方获取权限列表

例如:

<sec:http>
    ...
    <sec:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
<sec:http>

<bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
        <property name="principalRequestHeader" value="SM-USER"/>
        <property name="exceptionIfHeaderMissing" value="false"/>
        <property name="authenticationManager" ref="myAuthenticationManager" />
</bean>

```xml
<sec:http>
    ...
    <sec:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
<sec:http>

<bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
        <property name="principalRequestHeader" value="SM-USER"/>
        <property name="exceptionIfHeaderMissing" value="false"/>
        <property name="authenticationManager" ref="myAuthenticationManager" />
</bean>

<sec:authentication-manager id="myAuthenticationManager" >
    <sec:authentication-provider ref="preauthAuthProvider" />
    ...
</sec:authentication-manager>

<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
    <property name="preAuthenticatedUserDetailsService">
        <bean id="userDetailsServiceWrapper"  class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
            <property name="userDetailsService" ref="myUserDetailsService"/>
        </bean>
    </property>
</bean> 

<!-- 通过myUserDetailsService获取用户权限列表-->
<bean id="myUserDetailsService" class="com.cj.support.security.service.MyUserDetailsService"/>

700 CAS_FILTER

CAS认证过滤器(需手动注入)

  • CasAuthenticationEntryPoint:认证入口点,会跳转到CAS Server提供的登录界面
  • CAS Server :对登录信息进行处理,如果登录成功,就跳转回本系统指定的URL,并携带ticket
  • 拦截URL:默认为/j_spring_cas_security_check
    • 对ticket进行验证
    • 构建认证实体CasAuthenticationToken

PS: CAS(Center Authentication Service) 中心认证服务,提供单点登录门户 (SSO:Sigle Sign-On Portal)

800 FORM_LOGIN_FILTER

UsernamePasswordAuthenticationFilter:登录认证过滤器

  • 对登录账号进行认证处理,并把认证通过的实体保存到SecurityContext
  • 默认处理请求:POST : /j_spring_security_check?j_username=?&j_password=?
  • extends AbstractAuthenticationProcessingFilter
    • doFilter是由父类AbstractAuthenticationProcessingFilter完成
      • 认证 abstract Authentication attemptAuthentication(req,resp)
      • session策略处理认证信息 sessionStrategy.onAuthentication(auth,req,resp) (通过<session-management>标签定义)
      • 结果处理
    • @Override Authentication attemptAuthentication(req,res)
      • 默认只对POST方法处理,否则 throw new AuthenticationServiceException
      • 从request中获取username&password
      • 构造未认证的UsernamePasswordAuthenticationToken
      • 构建details authenticationDetailsSource.buildDetails(request)
      • 通过AuthenticationManager完成认证任务,返回认证实体 authenticationManager.authenticate(Authentication token)

配置:

<sec:form-login 
    login-page="/index" 
    authentication-failure-url="/index?login_error=t" 
    default-target-url="/index"
    login-processing-url="/common/login-process" 
    password-parameter="myPassword" 
    username-parameter="myUsername" />

相当于:

<bean id="authenticationFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager"/>
    <property name="postOnly " value="true"/>  <!-- 默认即为true,只处理post请求 -->
    <property name="filterProcessesUrl" value="/common/login-process"/>
    <property name="usernameParameter" value="myPassword "/>
    <property name="passwordParameter" value="myUsername"/>
</bean>
属性 说明
login-page 登录页面
authentication-failure-url 认证失败时跳转的URL
default-target-url 认证成功时跳转的URL
默认为:/spring_security_login?login_error=xxx
authentication-failure-handler-ref 登录失败处理器
authentication-success-handler-ref 登录成功处理器
login-processing-url 默认为:/j_spring_security_check
username-parameter 默认为: j_username
password-parameter 默认为: j_password

900 OPENID_FILTER

OpenIDAuthenticationFilter: OpenID认证

1000 LOGIN_PAGE_FILTER

DefaultLoginPageGeneratingFilter: 生成默认登录页

1100 DIGEST_AUTH_FILTER

DigestAuthenticationFilter: Digest认证过滤器(需手动注入)


<sec:http>
    <sec:custom-filter ref="digestFilter" position="DIGEST_AUTH_FILTER" />
    ...
</sec:http>

<bean id="digestFilter" class="org.springframework.security.web.authentication.www.DigestAuthenticationFilter">
    <property name="userDetailsService" ref="myUserService" />
    <property name="authenticationEntryPoint" ref="digestEntryPoint" />
</bean>

<bean id="digestEntryPoint" class="org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint">
    <property name="realmName" value="shuttle vendor" />
    <property name="key" value="zero" />
</bean>

1200 BASIC_AUTH_FILTER

BasicAuthenticationFilter: 基本认证

配置<sec:http-basic/>

1300 REQUEST_CACHE_FILTER

RequestCacheAwareFilter: 用于用户认证成功后,重新恢复因为登录被打断的请求(由requestCache处理)

说明:

未认证用户发送一个请求Request时,会有ExceptionTranslationFilter捕获到Authentication或匿名用户引发的AccessDeniedException,并将此Request暂时保存到Session,供下次通过认证时恢复这个Request

interface RequestCache实现类有:HttpSessionRequestCache(默认),NullRequestCache

配置不恢复打断请求:

<sec:request-cache ref="nullRequestCache"/>
<bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>

1400 SERVLET_API_SUPPORT_FILTER

SecurityContextHolderAwareRequestFilter: 封装一些ServletApi到Request对象

  • 功能: 包装Request对象SecurityContextHolderAwareRequestWrapper对象,传递到下一个Filter
  • 原理:使用装饰模式(Decorate Model),装饰HttpServletRequest对象
  • SecurityContextHolderAwareRequestWrappe 增加对非匿名认证实体的查询和判断api
    • boolean isUserInRole(String role):判断非匿名认证实体是否授权
    • String getRemoteUser():获取非匿名认证实体的登录账号
    • Principal getUserPrincipal():获取非匿名认证实体

1500 JAAS_API_SUPPORT_FILTER

JaasApiIntegrationFilter

1600 REMEMBER_ME_FILTER

RememberMeAuthenticationFilter: RememberMe AutoLogin 认证

说明: 当SecurityContextHolder中不存在Authentication用户授权信息时,此Filter会调用rememberMeServices 的autoLogin()方法从cookie中获取用户信息自动登录

  • 创建条件:配置了<remember-me>
  • 执行条件:SecurityContext实例中不存在Authentication认证信息时 (即没有认证实体时)
  • 处理:
    • 从cookie获取认证实体Authentication rememberMeServices.autoLogin(request, response)
    • 交由AuthenticationManager验证authenticationManager.authenticate(auth) (通过后设置认证信息到SecurityContext中)
    • 下一个Filter

Remember me功能设置了一个cookie在用户的浏览器上,它包含一个Base64编码的字符串,包含以下内容:

  • 一个MD5的散列值,包括过期日期/时间、用户名和密码;
  • 应用的key值,是在元素的key属性中定义的

使用:

  1. 通过配置<remember-me>,添加此Filter到过滤器链中

     <sec:remember-me 
         key="" 
         data-source-ref="" 
         user-service-ref="" 
         token-validity-seconds="" 
         token-repository-ref=""/>
    
    属性 说明
    Key 建议key值包含应用的唯一名称以及至少36位长度的随机字符,eg: 420046A65F774C9ABBF83E73DA1C07C9
    token-validity-seconds cookie的过期时间
    data-source-ref / token-repository-ref 默认使用TokenBasedRememberMeServices,也可配置使用PersistentTokenBasedRememberMeServices
  2. <form>中触发rememberMe功能

    <!-- URL?_spring_security_ remember_me=true -->
    <input id="remember" name="_spring_security_ remember_me" type="checkbox" value="true"/>
    <label for="remember "> Remember Me? </label>
    

1700 ANONYMOUS_FILTER

AnonymousAuthenticationFilter: 无认证实体时添加一个匿名认证实体

  • 执行条件:当前SecurityContext中没有认证实体
  • 功能: 产生一个匿名认证实体,并保存到SecurityContext中

即若前面Filter(UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter等)都未认证成功,则在当前的SecurityContext中添加一个经过匿名认证的token

配置:

<sec:anonymous key="test" granted-authority="ROLE_GUEST" username="guest"/>
属性 说明
granted-authority 设置权限,注入到authorities授权列表
默认为:ROLE_ANONYMOUS
username 匿名认证账号
默认为:anonymousUser

PS: 通过servlet的getRemoteUser等方法无法获取到登录账号, 因为SecurityContextHolderAwareRequestFilter过滤器在此Filter的前面

1800 SESSION_MANAGEMENT_FILTER

SessionManagementFilter: Session管理,提供两大类功能:

  1. session固化保护:通过session-fixation-protection配置
  2. session并发控制:通过concurrency-control配置

配置security.xml:

<sec:session-management session-fixation-protection="migrateSession" invalid-session-url="/common/timeout.jsp">
        <sec:concurrency-control max-sessions="1" expired-url= "/index?error=This session has been expired"/>
</sec:session-management>
属性 含义 说明
session-fixation-protection session固化保护 none:使得session固化攻击失效(未配置其他属性);
migrateSession:当用户经过认证后分配一个新的session,它保证原session的所有属性移到新session中;
newSession:当用户认证后建立一个新的session,原(未认证时)session的属性不会进行移到新session中来;
invalid-session-url: 设置invalidSessionStrategy的跳转URL,默认使用 SimpleRedirectInvalidSessionStrategy
concurrency-control session并发控制 从session缓存中获取当前session信息,如果发现过期了,就跳转到expired-url配置的url或者响应session失效提示信息
1. error-if-maximum-exceeded="true",max-sessions="1": 第二次登录时,是登录不了的;
2. error-if-maximum-exceeded="false" : 第二次是能够登录到系统的,但是第一个登录的账号再次发起请求时,会跳转到expired-url配置的url中;
3. 如果没有配置,则显示提示信息:This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
(参见ConcurrentSessionFilter)

配置web.xml:

<!-- 设置session 超时时间 -->
<session-config>
         <session-timeout>60</session-timeout>
</session-config>

<!-- 让servelt容器通知SpringSecurity session失效 -->
<!-- HttpSessionEventPublisher监听器:主要监听sessionCreated、sessionDestroyed事件,发布通知-->
<listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

PS: ConcurrentSessionFilter过滤器也是处理session失效,但是它所处理的仅仅是封装后的SessionInformation类,如果这个类满足失效条件,再执行session.invalidate强制失效

1900 EXCEPTION_TRANSLATION_FILTER

ExceptionTranslationFilter:仅捕获此Filter之后发生的异常

仅捕获此Filter之后发生的异常: FilterSecurityInterceptor,SwitchUserFilter,1900 Filter之后的自定义拦截器的异常

主要拦截两类安全异常:

  1. AuthenticationException 认证异常
    • 清空SecurityContext中的认证实体Authentication
    • 保存当前被打断的请求 requestCache.saveRequest(request, response);
    • 认证入口点AuthenticationEntryPoint处理此异常
  2. AccessDeniedException 访问拒绝异常
    • 匿名用户引发 (同认证异常Authentication的处理)
    • 非匿名用户引发 (由AccessDeniedHandler处理)

配置:

<sec:http auto-config="false" 
    entry-point-ref="http403EntryPoint" 
    access-denied-page="/home/accessDenied" >
    ...
</sec:http>
属性 说明
entry-point-ref 认证入口点,指向认证失败或匿名用户拒绝访问异常时的处理bean
默认:根据http标签下具体配置的认证类型加载对应的AuthenticationEntryPoint
access-denied-page 访问拒绝时跳转的页面
为设置access-denied-handler时,对应AccessDeniedHandlerImplerrorPage

手动指定AuthenticationEntryPoint:

<!-- 
这里使用Security自带的AuthenticationEntryPoint,也可使用自定义的
( Response 403 error,msg: "Access Denied" )
-->
<bean id="http403EntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"/>

2000 FILTER_SECURITY_INTERCEPTOR

FilterSecurityInterceptor: Web级别的访问控制

  • 功能:完成URL级别的授权处理
  • extends abstract class AbstractSecurityInterceptor
    • 获取资源权限列表 (通过SecurityMetadataSource 资源权限获取器)
    • 认证 (通过AuthenticationManager 认证管理器)
    • 访问控制判断 (通过AccessDecisionManager 访问决策器)

配置:

<sec:http pattern="/resources/**"  security="none"/>
<sec:http>
    <sec:intercept-url pattern="/security/**" access="ROLE_ADMIN"/>
    <sec:intercept-url pattern="/back/**" access="ROLE_ADMIN,ROLE_USER"/>
    ...
</sec:http>
<sec:http use-expressions="true" >
    <sec:intercept-url pattern="/" access="hasRole('ROLE_GUEST') or authenticated"/>
    <sec:intercept-url pattern="/" access="permitAll"/>
    <sec:intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"/>
    <sec:intercept-url pattern="/api/**" method="GET" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
     ...
</sec:http>
<sec:intercept-url>的属性 说明
pattern a URL pattern
path-type 设置pattern的匹配类型
(用于判断Request和设置的URL pattern是否匹配)
可以设置为:ant(默认),regex,ciRegex
(由对应的RequestMatcher处理)
access security rules
若设置use-expressions='true',则可使用SpEL(Spring expressions language)
method allowed Http Method (GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE
default value: null (allow all)
required-channel 允许的通道:http,https,any(默认)
security none 表示不进行安全检查,即不做授权判断,直接通过

SpEL expressions added by Spring Security 3.0 :

expression 说明
authentication The user’s authentication object
denyAll Always evaluates to false
hasAnyRole(list of roles) true if the user has been granted any of the roles specified
hasRole(role) true if the user has been granted the specified role
hasIpAddress(IP Address) The user's IP address (only available in web security)
isAnonymous() true if the current user is an anonymous user
isAuthenticated() true if the current user is not anonymous
isFullyAuthenticated() true if the current user is neither an anonymous nor a remember-me user
isRememberMe() Always evaluates to true
permitAll Always evaluates to true
principal The user’s principal object
authentication The user’s authentication object

2100 SWITCH_USER_FILTER

SwitchUserFilter: 切换用户

http标签

HttpSecurityBeanDefinitionParser解析标签

最小化配置:

<sec:http auto-config="true" >
        <sec:intercept-url pattern="/**" access="ROLE_ADMIN"/> 
        <!--使用Ant风格的路径-->
    </sec:http>
  • 使用<http>标签将会自动创建一个FilterChainProxy(它会委托给配置在web.xml中的DelegatingFilterProxy)以及链中的所有过滤器Bean
  • 通过配置auto-config='true',将得到三个免费赠品:
    • 额外登陆页
    • http基本认证
    • 登出功能
    • 即等价于显示配置了:<form-login/> ,<http-basic/> ,<logout/>标签
      <sec:http auto-config="true" >
        <sec:form-login/>
        <sec:http-basic/>
        <sec:logout/>
        <sec:intercept-url pattern="/**" access="ROLE_ADMIN"/>
      </sec:http>
      

认证

一般处理过程:

  1. XxxAuthenticationFilter 拦截Request,获取需认证的信息,构建要进行认证的认证实体xxxToken
  2. AuthenticationManager 认证管理器,对xxxToken进行认证 (实际调用实现类ProviderManagerauthenticate(xxxToken)方法)
    • 循环List<AuthenticationProvider> 认证器列表
    • 使用支持此认证Token的AuthenticationProvider认证器(XxxAuthenticationProvider)进行认证,返回认证Token
      • provider.supports(authentication.getClass()) 找到支持此认证xxxToken的认证器
      • provider.authenticate(authentication) 对此xxxToken进行认证,返回认证过的认证实体
      • 注意:只要有一个provider认证成功,则成功,后续的provider将不会执行

简单配置:

<sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider>
        <sec:user-service>
            <sec:user authorities="ROLE_USER" name="tom" password="123"/>
            <sec:user authorities="ROLE_USER" name="jerry" password="456"/>
            <sec:user authorities="ROLE_ADMIN" name="admin" password="111"/>
        </sec:user-service>
    </sec:authentication-provider>
</sec:authentication-manager>

PS: 用户的认证信息通过SecurityContextHolder信息传递。 eg: SecurityContextHolder.getContext().getAuthentication();

Authentication接口可以实现的方法:

方法签名 描述
Object getPrincipal() 返回安全实体的唯一标识(如,一个用户名)
Object getCredentials() 返回安全实体的凭证信息
List<GrantedAuthority> getAuthorities() 得到安全实体的权限集合,根据认证信息的存储决定的
Object getDetails() 返回一个跟认证提供者相关的安全实体细节信息

JDBC认证

SpringSecurity使用DaoAuthenticationProvider支持UsernamePasswordAuthenticationToken

需使用自定义的UserDetailsService实现类,从DB获取用户信息构建UserDetails对象,例如:

<bean id="myUserDetailsService" class="com.cj.security.service.MyUserDetailsServiceImpl"/>
public class MyUserDetailsService implements UserDetailsService{
    static Logger logger = Logger.getLogger(MyUserDetailsService.class);
    private ISecurityService securityService;

    public ISecurityService getSecurityService(){
        return securityService;
    }
    @Inject
    public void setSecurityService(ISecurityService securityService){
        this.securityService = securityService;
    }

    @Override
    public UserDetails loadUserByUsername(String account)
            throws UsernameNotFoundException{
        //MyUser implements UserDetails, CredentialsContainer
        //一般获取用户的account,password,salt信息
        MyUser user=securityService.getUser(account);
        if(user==null)
            throw new UsernameNotFoundException("No User");
        //获取用户角色列表
        List<GrantedAuthority> glist=securityService.getGrantedAuthorities(account);
        if(glist==null || glist.size()==0)
            throw new UsernameNotFoundException("No GrantedAuthority");
        UserDetails userDetails=new MyUser(user, glist);
        return userDetails;
    }
}

password使用plaintext明文(即不加密)

<sec:authentication-manager alias="myAuthenticationManager" >
    <sec:authentication-provider user-service-ref="myUserDetailsService" />
</sec:authentication-manager>

使用MD5+salt:username 加密 (即对{usermane}password加密)

<sec:authentication-manager>
    <sec:authentication-provider user-service-ref="myUserDetailsService">
        <sec:password-encoder hash="md5">
            <sec:salt-source user-property="username"/>
        </sec:password-encoder>
    </sec:authentication-provider>
</sec:authentication-manager>

相当于:

<authentication-manager alias="authenticationManager">
  <authentication-provider user-service-ref="myUserDetailsService">
    <password-encoder ref="passwordEncoder">
      <salt-source ref="saltSource"/>
    </password-encoder>
  </authentication-provider>
</authentication-manager>

<bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>

<bean class="org.springframework.security.authentication.dao.ReflectionSaltSource" id="saltSource">
  <property name="userPropertyToUse" value="username"/>
</bean>

使用MD5+salt:radom 加密(推荐!)

<sec:authentication-manager alias="myAuthenticationManager" >
    <sec:authentication-provider user-service-ref="mySaltUserDetailsService" >
        <sec:password-encoder hash="md5">
            <sec:salt-source user-property="usersalt"/>
        </sec:password-encoder>
    </sec:authentication-provider>
</sec:authentication-manager>

PS:扩展数据库schema,salt要与用户记录一起保存在数据库 ,eg: SysUser(account,password,enable,usersalt)

  • addUser/changePassword(SysUser u)
    int salt=(int)(Math.random()*1000000);
    u.setUsersalt(String.valueOf(salt));
    u.setPassword(passwordEncoder.encodePassword(u.getPassword(), u.getUsersalt()));
    
  • SQL insert user
    insert into sys_user(username, password, enabled, usersalt) values ('admin','admin',true,CAST(RAND()*1000000000 AS varchar));
    

LDAP认证

使用LdapAuthenticationProvider支持UsernamePasswordAuthenticationToken

  • LdapAuthenticator authenticator; 用于认证用户,不通过则throw BadCredentialsException
  • LdapAuthoritiesPopulator authoritiesPopulator; 用于获取权限信息列表(即角色列表)
  • boolean hideUserNotFoundExceptions = true;

必须加入依赖包:

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-ldap</artifactId>
  <version>${springSecurity.version}</version>
</dependency>

LDAP 介绍:

  • Lightweight Directory Access Protocol 轻量级目录访问协议
  • 是目录服务在TCP/IP上的实现
  • 允许根据需要使用ACI(一般都称为ACL或者访问控制列表)控制对数据读和写的权限

PS: ACI可以根据谁访问数据、访问什么数据、数据存在什么地方以及其它对数据进行访问控制。 因为这些都是由LDAP目录服务器完成的,所以不用担心在客户端的应用程序上是否要进行安全检查。

LDIF文件:

  • LDIF用文本格式表示目录数据库的信息,以方便用户创建、阅读和修改
  • 按照树型结构组织,由条目Entry组成(相当于DB中Table的记录) LDIF

SpringSecurity LDAP 提供两种认证方式:

  • BindAuthenticator 绑定认证
    • LDAP服务器进行认证和校验用户密码
    • 默认
  • PasswordComparisonAuthenticator 密码对比认证
    • SpringSecurity负责将密码编码成目录期望的格式然后与目录进行对比认证校验
    • 需配置<sec:password-compare/>,默认LDAP的密码编码算法为SHA
    • 为了安全,密码对比策略并不能真正从目录上读取(基于安全策略,读取目录的密码通常会被拒绝)
    • 作为替代,PasswordComparisonAuthenticator会从用户的目录条目作为根节点进行一个LDAP查找,试图查找与SpringSecurity编码密码值匹配的密码属性值

配置LdapServer:

<sec:ldap-server url="ldap://ldap.sing.seagate.com:389/o=xxx" id="ldapServer" />

通过LDAP完成用户认证,并加载用户的权限列表

配置从LDAP上获取认证用户(包括角色列表),直接使用LdapAuthenticationProvider(下面示例使用密码对比认证):

<sec:authentication-manager alias="myAuthenticationManager">
    <sec:ldap-authentication-provider
        server-ref="ldapLocal"
        user-search-base="ou=people"
        user-search-filter="(uid={0})"
        group-search-base="ou=Groups">
        <sec:password-compare/>
    </sec: ldap-authentication-provider>
</sec:authentication-manager>
属性 说明 默认
group-search-base 用于查找group(角色)
定义了基础的DN,LDAP集成应该基于此往下为用户查找一个或多个的匹配项。
默认值会在LDAP根中进行查找,这可能会代价较高;
group-search-filter LDAP查找的过滤器,用来匹配用户的DN与group-search-base之下的条目属性;
过滤器通过两个参数进行参数化设置:
第一个({0})作为用户的DN
第二个作为({1})作为用户的名字
默认值为(uniqueMember={0})
group-role-attribute 它定义了匹配条目中用来组装用户GrantedAuthority的属性; 默认值为cn
role-prefix 要拼到在group-role-attribute中发现值的前缀以产生Spring Security的GrantedAuthority 默认值为“ROLE_”
user-search-base 用于查找users /
user-search-filter 用于过滤users /
user-details-class 匹配构建UserDetails对象方式: inetOrgPerson,person person

通过LDAP完成用户认证,从其他地方加载用户的权限列表

  1. 配置AuthenticationManager & LdapAuthenticationProvider

     <sec:authentication-manager alias="myAuthenticationManager">
         <!-- <sec:ldap-authentication-provider  user-search-base="ou=people" user-search-filter="(uid={0})" /> -->
         <sec:authentication-provider ref="ldapAuthenticationProvider"/>
     </sec:authentication-manager>
    
     <bean id="ldapAuthenticationProvider" 
         class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
         <constructor-arg name="authenticator" ref="bindAuthenticator"/>
         <constructor-arg name="authoritiesPopulator" ref="ldapAuthoritiesPopulator"/>
         <!-- <property name="hideUserNotFoundExceptions" value="false"/> -->
     </bean>
    
  2. 配置Provider中的LdapAuthenticator用于认证用户(DirContextOperations对象)

     <bean id="bindAuthenticator" class="org.springframework.security.ldap.authentication.BindAuthenticator">
         <constructor-arg name="contextSource" ref="ldapServer"/>
         <property name="userSearch" ref="ldapSearch"/>
     </bean>
    
     <bean id="ldapSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">  
       <constructor-arg name="searchBase" value="ou=people"/>   <!-- user-search-base -->  
       <constructor-arg name="searchFilter" value="(uid={0})"/> <!-- user-search-filter -->
       <constructor-arg name="contextSource" ref="ldapServer"/>
     </bean>
    
  3. 配置Provider中的LdapAuthoritiesPopulator用于获取权限信息列表(Collection<? extends GrantedAuthority>

    • 方式1:扩展UserDetailsService

        <bean class="org.springframework.security.ldap.authentication.UserDetailsServiceLdapAuthoritiesPopulator" id="ldapAuthoritiesPopulator">
           <constructor-arg ref="myUserDetailsService"/>
         </bean>
      
         <bean id="myUserDetailsService" class="com.cj.security.service.MyUserDetailsServiceImpl"/>
      
        public class MyUserDetailsService implements UserDetailsService{
            static Logger logger = Logger.getLogger(MyUserDetailsService.class);
            private ISecurityService securityService;
      
            public ISecurityService getSecurityService(){
                return securityService;
            }
            @Inject
            public void setSecurityService(ISecurityService securityService){
                this.securityService = securityService;
            }
      
            @Override
            public UserDetails loadUserByUsername(String account)
                    throws UsernameNotFoundException{
                //获取用户角色列表
                List<GrantedAuthority> glist=securityService.getGrantedAuthorities(account);
                if(glist==null || glist.size()==0)
                    throw new UsernameNotFoundException("No GrantedAuthority");
                UserDetails userDetails=new User(account,null,glist);
                return userDetails;
            }
        }
      
    • 方式二:扩展LdapAuthoritiesPopulator

        <bean id="ldapAuthoritiesPopulator" class="com.cj.support.security.ldap.MyLdapAuthoritiesPopulator"/>
      
        public class MyLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator{
            static Logger logger = Logger.getLogger(MyLdapAuthoritiesPopulator.class); 
            private ISecurityService securityService;
            public ISecurityService getSecurityService(){
                return securityService;
            }
            @Inject
            public void setSecurityService(ISecurityService securityService){
                this.securityService = securityService;
            }
      
            @Override
            public Collection<? extends GrantedAuthority> getGrantedAuthorities(
                    DirContextOperations userData, String username){
                return securityService.getGrantedAuthorities(username);
            }
        }
      

CAS

  1. 使用必须额外加入CAS包

     <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-cas</artifactId>
       <version>${springSecurity.version}</version>
     </dependency>
     <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-cas-client</artifactId>
       <version>${springSecurity.version}</version>
     </dependency>
    
  2. CasAuthenticationFilter加入FilterChain,并注入Cas AuthenticationEntryPoint

     <sec:http auto-config="true" entry-point-ref="casAuthEntryPoint" access-denied-page="/home/accessDenied">
         <sec:custom-filter ref="casAuthenticationFilter" position="CAS_FILTER"/>
         <sec:logout logout-success-url="/index"/>
     </sec:http>
    
  3. 配置CAS 认证入口点AuthenticationEntryPoint

     <bean id="casAuthEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
         <property name="loginUrl" value="https://server:8443/cas/login"/>
         <property name="serviceProperties" ref="casService"/>
     </bean>    
    
     <bean id="casService" class="org.springframework.security.cas.ServiceProperties">
         <property name="service" value="http://localhost:8888/docms/j_spring_cas_security_check"/>
     </bean>
    
  4. 配置CasAuthenticationFilter,注入authenticationManager

    <bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
            <property name="authenticationManager" ref="authenticationmanager"/>
    </bean>
    
    <sec:authentication-manager alias="authenticationmanager">
        <sec:authentication-provider ref="casAuthenticationProvider"/>
    </sec:authentication-manager>
    
  5. 配置CasAuthenticationProvider

     <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
         <property name="ticketValidator" ref="casTicketValidator"/>
         <property name="serviceProperties" ref="casService"/>
         <property name="key" value="docms"/>
         <property name="authenticationUserDetailsService" ref="authenticationUserDetailsService"/>
     </bean>
    
     <bean id="casTicketValidator" class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
         <constructor-arg value="https://server:8443/cas/"/>
     </bean>
    
     <bean id="authenticationUserDetailsService" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
             <property name="userDetailsService" ref="userDetailsManager"/>
     </bean>
    

Siteminder

使用RequestHeaderAuthenticationFilter获取经过Siteminder认证后的用户信息,通过PreAuthenticatedAuthenticationProvider认证器进一步处理PreAuthenticatedAuthenticationToken构建认证实体

具体请参考上文中【600 PRE_AUTH_FILTER】的相关介绍

访问控制

SpringSecurity提供两个访问控制的级别:

  • Web层(RequestURL级别):基于Servlet Filter
    • 使用FilterSecurityInterceptor (URL拦截器)
  • 方法层(Method级别):基于Spring AOP
    • 使用MethodSecurityInterceptor (Method拦截器)

这两个拦截器都继承自abstract class AbstractSecurityInterceptor,包含:

  • interface SecurityMetadataSource 资源权限获取器:资源权限表,SpringSecurity启动时就收集创建
  • interface AuthenticationManager 认证管理器:对认证实体重新进行认证
  • interface AccessDecisionManager 访问决策器:用于验证认证实体Authentication是否有权限访问资源
    • accessDecisionManager.decide(authenticated, fi, attributes);
    • 通过统计投票者的投票结果,完成决策(调用List<AccessDecisionVoter> decisionVoters投票者们的vote()方法,统计投票)
    • 有三种访问决策管理器的实现类:
      • AffirmativeBased(默认,一票赞成则通过)
      • ConsensusBased(多数:赞成票>反对票)
      • UnanimusBased(无异议:全部赞成则通过)
    • 默认加载的投票器AccessDecisionVoter为:
      • AuthenticatedVoter
      • RoleVoter
    • 注意: 若<sec:http>中使用use-expressions="true"则注入WebExpressionVoter,不再加载其他AccessDecisionVoter

Web层(RequestURL级别)

基于Servlet Filter

一般处理过程

  1. FilterSecurityInterceptor 拦截Request
  2. FilterSecurityMetadataSource 获取访问Request所需的权限列表
  3. 使用AuthenticationManager 对当前认证实体重新认证
  4. 使用AccessDecisionManager实现类 的decide(authentication, fi, attributes)方法决策当前Request是否有访问权限
    • 默认为AffirmativeBased决策访问器
    • 循环投票器,统计投票结果 List<AccessDecisionVoter>
    • 投票器AccessDecisionVoter
      • boolean support(attr) 判断是否支持,不支持则返回ACCESS_ABSTAIN,表弃票
      • int vote(auth,fi,attributes) 投票,并返回投票结果

扩展:从其他地方获取访问权限

PS: 因为SpringSecurity自带的FilterSecurityInterceptor 会在SecurityNamespaceHandler中就加载构建,无法修改,故在此Filter之前构建自己的Filter进行授权处理

  1. 配置

     <!-- 1. 设置Filter-->
     <sec:http>
         <sec:custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="mySecurityFilter" />
         <!-- 定义匿名用户-->
         <sec:anonymous key="test" granted-authority="ROLE_GUEST" username="guest"/>
     </sec:http>
    
     <!-- 2. 自定义FilterSecurityInterceptor -->
     <bean id="mySecurityFilter" class="com.cj.security.core.filter.MySecurityFilter">
         <property name="securityMetadataSource" ref="mySecurityMetadataSource"/>
         <property name="accessDecisionManager" ref="myAccessDecisionManager"/>
         <property name="authenticationManager" ref="myAuthenticationManager"/>
     </bean>
    
     <!--3. 自定义SecurityMetadataSource -->
     <bean id="mySecurityMetadataSource" class="com.cj.support.security.intercept.MySecurityMetadataSource">
             <constructor-arg name="securityService" ref="securityService"/>
     </bean>
    
     <!-- 4. 定义认证管理-->
     <sec:authentication-manager id="myAuthenticationManager" >
             <sec:authentication-provider user-service-ref="myUserDetailsService" />
     </sec:authentication-manager>
    
     <!-- 5. 自定义决策管理:增加匿名用户的访问控制-->
     <bean id="myAccessDecisionManager" class="com.cj.security.core.access.MyAccessDecisionWithGuestManager">
         <property name="anonymous" value="ROLE_GUEST"/>
     </bean>
    
  2. 自定义FilterSecurityInterceptor

     public class MySecurityFilter extends AbstractSecurityInterceptor 
                                     implements Filter{
         ...
         @Override
         public void doFilter(ServletRequest request, ServletResponse response,
                 FilterChain chain) throws IOException, ServletException{
             //System.out.println("MySecurityFilter-------");
             FilterInvocation fi = new FilterInvocation(request, response, chain);  
             invoke(fi);  
    
         }
         private void invoke(FilterInvocation fi) throws IOException, ServletException {  
             Collection<ConfigAttribute> attributes=this.obtainSecurityMetadataSource().getAttributes(fi);
             InterceptorStatusToken token = super.beforeInvocation(fi);  
             try {  
                 fi.getChain().doFilter(fi.getRequest(), fi.getResponse());  
             } finally {  
                 super.afterInvocation(token, null);
             }
         }
     }
    
  3. 自定义SecurityMetadataSource,在构造时加载所有访问控制信息

     public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource 
     {
         private ISecurityService securityService;
         private static Map<RequestMatcher, Collection<ConfigAttribute>> resourceMap = null;
    
         public MySecurityMetadataSource(ISecurityService securityService) {  
             this.securityService = securityService;  
             resourceMap=securityService.loadResourceDefine();  
         }
         @Override
         public boolean supports(Class<?> clazz){
             return true;
         }
    
         //其他方法实现,可参考DefaultFilterInvocationSecurityMetadataSource类
         ...
     }
    
  4. 自定义决策管理:增加匿名用户的访问控制判断

     public class MyAccessDecisionWithGuestManager implements AccessDecisionManager{
         private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
         private String anonymous="ROLE_ANONYMOUS";
         public String getAnonymous(){
             return anonymous;
         }
         public void setAnonymous(String anonymous){
             this.anonymous = anonymous;
         }
         private boolean isFullyAuthenticated(Authentication authentication) {
             return (!authenticationTrustResolver.isAnonymous(authentication) 
             && !authenticationTrustResolver.isRememberMe(authentication));
         }
         @Override
         public void decide(Authentication authentication, Object object,
         Collection<ConfigAttribute> configAttributes)
                 throws AccessDeniedException, InsufficientAuthenticationException{
             if(configAttributes == null) {
                 return;
             }
             //所请求的资源拥有的权限(一个资源对多个权限)  
             Iterator<ConfigAttribute> iterator = configAttributes.iterator();  
             while(iterator.hasNext())  {  
                 ConfigAttribute configAttribute = iterator.next();  
                 //访问所请求资源所需要的权限  
                 String attribute = configAttribute.getAttribute(); 
                 //System.out.println("needPermision:"+attribute);
                 if(this.anonymous.equals(attribute)){
                     if(authenticationTrustResolver.isAnonymous(authentication) 
                             || isFullyAuthenticated(authentication)
                             || authenticationTrustResolver.isRememberMe(authentication))
                         return;
                 }
                 //用户所拥有的权限authentication  
                 for(GrantedAuthority ga : authentication.getAuthorities()) {  
                     if(attribute.equals(ga.getAuthority())) 
                         return;  
                 }  
             }  
             //没有权限  
             throw new AccessDeniedException("Access Denied -- You don't have permission!");  
         }
    
         @Override
         public boolean supports(ConfigAttribute attribute){
             return true;
         }
         @Override
         public boolean supports(Class<?> clazz){
             return true;
         }
     }
    

方法层(Method级别)

基于Spring AOP

一般处理过程

  • SecurityNamespaceHandler中注入 GlobalMethodSecurityBeanDefinitionParserMethodSecurityMetadataSourceBeanDefinitionParser
  • 收集配置的访问控制信息存入MethodSecurityMetadataSource实例中

支持四种方法层的安全

  1. Spring 简单注解:

     <global-method-security secured-annotations="enabled" />
    
    • @Secured
        /*
        指认证的用户必须被授予"ROLE_SPITTER", "ROLE_ADMIN"其中的一个权限才能访问该方法;
        如果没有这两个中任何一个权限,将抛出异常或是AuthenticationException or AccessDeniedException的子异常;
        如果方法在Web请求中调用,异常将被SpringSecurity自动调用
        */
        @Secured({"ROLE_SPITTER", "ROLE_ADMIN"}) 
        public void addSpittle(Spittle spittle) { // ... }
      
  2. JSR-250标准:

     <!--  JSR-250's @RolesAllowed与secured-annotations可同时被启用,
     推荐使用该方式启用注解,因为他是Java的标准注解。-->
     <global-method-security jsr250-annotations="enabled" />
    
    • @RolesAllowed
        @RolesAllowed({"ROLE_USER","ROLE_ADMIN"})
        public void changePassword(String username, String password);
      
    • @PermitAll
    • @DenyAll
  3. Pre-/Post-invocation (可使用SpEL表达式):

     <global-method-security pre-post-annotations="enabled" />
    
    • @PreAuthorized : 基于表达式的结果在调用方法或类前限制被访问。
        /*
            拥有角色ROLE_SPITTER的用户的参数spittle字符只能少于140个,
      而拥有角色ROLE_PREMIUM的用户则不受此限制
        */
        @PreAuthorize("(hasRole('ROLE_SPITTER') and #spittle.text.length() <= 140) or hasRole('ROLE_PREMIUM')") 
        public void addSpittle(Spittle spittle) { ... }
      
      • @PostAuthorized:如果表达式结果为false,允许方法被调用,同时抛出一个安全异常。该注解主要是基于被保护的方法的返回值来决定执行表达式
        /*
          当返回的Spittle对象属于被认证的用户时才可以访问该方法
          本例中returnObject是SpringEL提供的一个name用于方便获取返回的对象,
        而principal是SpringSecuroty提供的用于代表被当前认证的用户。
        如果认证失败,则AccessDeniedException异常将抛出
        */
        @PostAuthorize("returnObject.spitter.username == principal.username") 
        public Spittle getSpittleById(long id) { ... }
        
    • @PreFilter:允许一个方法被调用,但是用每一个表达式去过滤方法的结果。
    • @PostFilter:允许一个方法被调用,但是过滤在进如方法之前的输入。
  4. 使用protect-pointcut添加安全切点

     <!-- 
         该配置将识别任何拥有@Sensitive的方法
         access属性则指,认证的用户必须拥有的角色去访问表达式识别的
     -->
     <global-method-security>
         <protect-pointcut access="ROLE_SPITTER"  expression="execution(@com.habuma.spitter.Sensitive * *.*(String))"/>
     </global-method-security>
    

结合SpringMVC使用

  1. 在mvc的servlet配置文件(xxx-servlet.xml):

    <sec:global-method-security pre-post-annotations="enabled"/>
    

    PS: 若要在MVC Controller方法上加@PreAuthorize,@PostAuthorize,@PreFilter,@PostFilter, 此配置一定要放在spring mvc的xml中,否则无效

  2. 在XxxController方法上添加方法注解:

     @PostAuthorize("!(#type=='user' and returnObject[extra].senderType=='Group' and !hasRole('ROLE_GROUP'))")
     @RequestMapping(value={"/inputs/{type}/{status}/{id}"},method=RequestMethod.GET)
     @ResponseBody
     public Object getInput(@PathVariable("type") String type,@PathVariable("status") String status,@PathVariable int id) { ... }