基于Spring security的认证和权限管理流程

不深入理解Spring security的工作流程,无法感知基于Sping secury的CAS登录流程和权限管理,对从全局上认知系统架构是极大的阻碍。

Spring securiy官网地址  http://projects.spring.io/spring-security/

 

------------------------

 goals:

1  理解spring security的原理和工作流程

2  熟悉spring security源码,要清楚的知道springFilterChain有哪些filter组成,解读关键代码,

    认知架构和扩展点

3  扩展spring security

4  理解CAS流程,整合CAS登录 

 

-----------------------

在项目中引入Spring-security. 

 

1 POM中加入对Spring-security的依赖

 

The recommended way to get started using spring-security in your project is with a dependency management system – the snippet below can be copied and pasted into your build. Need help? See our getting started guides on building with Maven and Gradle.

<dependencies>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>4.0.2.RELEASE</version>
    </dependency>
</dependencies>

 如果项目中引入了CAS SSO,还需要加入对cas-client的依赖,一个完整的依赖可能如下(全部引入可能会存在冗余依赖的情况),需要引入spring(core,mvc),spring-security(taglib,core,security-web),cas(client)

 

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<servlet-api_version>3.0.1</servlet-api_version>

<slf4j_version>1.6.6</slf4j_version>

<httpclient_version>4.2</httpclient_version>

<commons-io_version>2.4</commons-io_version>

<fastjson_version>1.2.4</fastjson_version>

<spring_version>4.0.2.RELEASE</spring_version>

<security.version>3.2.3.RELEASE</security.version>

<cas-client.version>3.0.8.RELEASE</cas-client.version>

<cas-client-core.version>3.1.11</cas-client-core.version>

<commons-lang.version>2.5</commons-lang.version>

<javadoc.skip>>false</javadoc.skip>

</properties>

 

<dependencies>

<dependency>

<groupId>org.apache.httpcomponents</groupId>

<artifactId>httpclient</artifactId>

<version>${httpclient_version}</version>

</dependency>

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>fastjson</artifactId>

<version>${fastjson_version}</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>slf4j-api</artifactId>

<version>${slf4j_version}</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>jcl-over-slf4j</artifactId>

<version>${slf4j_version}</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>slf4j-log4j12</artifactId>

<version>${slf4j_version}</version>

</dependency>

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>javax.servlet-api</artifactId>

<version>${servlet-api_version}</version>

</dependency>

<dependency>

<groupId>commons-io</groupId>

<artifactId>commons-io</artifactId>

<version>${commons-io_version}</version>

</dependency>

<!-- SPRING FRAMEWORK -->

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-context</artifactId>

<version>${spring_version}</version>

</dependency>

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-webmvc</artifactId>

<version>${spring_version}</version>

</dependency>

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-test</artifactId>

<version>${spring_version}</version>

<scope>test</scope>

</dependency>

<!-- END SPRING FRAMEWORK -->

<!-- SPRING SECURITY -->

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-core</artifactId>

<version>${security.version}</version>

<scope>compile</scope>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-config</artifactId>

<version>${security.version}</version>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-taglibs</artifactId>

<version>${security.version}</version>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-cas-client</artifactId>

<version>${cas-client.version}</version>

<exclusions>

<exclusion>

<artifactId>cas-client-core</artifactId>

<groupId>org.jasig.cas</groupId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>org.jasig.cas.client</groupId>

<artifactId>cas-client-core</artifactId>

<version>${cas-client-core.version}</version>

<exclusions>

<exclusion>

<artifactId>servlet-api</artifactId>

<groupId>javax.servlet</groupId>

</exclusion>

</exclusions>

</dependency>

<!-- END SPRING SECURITY -->

<dependency>

<groupId>commons-lang</groupId>

<artifactId>commons-lang</artifactId>

<version>${commons-lang.version}</version>

</dependency>

</dependencies>

 

需要注意版本号的选择

<spring_version>4.0.2.RELEASE</spring_version>

<security.version>3.2.3.RELEASE</security.version>

<cas-client.version>3.0.8.RELEASE</cas-client.version>

<cas-client-core.version>3.1.11</cas-client-core.version>

 

 这些版本号是线上环境使用的,需要注意 

1 )cas-client.version是3.0.8 并不是3.2.3 ,并没有和security版本保持一致

2 )security的版本和spring的版本并不是一致的,存在兼容关系,security是构建在spring core之上的框架

   security和spring的版本选择不当,会导致不能工作。

 

2 web.xml中配置spring-serurity提供的filter,并配置spring WebXmlApplicationContext加载spring-security.xml

 

<context-param>

<param-name>contextConfigLocation</param-name>

<param-value>

classpath:applicationContext.xml

classpath:spring-security.xml

</param-value>

</context-param>

 

<listener>

<listener-class>

org.springframework.web.context.ContextLoaderListener

</listener-class>

</listener>

 

   <filter>  

       <filter-name>CAS Single Sign Out Filter</filter-name>  

       <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>  

    </filter>  

<filter-mapping>  

   <filter-name>CAS Single Sign Out Filter</filter-name>  

   <url-pattern>/*</url-pattern>

</filter-mapping>

 

<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>*.html</url-pattern>

<url-pattern>/j_spring_cas_security_check</url-pattern>

</filter-mapping>

 

<servlet>

<servlet-name>enterprise</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<load-on-startup>1</load-on-startup>

<init-param>

<param-name>contextConfigLocation</param-name>

<param-value>

classpath:spring-controllers.xml

</param-value>

</init-param>

</servlet>

 

 

<servlet>

<servlet-name>enterpriseApi</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<load-on-startup>1</load-on-startup>

<init-param>

<param-name>contextConfigLocation</param-name>

<param-value>

classpath:spring-controllers-api.xml

</param-value>

</init-param>

</servlet>

 

<servlet-mapping>

<servlet-name>enterprise</servlet-name>

<url-pattern>*.html</url-pattern>

</servlet-mapping>

 

<servlet-mapping>

<servlet-name>enterpriseApi</servlet-name>

<url-pattern>/api/*</url-pattern>

</servlet-mapping>

  

3 配置spring-security.xml (根据需要决定是否使用CAS登录)

 

带CAS登录的版本(部分):

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" 

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 

xmlns:security="http://www.springframework.org/schema/security"

xsi:schemaLocation="http://www.springframework.org/schema/beans 

http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 

http://www.springframework.org/schema/security 

http://www.springframework.org/schema/security/spring-security-3.2.xsd"

>

 

<bean id="casConfigLoader" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="locations">

<list>

<value>classpath:cas.properties</value>

</list>

</property>

</bean>

 

<security:http auto-config="true" use-expressions="true" entry-point-ref="casEntryPoint" access-denied-page="/error.htm">

<security:intercept-url pattern="/merchantUser/toMerchantAssetDatails*" access="hasAnyRole('ROLE_COMPANY_MERCHANT','ROLE_PERSONAL_MERCHANT')" /> 

......................

<security:intercept-url pattern="/**/*" access="permitAll" />

<security:anonymous />

<security:logout logout-url="/account/logout.html" logout-success-url="/account/logoutSuccess.html" />

<security:custom-filter position="CAS_FILTER" ref="casFilter" />

<security:custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="dynamicAuthenticationFilter" />

<security:request-cache ref="myRequestCache" />

</security:http>

 

<security:authentication-manager alias="authenticationManager">   鉴权管理器

    <security:authentication-provider ref="casAuthenticationProvider" />      鉴权提供者

  </security:authentication-manager>

 

<bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">

    <property name="service" value="${cas.securityContext.serviceProperties.service}"/>   cas server客户端地址,CAs server登录成功后,会重新将浏览器重定向到此地址,

    <property name="sendRenew" value="false"/>

  </bean>

 

<bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">   自定义CasFilter

  <property name="authenticationManager" ref="authenticationManager"/>

</bean>

 

<bean id="myRequestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache">

<property name="requestMatcher">

<bean class="org.springframework.security.web.util.matcher.RegexRequestMatcher">  -- 自定义RequestMatcher

<constructor-arg value="^((?!/usercenter/message/unread/jsonp.html).)*$" index="0"></constructor-arg>

<constructor-arg value="GET" index="1"></constructor-arg>

</bean>

</property>

</bean>

 

<bean id="casEntryPoint" class="XXXXX">  -  自定义入口,此入口处理AuthenticationException,区分ajax,页面请求,鉴权失败(匿名用户身份访问被拒绝时)将页面重定向到CAS server

  <property name="loginUrl" value="${cas.securityContext.casProcessingFilterEntryPoint.loginUrl}"/>

  <property name="bizLoginUrl" value="${cas.securityContext.casProcessingFilterEntryPoint.bizLoginUrl}"/>

  <property name="beeLoginUrl" value="${cas.securityContext.casProcessingFilterEntryPoint.beeLoginUrl}"/>

  <property name="serviceProperties" ref="serviceProperties"/>

  <property name="checkUserUrl" value="/account/isUserSign.html"/>

</bean>

 

<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">

    <property name="userDetailsService" ref="userDetailsService"/>

    <property name="serviceProperties" ref="serviceProperties" />  

    <property name="ticketValidator">

      <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">

        <constructor-arg index="0" value="${cas.securityContext.ticketValidator.casServerUrlPrefix}" />

        </bean>

    </property>

    <property name="key" value="password"/>

  </bean>

 

<bean id="userDetailsService" class="XXXX">  -- 装入用户的角色,用于权限管理

  <property name="redisUtil" ref="redisUtil" />

  </bean>

 

<bean id="customLogoutSuccessHandler" class="XXX">

  <property name="redisUtil" ref="redisUtil" />

  </bean>

 

自定义filter

<bean id="dynamicAuthenticationFilter" class="com.enterprise.web.security.DynamicAuthenticationFilter">

    <!-- key property is used to construct CasAuthenticationToken,this attribute must follow the key definition in casAuthenticationProvider element -->

   <property name="key" value="password"/>

   <property name="dynamicAuthorityProvider" ref="dynamicAuthorityManager"/>

</bean>

<bean id="dynamicAuthorityManager" class="com.enterprise.web.security.DynamicAuthorityManager">

<property name="redisUtil" ref="redisUtil" />

</bean>

</beans>

 

 Cas.properties:

cas.securityContext.serviceProperties.service=http://XX/j_spring_cas_security_check  j_spring_cas_security_check 此URL后缀不可以修改,是authenticationURL

cas.securityContext.serviceProperties.service.ba=http://XXX/j_spring_cas_security_check

cas.securityContext.casProcessingFilterEntryPoint.loginUrl=https://passportXX/cas/XX

cas.securityContext.casProcessingFilterEntryPoint.bizLoginUrl=https://XX/cas/bizLogin

cas.securityContext.casProcessingFilterEntryPoint.beeLoginUrl=https://XX/cas/wapBeeLogin

cas.securityContext.ticketValidator.casServerUrlPrefix=http://passport.XX/cas

.....

 

 不使用Cas登录,一份可能的配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 

    xmlns:security="http://www.springframework.org/schema/security"

xsi:schemaLocation="http://www.springframework.org/schema/beans 

  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

http://www.springframework.org/schema/security 

http://www.springframework.org/schema/security/spring-security-3.2.xsd">

 

<security:http auto-config="true" use-expressions="true" access-denied-page="/auth/denied">

<security:intercept-url pattern="/auth/login" access="permitAll"/>

<security:intercept-url pattern="/main/admin" access="hasRole('ROLE_ADMIN')"/>

<security:intercept-url pattern="/main/common" access="hasRole('ROLE_USER')"/>

<security:intercept-url pattern="/*/**" access="permitAll"/>

<!-- login-page:指定登录页面  -->

<security:form-login

login-page="/auth/login" 

authentication-failure-url="/auth/login?error=true" 

default-target-url="/main/common"/>

<security:logout 

invalidate-session="true" 

logout-success-url="/auth/login" 

logout-url="/auth/logout"/>

</security:http>

 

<!-- 指定一个自定义的authentication-manager用于鉴权用户的身份 :customUserDetailsService -->

<security:authentication-manager alias="authenticationManager">

   <security:authentication-provider user-service-ref="customUserDetailsService">

        <security:password-encoder ref="passwordEncoder"/>

       </security:authentication-provider> 

</security:authentication-manager>

 

<!-- 对密码进行MD5编码 -->

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

<bean id="customUserDetailsService" class="com.XX.CustomUserDetailsService"/>

</beans>

 

public class CustomUserDetailsService implements UserDetailsService { // DAO 和service都应当抽象出接口以解耦具体实现

protected static Logger logger = LoggerFactory.getLogger(CustomUserDetailsService.class);

 

private final UserDao userDAO = new UserDao();

 

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

UserDetails user = null;

try {

DbUser dbUser = userDAO.qryUser(username);

// Populate the Spring User object with details from the dbUser  

// Here we just pass the username, password, and access level ,getAuthorities() will translate the access level to the correct role type  

user = new User(dbUser.getUsername(), dbUser.getPassword().toLowerCase(), true, true, true, true, getAuthorities(dbUser.getAccess()));

} catch (Exception e) {

logger.error("Error in retrieving user");

throw new UsernameNotFoundException("Error in retrieving user");

}

return user;

}

 

-------------

思想,架构,模块,流程

 

Spring security的核心功能是“登录”  和“鉴权”

登录:可以是cas登录,或者是非Cas

鉴权:用于判断当前用户(没有登录时认为是匿名用户)是否有权限访问受保护的资源

 

Spring security的工作流程简单描述:

Spring security提供的登录和鉴权能力通过一组filter实现,tomcat启动时,触发spring security  beans的加载,配置,织入,webapp在收到匹配的请求后,spring security 内置的filter会逐一进行处理请求,验证用户是否已经登录,没有登录则重定向到登录界面,如果已经登录则验证用户是否有访问指定资源(URL)的权限,没有就返回accessDenied,否则继续向下处理,spring securiy处理流程结束

 

---------------------------------------------

Security 命名空间的使用和解析


基于Spring security的认证和权限管理流程_第1张图片
 


基于Spring security的认证和权限管理流程_第2张图片
 

handler class:org.springframework.security.config.SecurityNamespaceHandler

  

基于Spring security的认证和权限管理流程_第3张图片
 

HttpSecurityBeanDefinitionParse解析http元素,注册FilterChainProxy的别名为springSecurityFilerChain。

所以filter org.springframework.web.filter.DelegatingFilterProxy必须将filterName设置为springSecurityFilerChain,否则会找不到deleagte


基于Spring security的认证和权限管理流程_第4张图片
 

 security-httpParser,解析filterChainBean

基于Spring security的认证和权限管理流程_第5张图片
 filterChainProxy依赖XXSecurityFilterChain注入chainList

 
基于Spring security的认证和权限管理流程_第6张图片

 

 


基于Spring security的认证和权限管理流程_第7张图片
 

 

 parseHandler完成了security标签的解析后,spring容器将根据beanDefinition实例化bean,注入依赖,被实例化的bean包括security内置的一组filter,这些filter以链的形式按序处理匹配的request

 

---------------------------------------

 

核心类分析:filter,facilities

 

DelegatingFilterProxy

 <filter>

<filter-name>springSecurityFilterChain</filter-name>

<filter-class>org.springframework.web.filter.DelegatingFilterProxy

</filter-class>

</filter>

此filter代理的是org.springframework.security.filterChainProxy,

tomcat启动时,加载此filter,注入filterChainProxy的实例。


基于Spring security的认证和权限管理流程_第8张图片
 


基于Spring security的认证和权限管理流程_第9张图片
 

FilterChainProxy

 

public class FilterChainProxy extends GenericFilterBean  这个类是是一个filter

FilterChainProxy的能力是根据request,securityFilterChains获得与当前请求匹配的fiilter列表(此filter列表可以通过securityFilterChain进行定制),然后根据originalChain,filters,fwRequest,构造VirtualFilterChain,然后将请求转发给VirtualFilterChain进行处理

 

  

基于Spring security的认证和权限管理流程_第10张图片
 

 

VirtualFilterChain  

将filter list(默认是springSecurity提供的内置filter)串在一起,调度filter处理请求。

这个类的设计可以参照tomcat的FilterChain,几乎是完全一致的,需要在内部维护一个list(array)

,size,currentPosition,且chain每次处理一个新的请求都必须重建

基于Spring security的认证和权限管理流程_第11张图片
 

 默认的filter列表(可扩展)如下:

 

org.springframework.security.web.context.SecurityContextPersistenceFilter@c13a68

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@1e1b271

org.springframework.security.web.authentication.logout.LogoutFilter@8a49e4

org.springframework.security.cas.web.CasAuthenticationFilter@f9462c

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@16e09c5

org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@14375a8

org.springframework.security.web.authentication.www.BasicAuthenticationFilter@1e1890a

org.springframework.security.web.savedrequest.RequestCacheAwareFilter@1ec23c7

org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@20fb86

org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1613a47

org.springframework.security.web.session.SessionManagementFilter@394057

org.springframework.security.web.access.ExceptionTranslationFilter@af2048

org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4c2b5d

 
基于Spring security的认证和权限管理流程_第12张图片
 

--------------------------------------------------------------------------

Security 内置filter分析,当前研究的版本是3.2.3

 

---------------------
SecurityContextPersistenceFilter

 

Populates the SecurityContextHolder with information obtained from the configured SecurityContextRepository prior to the request and stores it back in the repository once the request has completed and clearing the context holder. By default it uses an HttpSessionSecurityContextRepository. See this class for information HttpSession related configuration options. 

 

This filter will only execute once per request, to resolve servlet container (specifically Weblogic) incompatibilities. 

 

This filter MUST be executed BEFORE any authentication processing mechanisms. Authentication processing mechanisms (e.g. BASIC, CAS processing filters etc) expect the SecurityContextHolder to contain a valid SecurityContext by the time they execute. 

 

This is essentially a refactoring of the old HttpSessionContextIntegrationFilter to delegate the storage issues to a separate strategy, allowing for more customization in the way the security context is maintained between requests. 

 

The forceEagerSessionCreation property can be used to ensure that a session is always available before the filter chain executes (the default is false, as this is resource intensive and not recommended).

 

 功能分析:持久化相关,从session里面获取SecurityContext,如果没则创建一个emptyContext,将securityContext set到threadLoacal便于后续读取, 向下分发请求后,在finally中移除threadLocal,如果context发生了改变(比如后续通过了UserNameLogin或者Cas登录),则替换session中的securityContext.

 

基于Spring security的认证和权限管理流程_第13张图片
 

-----------------------------------

 WebAsyncManagerIntegrationFilter

 

Provides integration between the SecurityContext and Spring Web's WebAsyncManager by using the SecurityContextCallableProcessingInterceptor.beforeConcurrentHandling(org.springframework.web.context.request.NativeWebRequest, Callable) to populate the SecurityContext on the Callable.

 

功能不详,不影响对和核心流程的理解

 

-------------------------------------

LogoutFilter

 

Logs a principal out.

Polls a series of LogoutHandlers. The handlers should be specified in the order they are required. Generally you will want to call logout handlers TokenBasedRememberMeServices and SecurityContextLogoutHandler (in that order).

After logout, a redirect will be performed to the URL determined by either the configured LogoutSuccessHandler or the logoutSuccessUrl, depending on which constructor was used.

 

作用:登出,判断当前URL是否是登出URL,如果是执行登出操作,登出如果能执行成功,则跳转到logoutSuccessUrl

 

security 配置: 

<security:logout logout-url="/account/logout.html" logout-success-url="/account/logoutSuccess.html" />


基于Spring security的认证和权限管理流程_第14张图片
 

logoutHandler 的默认实现是SecurityContextLogoutHandler,此handler会让当前session失效,并清除SecurityContextHolder中存放的context.

 

基于Spring security的认证和权限管理流程_第15张图片
 

logoutSucessHandler的默认实现是SimpleUrlLogoutSuccessHandler,此handler会触发rediectTo logoutSucessURL.

 

关于登出,如果使用了cas SSO,则在登出时,用户需要在cas server登出,同时所有关联的子系统都必须同步登出(都必须得到通知)

 

---------------------------------------------

CasAuthenticationFilter

 

Processes a CAS service ticket.

A service ticket consists of an opaque ticket string. It arrives at this filter by the user's browser successfully authenticating using CAS, and then receiving a HTTP redirect to a service. The opaque ticket string is presented in the ticket request parameter. This filter monitors the service URL so it can receive the service ticket and process it. The CAS server knows which service URL to use via the ServiceProperties.getService() method.

 

作用:处理casServer返回的ticket,cas server在处理了用户的登录后,会将客户端浏览器重定向到类似于

cas.securityContext.serviceProperties.service=http://XXX/j_spring_cas_security_check这样的地址,并附着serviceTicket更在后面,CasAuthenticationFilter 识别j_spring_cas_security_check(前提条件)和serviceTicket,到cas server校验ticket是否由casServer办法,如果鉴别通过,则说明用户的身份是合法的


基于Spring security的认证和权限管理流程_第16张图片
 

 

 AbstractAuthenticationProcessingFilter  定义了鉴权处理流程,可以作为Template Method(模板方法模式)进行理解,此方法有两个子类

 


基于Spring security的认证和权限管理流程_第17张图片

 

核心方法:

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)

            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;

        HttpServletResponse response = (HttpServletResponse) res;

        if (!requiresAuthentication(request, response)) {

            chain.doFilter(request, response);

            return;

        }

        if (logger.isDebugEnabled()) {

            logger.debug("Request is to process authentication");

        }

 

        Authentication authResult;

        try {

            authResult = attemptAuthentication(request, response);

            if (authResult == null) {

                // return immediately as subclass has indicated that it hasn't completed authentication

                return;

            }

            sessionStrategy.onAuthentication(authResult, request, response);

        } catch(InternalAuthenticationServiceException failed) {

            logger.error("An internal error occurred while trying to authenticate the user.", failed);

            unsuccessfulAuthentication(request, response, failed);

            return;

        }

        catch (AuthenticationException failed) {

            // Authentication failed

            unsuccessfulAuthentication(request, response, failed);

            return;

        }

 

        // Authentication success

        if (continueChainBeforeSuccessfulAuthentication) {

            chain.doFilter(request, response);

        }

 

        successfulAuthentication(request, response, chain, authResult); 

    }

 

CasAuthenticationFilter核心代码分析:

1 区分stateFul和stateless,有状态和无状态

 

    /** Used to identify a CAS request for a stateful user agent, such as a web browser. */

    public static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";

 

    /**

     * Used to identify a CAS request for a stateless user agent, such as a remoting protocol client (e.g.     * Hessian, Burlap, SOAP etc). Results in a more aggressive caching strategy being used, as the absence of a

     * <code>HttpSession</code> will result in a new authentication attempt on every request.

     */

    public static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";

 

 2  attemptAuthentication() 获取cas server返回的ticket,然后到cas server鉴权,鉴权通过后,根据server返回的username,通过userService加载user信息,构造casAuthenticationToken。

 

(1)cas server鉴权通过后,返回了tikcet

 (2)发http请求到cas server,validate cas ticket,需要拼接validateToken URL

基于Spring security的认证和权限管理流程_第18张图片
 

cas server返回的xml信息中包括用户名(至少我现在用的这个server,返回的是xml而非json)

<cas:serviceResponse

xmlns:cas='http://www.yale.edu/tp/cas'>

<cas:authenticationSuccess>

<cas:user>zfg152543</cas:user>

<cas:attributes>

<cas:id>1231123907</cas:id>

</cas:attributes>

</cas:authenticationSuccess>

</cas:serviceResponse>

 

(3)  cas client根据server返回的userName,调用userService加载user信息,构造CasAutehtincationToken,完成对用户的识别


基于Spring security的认证和权限管理流程_第19张图片
 

   

cas client完成了用户身份的识别 后,需要将用户重定向到之前访问的地址并将token保存到session,这通过

AbstractAuthenticationProcessingFilter/successfulAuthentication(request, response, chain, authResult);实现:

基于Spring security的认证和权限管理流程_第20张图片
 

redirect:
基于Spring security的认证和权限管理流程_第21张图片
 

 spring security已经对response进行了包装,delegate redirect之前,会先触发doSaveContext


基于Spring security的认证和权限管理流程_第22张图片
 

 SaveContextOnUpdateOrErrorResponseWrapper/doSaveContext()

    /**

     * Calls <code>saveContext()</code> with the current contents of the

     * <tt>SecurityContextHolder</tt> as long as

     * {@link #disableSaveOnResponseCommitted()()} was not invoked.

     */

    private void doSaveContext() {

        if(!disableSaveOnResponseCommitted) {

            saveContext(SecurityContextHolder.getContext());  //将context保存到session,后续请求可 以基于session中的SecurityContext进行鉴权和处理了

            contextSaved = true;

        } else if(logger.isDebugEnabled()){

            logger.debug("Skip saving SecurityContext since saving on response commited is disabled");

        }

    }

===============================

 因为AbstractAuthenticationProcessingFilter/doFilter 的最后没有调用chain.doFilter(),至此cas登录流程完成,后续请求将直接基于session中存放的casAuthentiocationToken进行鉴权,无需继续和casServer进行交互(大多数场景下)

================================

 

UsernamePasswordAuthenticationFilter

 

Processes an authentication form submission. Called AuthenticationProcessingFilter prior to Spring Security 3.0.

Login forms must present two parameters to this filter: a username and password. The default parameter names to use are contained in the static fields SPRING_SECURITY_FORM_USERNAME_KEY and SPRING_SECURITY_FORM_PASSWORD_KEY. The parameter names can also be changed by setting the usernameParameter and passwordParameter properties.

This filter by default responds to the URL /j_spring_security_check.

 

作用:在没有配置CAS登录的前提下,处理form提交的username和password. 这个filter在CasFilter之后,如果已经启用了Cas登录,因为URL:/j_spring_security_check已经被CasFilter处理,此filter可以忽略。

 

一个可能的form:

基于Spring security的认证和权限管理流程_第23张图片
 

如果没有配置Cas登录,此时返回的filter列表中不包括CasFilter,此UsernamePasswordAuthenticationFilter会处理j_spring_security_check,此时的filter列表如下:

org.springframework.security.web.context.SecurityContextPersistenceFilter@16fbcdb

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@d7e31d

org.springframework.security.web.authentication.logout.LogoutFilter@17fb9aa

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@767cfb

org.springframework.security.web.authentication.www.BasicAuthenticationFilter@1077d72

org.springframework.security.web.savedrequest.RequestCacheAwareFilter@bce8b8

org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@52b277

org.springframework.security.web.authentication.AnonymousAuthenticationFilter@142dc3d

org.springframework.security.web.session.SessionManagementFilter@894c51

org.springframework.security.web.access.ExceptionTranslationFilter@1f16ea1

org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1963401

 

 缺少的两个filter是:

org.springframework.security.cas.web.CasAuthenticationFilter@f9462c

org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@14375a8

 

一段配置:

<security:http auto-config="true" use-expressions="true" access-denied-page="/auth/denied">

<security:intercept-url pattern="/auth/login" access="permitAll"/>

<security:intercept-url pattern="/main/admin" access="hasRole('ROLE_ADMIN')"/>

<security:intercept-url pattern="/main/common" access="hasRole('ROLE_USER')"/>

<security:intercept-url pattern="/*/**" access="permitAll"/>

<!-- login-page:指定登录页面  -->

<security:form-login

login-page="/auth/login"   // 登录页面的URL

authentication-failure-url="/auth/login?error=true"   // 验证用户的身份失败后需要跳转到的页面

default-target-url="/main/common"/>  // 验证客户身份后默认跳转的界面

<security:logout 

invalidate-session="true"  // invalidate-session:指定在退出系统时是否要销毁Session

logout-success-url="/auth/login"  // 登出成功后需要跳转到的页面

logout-url="/auth/logout"/>  // 登出URL

</security:http>

 

<!-- 指定一个自定义的authentication-manager用于鉴权用户的身份 :customUserDetailsService -->

<security:authentication-manager alias="authenticationManager">

   <security:authentication-provider user-service-ref="customUserDetailsService">

       <security:password-encoder ref="passwordEncoder"/>

       </security:authentication-provider> 

</security:authentication-manager>

 

<!-- 对密码进行MD5编码 -->

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

<bean id="customUserDetailsService" class="com.qbao.microbiz.service.CustomUserDetailsService"/>

 

 UsernamePasswordAuthenticationFilter/attemptAuthentication 流程分析:

1  obtainUsername,obtainPassword

2 AbstractUserDetailsAuthenticationProvider:authenticate,通过userDetailsService加载user,

 (  如果userDetailsService无法装入user,应当抛出UsernameNotFoundException,AuthenticationProvider 会捕获此异常抛出之),user加载后,判断用户的密码是否正确

        String presentedPassword = authentication.getCredentials().toString();

 

        if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) 如果密码错误,则抛出BadCredentialsException

 

===================================================

UsernamePasswordAuthenticationFilter和CasAuthenticationFilter的区别和联系

1  识别用户的方式不同,

CasAuthenticationFilter需要访问Cas server,校验ticket,然后根据返回的username,构造user信息

UsernamePasswordAuthenticationFilter 直接在本地校验用户名和密码是否正确

2 相同之处

鉴权流程都是由AbstractAuthenticationProcessingFilter负责定义,只是attempAuthentication的实现不同.

流程: accept --》 try auth ---》sucess ----》 sucessHandler处理: 跳转到前一个受保护的资源

                                           ---》 fail  -----》failureHandler处理:跳转到登录失败界面或者sendError

 

========================================================

补充认证失败的处理  AbstractAuthenticationProcessingFilter/unsuccessfulAuthentication()


基于Spring security的认证和权限管理流程_第24张图片
 

failuerHandler引导客户端条跳转到错误界面或者向客户端发送错误
基于Spring security的认证和权限管理流程_第25张图片
 

--------------------------------------------------------------------------------------------------------------------------------

                                  故常无欲以观其妙,常有欲以观其徼

-------------------------------------------------------------------------------------------------------------------------------

 

 DefaultLoginPageGeneratingFilter

作用:  generateLoginPageHtml(request, loginError, logoutSuccess);然后推送给客户端

此filter几乎没用,生成页面需要考虑友好统一的样式和布局,自动生成的界面完全无法满足要求

 

     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)

            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;

        HttpServletResponse response = (HttpServletResponse) res;

 

        boolean loginError = isErrorPage(request);

        boolean logoutSuccess = isLogoutSuccess(request);

        if (isLoginUrlRequest(request) || loginError || logoutSuccess) {

            String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);

            response.setContentType("text/html;charset=UTF-8");

            response.setContentLength(loginPageHtml.length());

            response.getWriter().write(loginPageHtml);

 

            return;

        }

 

        chain.doFilter(request, response);

    }

 

------------------------------

BasicAuthenticationFilter

 

Processes a HTTP request's BASIC authorization headers, putting the result into the SecurityContextHolder. 

 

For a detailed background on what this filter is designed to process, refer to RFC 1945, Section 11.1. Any realm name presented in the HTTP request is ignored. 

 

In summary, this filter is responsible for processing any request that has a HTTP request header of Authorization with an authentication scheme of Basic and a Base64-encoded username:password token. For example, to authenticate user "Aladdin" with password "open sesame" the following header would be presented: 

 

作用:如果request携带Authorization header,则处理之。

具体到底要怎么用,待研究,Cas登录和正常的userNameAuth 可以忽略此filter

DoFilter:

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)

            throws IOException, ServletException {

        final boolean debug = logger.isDebugEnabled();

        final HttpServletRequest request = (HttpServletRequest) req;

        final HttpServletResponse response = (HttpServletResponse) res;

 

        String header = request.getHeader("Authorization");

 

        if (header == null || !header.startsWith("Basic ")) {

            chain.doFilter(request, response);  //  cas,usernameauth,here 

            return;

        }

 

-----------------------------------------------------

 RequestCacheAwareFilter

 

Responsible for reconstituting the saved request if one is cached and it matches the current request.

It will call getMatchingRequest on the configured RequestCache. If the method returns a value (a wrapper of the saved request), it will pass this to the filter chain's doFilter method. If null is returned by the cache, the original request is used and the filter has no effect.

 

作用:从缓存(httpSession)中取出上一次访问时使用的request,如果有,则用此request替换当前request,继续向下处理

 

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

            throws IOException, ServletException {

 

        HttpServletRequest wrappedSavedRequest =

            requestCache.getMatchingRequest((HttpServletRequest)request, (HttpServletResponse)response);

 

        chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest, response);

    }

 

think:

1 XXAware是一种设计思想,think ApplicationContextAware接口的作用

2 requestCache的设计基于session,

     取出SPRING_SECURITY_SAVED_REQUEST后,可以选择是否需要将其从session中移除, remind “last”

   
基于Spring security的认证和权限管理流程_第26张图片
 

  save request,在鉴权失败时,spring security会把此请求保存起来,供后面鉴权通过后使用和后续恢复请求

  
基于Spring security的认证和权限管理流程_第27张图片
 

 

思考:为什么需要RequestCacheAwareFilter这个filter,明明usernameAuthFilter或者CasFilter在认证成功之后,已经拿到了之前访问的request,并重新redirect到targetURL了。

Answer: 这个filter是为了实现请求恢复, 虽然在认证通过后,server会redirect浏览器,重新访问targetURL(受保护的资源),浏览器重新发起的请求较之最初的请求地址相同,但可能会存在缺失参数的情况,比方说之前的请求是post提交,通过此filter,我们可以将后面的这个请求替换为原先(original)的那个,如是则实现了请求的中继(接着处理最初的请求)

 

----------------------------------------------------

现在我们终于可以知道为什么savedRequestAwareAuthenticationSuccessHandler执行了get但是并没有将lastRequest移除了。

 

基于Spring security的认证和权限管理流程_第28张图片
 

-----------------------------------------------------------------------------

一个发散性思考:

1 为什么spring security在认证通过后,一定要重新redirect,而不是立刻将castoken---> securityContext 保存到session,然后立即重新接着之前的处理位置,重新执行原因的请求呢?

我的思考: 1  redirect会触发请求的重新请求,相当于如下的一个完整场景:

                        用户已经登录,然后客户端发起了一个建立在用户已经登录的前提下的请求

                   2 用户认证后,做一次redirect并不会对性能造成影响

 

总之: 一个设计一定有它自身的道理,虽然你可能并不知道why,或者并不认可它

--------------------------------------------------------------------------------

 

 

 SecurityContextHolderAwareRequestFilter

作用:SecurityContextHolderAware,可感知到SecurityContext,在运行时用 Servlet3SecurityContextHolderAwareRequestWrapper替换进入filter时传入的request.然后将请求向下分发。

 

核心代码:

基于Spring security的认证和权限管理流程_第29张图片
 

HttpSevlet3RequestFactory

此factory负责根据originRequest,构造期待的wrapper

 

基于Spring security的认证和权限管理流程_第30张图片
 

需要关注Servlet3SecurityContextHolderAwareRequestWrapper对应的层次结构

 

Servlet3SecurityContextHolderAwareRequestWrapper hierarchy,需要考虑异步特征

 

基于Spring security的认证和权限管理流程_第31张图片
 
SecurityContextHolderAwareRequestWrapper:

 

A Spring Security-aware HttpServletRequestWrapper, which uses the SecurityContext-defined Authentication object to implement the servlet API security methods:

  • getUserPrincipal()
  • SecurityContextHolderAwareRequestWrapper.isUserInRole(String)
  • HttpServletRequestWrapper.getRemoteUser().

------------------------

摘几个方法:

org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper.getRemoteUser()

 

private boolean isGranted(String role) {     // 观这个方法,健壮,清晰

    Authentication auth = getAuthentication();

 

    if( rolePrefix != null ) {

        role = rolePrefix + role;

    }

 

    if ((auth == null) || (auth.getPrincipal() == null)) {

        return false;

    }

 

    Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();

 

    if (authorities == null) {

        return false;

    }

 

 

    for (GrantedAuthority grantedAuthority : authorities) {

        if (role.equals(grantedAuthority.getAuthority())) {

            return true;

        }

    }

 

    return false;

}

 

 

===================================================================================

在应用层将request转成SecurityContextHolderAwareRequestWrapper:

 

前面已经说了SecurityContextHolderAwareRequestFilter用 Servlet3SecurityContextHolderAwareRequestWrapper替换了进入filter时传入的request.然后将wrapper向下分发,事实上此filter在后续会一直被传输到应用层,这就是为什么在应用层可以做强转的原因。

 

demo:


基于Spring security的认证和权限管理流程_第32张图片

===================================================================================

 

思考Wrapper这种模式:

wrapper其实就是Decorator模式(和proxy非常的类似,在我看来唯一的区别就是proxy模式里面不写被包装的对象,proxy模式可用于代理远程服务),通过包装,我们可以在被包装的对象的原有功能上,加上新的功能,比如preHandle,postHandle,加入逻辑判断,加方法等等,经过层层的包装,最后暴露给外部使用的对象功能会越来越强大,但是访问方式保持不变。

 

===================================================================================

 

AnonymousAuthenticationFilter

 

Detects if there is no Authentication object in the SecurityContextHolder, and populates it with one if needed.

 

作用:通过SecurityContextHolder取不到authentication(代表当前已经登录的用户)时,构造AnonymousAuthenticationToken放到SecurityContext,后续将基于匿名用户身份进行鉴权(此时我们认为当前登录的用户是匿名用户)。

 


基于Spring security的认证和权限管理流程_第33张图片
 

在用户没有登录的情况下,从session中取出来的用户名是anyomousUser,guest?


基于Spring security的认证和权限管理流程_第34张图片

 

我一直以为使用了CAS后,拿到的token都是CasAuthenticationToken,看来是我想错了
 

===============================================

SessionManagementFilter

session安全机制

 

 

/**
 * Detects that a user has been authenticated since the start of the request and, if they have, calls the
 * configured {@link SessionAuthenticationStrategy} to perform any session-related activity such as
 * activating session-fixation protection mechanisms or checking for multiple concurrent logins.
 *
 * @author Martin Algesten
 * @author Luke Taylor
 * @since 2.0
 */
public class SessionManagementFilter extends GenericFilterBean {
    //~ Static fields/initializers =====================================================================================

    static final String FILTER_APPLIED = "__spring_security_session_mgmt_filter_applied";
 

 

===========================================

ExceptionTranslationFilter

基于Spring security的认证和权限管理流程_第35张图片
 

 

作用分析:

java异常和http应答之间的桥梁,后续的filter抛出异常后,如下处理

1 验证用户身份错误或者是(访问拒绝但是当前用户是AnoumosUser)

   --  通过authenticationEntryPoint进行处理,比如redirect到CAS登录,或者返回错误

2 访问拒绝,指的是用户试图访问未经授权的资源

  -- 通过accessDeniedHandler返回客户端错误,一般是返回出错页面

 

 

 

   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            chain.doFilter(request, response);

            logger.debug("Chain processed normally");
        }
        catch (IOException ex) {
            throw ex;
        }
        catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace
            Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
            RuntimeException ase = (AuthenticationException)
                    throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);

            if (ase == null) {
                ase = (AccessDeniedException)throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }

            if (ase != null) {
                handleSpringSecurityException(request, response, chain, ase);
            } else {
                // Rethrow ServletExceptions and RuntimeExceptions as-is
                if (ex instanceof ServletException) {
                    throw (ServletException) ex;
                }
                else if (ex instanceof RuntimeException) {
                    throw (RuntimeException) ex;
                }

                // Wrap other Exceptions. This shouldn't actually happen
                // as we've already covered all the possibilities for doFilter
                throw new RuntimeException(ex);
            }
        }
    }

 

 

  protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            AuthenticationException reason) throws ServletException, IOException {
        // SEC-112: Clear the SecurityContextHolder's Authentication, as the
        // existing Authentication is no longer considered valid
        SecurityContextHolder.getContext().setAuthentication(null);
        requestCache.saveRequest(request, response);
        logger.debug("Calling Authentication entry point.");
        authenticationEntryPoint.commence(request, response, reason);
    }

 

 

 One self Defined CasEntryPoint

package com.enterprise.web.security;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.jasig.cas.client.util.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.util.Assert;


public class HyipCasAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
	private static final String RESPONSE_TYPE_APPLICATION_JSON = "application/json";
	private static final String HEADER_RESPONSE_CONTENT_TYPE = "Response-Content-Type";
	private static final String JSON_MSG_INVALID_SESSION = "{\"success\":false,\"message\":\"session timeout,please login!\",\"returnCode\":-1000,\"data\":null}";
	private static final String SESSION_TIME_OUT_MSG = "{\"success\":false,\"message\":\"session timeout,please login!\",\"returnCode\":-1,\"data\":null}";
	private static final String JSON_MSG_INVALID_SESSION_NEW = "{\"message\":\"session timeout,please login!\",\"responseCode\":1004,\"data\":null}";
	private final static Logger LOG = LoggerFactory.getLogger(HyipCasAuthenticationEntryPoint.class);
   //~ Instance fields ================================================================================================
   private ServiceProperties serviceProperties;

   private String loginUrl;
   private String bizLoginUrl;
   private String beeLoginUrl;

   private String checkUserUrl;

   /**
    * Determines whether the Service URL should include the session id for the specific user.  As of CAS 3.0.5, the
    * session id will automatically be stripped.  However, older versions of CAS (i.e. CAS 2), do not automatically
    * strip the session identifier (this is a bug on the part of the older server implementations), so an option to
    * disable the session encoding is provided for backwards compatibility.
    *
    * By default, encoding is enabled.
    * @deprecated since 3.0.0 because CAS is currently on 3.3.5.
    */
   @Deprecated
   private boolean encodeServiceUrlWithSessionId = true;

   //~ Methods ========================================================================================================

   public void afterPropertiesSet() throws Exception {
       Assert.hasLength(this.loginUrl, "loginUrl must be specified");
       Assert.hasLength(this.bizLoginUrl, "bizLoginUrl must be specified");
       Assert.notNull(this.serviceProperties, "serviceProperties must be specified");
   }

   private boolean isNotCheckUserUrl(String url){
	   return checkUserUrl == null || !checkUserUrl.equalsIgnoreCase(url);
   }
   
   public final void commence(final HttpServletRequest servletRequest, final HttpServletResponse response,
           final AuthenticationException authenticationException) throws IOException, ServletException {
	   String requestURI = servletRequest.getServletPath();
       final String urlEncodedService = createServiceUrl(servletRequest, response);
       final String redirectUrl = createRedirectUrl(requestURI, urlEncodedService);

       preCommence(servletRequest, response);
       
       String type = servletRequest.getHeader(HEADER_RESPONSE_CONTENT_TYPE);
       LOG.info("invalid session for content type < " + type + ">" + " and url:" + requestURI);


       //新版客户端,2013-10-17.
       String client = servletRequest.getHeader("sourceType");
       LOG.info("client_sourceType:"+client);
       if(client!=null&&client.equals("client") && isNotCheckUserUrl(requestURI)){
           LOG.info("客户端请求session失效,需要登录"  + "\"");
//           response.setHeader("needTicket", "1");
//           response.setStatus(200);
           PrintWriter writer = response.getWriter();
           writer.write(JSON_MSG_INVALID_SESSION_NEW);
           writer.close();
           return;
       }


	   //web端ajax异步session失效跳转到登陆页面2014-02-18
       if(servletRequest.getHeader("x-requested-with")!=null   
               && servletRequest.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest")){   
    	   LOG.info("web端ajax异步调用session失效,需要登录");
    	   PrintWriter writer = response.getWriter();
           writer.write(SESSION_TIME_OUT_MSG);
           writer.close();
           return;
       }
       
       
    if (RESPONSE_TYPE_APPLICATION_JSON.equalsIgnoreCase(type) && isNotCheckUserUrl(requestURI)) {
        LOG.info("return json format data of session timeout.");
    	   PrintWriter writer = response.getWriter();
    	   writer.write(JSON_MSG_INVALID_SESSION);
    	   writer.close();
       }else{
    	   response.sendRedirect(redirectUrl);
       }
   }

   /**
    * Constructs a new Service Url.  The default implementation relies on the CAS client to do the bulk of the work.
    * @param request the HttpServletRequest
    * @param response the HttpServlet Response
    * @return the constructed service url.  CANNOT be NULL.
    */
   protected String createServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
       return CommonUtils.constructServiceUrl(null, response, this.serviceProperties.getService(), null, this.serviceProperties.getArtifactParameter(), this.encodeServiceUrlWithSessionId);
   }

   /**
    * Constructs the Url for Redirection to the CAS server.  Default implementation relies on the CAS client to do the bulk of the work.
    *
    * @param serviceUrl the service url that should be included.
    * @return the redirect url.  CANNOT be NULL.
    */
   protected String createRedirectUrl(final String requestURI, final String serviceUrl) {
	   //当用户停留在商家平台下的页面超时,需要跳转到商家登录页面,而不是会员登录页面
	   if (StringUtils.isNotBlank(requestURI) && requestURI.indexOf("merchantUser") != -1) {
		   return CommonUtils.constructRedirectUrl(this.bizLoginUrl, this.serviceProperties.getServiceParameter(), serviceUrl, this.serviceProperties.isSendRenew(), false);
	   } else if(StringUtils.isNotBlank(requestURI) && requestURI.indexOf("/bee/") != -1) {
		   return CommonUtils.constructRedirectUrl(this.beeLoginUrl, this.serviceProperties.getServiceParameter(), serviceUrl, this.serviceProperties.isSendRenew(), false);
	   } else {
		   return CommonUtils.constructRedirectUrl(this.loginUrl, this.serviceProperties.getServiceParameter(), serviceUrl, this.serviceProperties.isSendRenew(), false);
	   }
       
   }

   /**
    * Template method for you to do your own pre-processing before the redirect occurs.
    *
    * @param request the HttpServletRequest
    * @param response the HttpServletResponse
    */
   protected void preCommence(final HttpServletRequest request, final HttpServletResponse response) {

   }

   /**
    * The enterprise-wide CAS login URL. Usually something like
    * <code>https://www.mycompany.com/cas/login</code>.
    *
    * @return the enterprise-wide CAS login URL
    */
   public final String getLoginUrl() {
       return this.loginUrl;
   }

   public final ServiceProperties getServiceProperties() {
       return this.serviceProperties;
   }

   public final void setLoginUrl(final String loginUrl) {
       this.loginUrl = loginUrl;
   }
   
   public String getBizLoginUrl() {
		return bizLoginUrl;
   }

   public void setBizLoginUrl(String bizLoginUrl) {
		this.bizLoginUrl = bizLoginUrl;
   }
   
   public void setBeeLoginUrl(String beeLoginUrl) {
		this.beeLoginUrl = beeLoginUrl;
   }

   public void setCheckUserUrl(String checkUserUrl) {
	this.checkUserUrl = checkUserUrl;
}

   public final void setServiceProperties(final ServiceProperties serviceProperties) {
       this.serviceProperties = serviceProperties;
   }

   /**
    * Sets whether to encode the service url with the session id or not.
    *
    * @param encodeServiceUrlWithSessionId whether to encode the service url with the session id or not.
    * @deprecated since 3.0.0 because CAS is currently on 3.3.5.
    */
   @Deprecated
   public final void setEncodeServiceUrlWithSessionId(final boolean encodeServiceUrlWithSessionId) {
       this.encodeServiceUrlWithSessionId = encodeServiceUrlWithSessionId;
   }

   /**
    * Sets whether to encode the service url with the session id or not.
    * @return whether to encode the service url with the session id or not.
    *
    * @deprecated since 3.0.0 because CAS is currently on 3.3.5.
    */
   @Deprecated
   protected boolean getEncodeServiceUrlWithSessionId() {
       return this.encodeServiceUrlWithSessionId;
   }
}

 

 --------------------

 FilterSecurityInterceptor

判断用户能否访问指定的资源(通过URL标记)

 


基于Spring security的认证和权限管理流程_第36张图片
 

 
基于Spring security的认证和权限管理流程_第37张图片
 

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                && observeOncePerRequest) {
            // filter already applied to this request and user wants us to observe
            // once-per-request handling, so don't re-do security checking
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            // first time this request being called, so perform security checking
            if (fi.getRequest() != null) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }

            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, null);
        }
    }

 

 

public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware,

    protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, "Object was null");
        final boolean debug = logger.isDebugEnabled();

        if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
            throw new IllegalArgumentException("Security invocation attempted for object "
                    + object.getClass().getName()
                    + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                    + getSecureObjectClass());
        }

        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);

        if (attributes == null || attributes.isEmpty()) {
            if (rejectPublicInvocations) {
                throw new IllegalArgumentException("Secure object invocation " + object +
                        " was denied as public invocations are not allowed via this interceptor. "
                                + "This indicates a configuration error because the "
                                + "rejectPublicInvocations property is set to 'true'");
            }

            if (debug) {
                logger.debug("Public object - authentication not attempted");
            }

            publishEvent(new PublicInvocationEvent(object));

            return null; // no further work post-invocation
        }

        if (debug) {
            logger.debug("Secure object: " + object + "; Attributes: " + attributes);
        }

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
                    "An Authentication object was not found in the SecurityContext"), object, attributes);
        }

        Authentication authenticated = authenticateIfRequired();

        // Attempt authorization
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));

            throw accessDeniedException;
        }

        if (debug) {
            logger.debug("Authorization successful");
        }

        if (publishAuthorizationSuccess) {
            publishEvent(new AuthorizedEvent(object, attributes, authenticated));
        }

        // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);

        if (runAs == null) {
            if (debug) {
                logger.debug("RunAsManager did not change Authentication object");
            }

            // no further work post-invocation
            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
        } else {
            if (debug) {
                logger.debug("Switching to RunAs Authentication: " + runAs);
            }

            SecurityContext origCtx = SecurityContextHolder.getContext();
            SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
            SecurityContextHolder.getContext().setAuthentication(runAs);

            // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object);
        }
    }

 

 FilterSecurityInterceptor鉴权通过后,request的响应流程从SpringSecurity中退出,沿着最初的filterChain继续向下

 ====================           登录和鉴权完成                       ==================

 

 基于Spring-security实现动态权限管理

用户的动态权限


基于Spring security的认证和权限管理流程_第38张图片
 

 --------------------

概念辨析

 

Credentials   用户提供的用于登录用的凭据信息,如用户名/ 密码、证书、IP 地址、Cookie 值等。比如 UsernamePasswordCredentials ,封装的是用户名和密码。CAS 进行认证的第一步,就是把从UI 或request 对象里取到的用户凭据封装成Credentials 对象,然后交给认证管理器去认证。

 

Principal   封装用户,标志用户实体

 

Authentication   Authentication是认证管理器的最终处理结果, Authentication 封装了 Principal ,认证时间,及其他一些属性(可能来自 Credentials )。

 

 

如下代码实现了在登录完成后获取用户信息

public class BaseController {
	@Autowired
	protected RedisTemplate<String, String> redisTemplate;
	
	Log log = LogFactory.getLog(this.getClass());

	@Autowired
	protected RedisUtil redisUtil;

	protected HyipUserDetail getHyipUserDetail(){
		SecurityContext context = SecurityContextHolder.getContext();
		if (context == null) {
			return null;
		}
		Authentication authentication = context.getAuthentication();
		if (authentication != null) {
			Object principal = authentication.getPrincipal();
			if (principal instanceof HyipUserDetail) {
				return (HyipUserDetail)principal;
			}
		}
		return null;
	}

 

 Authentication的有不同的实现,应用于不同的场景

 

基于Spring security的认证和权限管理流程_第39张图片
 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(基于Spring security的认证和权限管理流程)