java servlet3.1规范解读系列六: web安全之spring secret 处理

Spring Security是通过自定义的Filter对相关的URL进行权限控制,这些个filter组合起来通过两个过程对权限进行了控制,认证(authentication)和授权(authorization)。认证是来识别当前用户是谁的过程,授权是判断当前用户是否有权限进行相关操作的过程。

认证(authentication)

认证的过程相对简单,基本都是判断当前正在操作的用户(Principal)和密码(credentials)是否匹配。对与简单登录的方式,就是去匹配用户名和密码;对于单点登录,可能就是去验证token是否有效了。获取到这些信息后,会包装到对象Authentication中。这个Authentication就是后续Filter决定页面跳转的依据。Authentication定义如下:

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

授权(authorization)

对于需要控制权限的URL,一般都要三部分信息:

  1. URL的pattern,即对哪些URL进行权限控制
  2. 设置权限,即最低需要什么权限才能访问这些URL
  3. 当前用户是什么权限

获取用户权限

前两点可以通过配置<security:intercept-url pattern="/abc/**" access="hasRole('USER')"/>指定,并被包装成SecurityMetadataSource对象,那么当前登录用户的权限从哪里获取?

Spring Security定义了如下接口, UserDetailsService用于获取UserDetail,而UserDetail里包含权限。

public interface UserDetailsService {
    /**
     * Locates the user based on the username. In the actual implementation, the search
     * may possibly be case sensitive, or case insensitive depending on how the
     * implementation instance is configured. In this case, the <code>UserDetails</code>
     * object that comes back may have a username that is of a different case than what
     * was actually requested...
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

public interface UserDetails extends Serializable {
    /**
     * Returns the authorities granted to the user. Cannot return null.
     */
    Collection<? extends GrantedAuthority> getAuthorities();
    ......
}

具体的获取UserDetail的过程,要根据具体的业务进行处理,比如从数据库获取,或者从缓存中得到。获取之后包装为Authentication对象供后续Filter使用。

UserDetailsService在身份认证中的作用

    Spring Security中进行身份验证的是AuthenticationManager接口,ProviderManager是它的一个默认实现,但它并不用来处理身份认证,而是委托给配置好的AuthenticationProvider,每个AuthenticationProvider会轮流检查身份认证。检查后或者返回Authentication对象或者抛出异常。

    验证身份就是加载响应的UserDetails,看看是否和用户输入的账号、密码、权限等信息匹配。此步骤由实现AuthenticationProvider的DaoAuthenticationProvider(它利用UserDetailsService验证用户名、密码和授权)处理。包含 GrantedAuthority 的 UserDetails对象在构建 Authentication对象时填入数据。

 

 

认证处理流程:

关键的filter

AbstractAuthenticationProcessingFilter

用于认证,其步骤如下:

  1. 从request获取相关用户信息(密码、token等)构造Authentication;
  2. AuthenticationManager其中的AuthenticationProvider进行Authentication验证;Provider里可能会获取UserDetails(包含用户具有的权限)放入Authentication中;若验证通过,则返回该Authentication中;否则抛异常;
  3. 该Filter捕获到异常,则导航到相关error或login页面;若正常,则根据具体的代码设置,由后面的filter继续处理或直接访问到资源;
 // Authentication success. 上面第三步
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

当然,这个filter并不是每个url都会去拦截的,只有满足一定条件的url才会去拦截。比如不需要权限控制的url就不会被拦截。

FilterSecurityInterceptor和ExceptionTranslationFilter

这两个filter用于授权。其在Spring Security的Filter Chain中处于很靠后的位置。

FilterSecurityInterceptor的关键代码如下:

//获取Authentication
        Authentication authenticated = authenticateIfRequired();

        // Attempt authorization
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                    accessDeniedException));
            //这里抛出去的异常会由ExceptionTranslationFilter捕获
            throw accessDeniedException;
        }

ExceptionTranslationFilter关键代码如下:

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
                ........
            }
        }

ExceptionTranslationFilter会根据抛出的异常是AccessDeniedException还是AuthenticationException来判断导航页面。

在Spring Security所有的Filter中,只有两种filter是可以导航到页面的:XXXAuthenticationProcessingFilter和ExceptionTranslationFilter,因此在配置时,需要提供相关页面url给这两种filter。

常用的几个Filter

1. SecurityContextPersistenceFilter

该filter默认启用,作用是保存Authentication对象使后续的filter可以获得这个对象。对于同一个用户(session id相同),会从Session中取出用户的校验结果。若是第一次访问,则会新建SecurityContext放入SecurityContextHolder(该Holder实际上是用一个ThreadLocal来保存SecurityContext)待后续代码保存校验结果。

2. CasAuthenticationFilter

用于处理CAS service的token,对于用到单点登录的系统,这个filter会经常使用。

3. AnonymousAuthenticationFilter

默认启用。由上文可知,AbstractAuthenticationProcessingFilter中可以构造Authentication。那么不需要权限控制的url怎么去构造Authentication呢?AnonymousAuthenticationFilter就发挥了作用。该filter构造了一个pesudo Authentication提供给FilterSecurityInterceptor,从而使所有的url的处理流程统一。

基本步骤

 

 

上图包括了最基本的验证流程,当然默认还有很多filter会起作用,图中并没有显示。其中AbstractAuthenticationProcessingFilter并不是必需的,其他的四个都会默认启用。

一些疑问

  • 既然FilterChainProxy也是一个代理,为何不在web.xml中直接配置这个呢?
  • 为何DelegatingFilterProxy没有直接代理FilterChain,而是又通过另一层FilterChainProxy去代理FilterChain?
  • 不使用代理,手动方式将需要的Filter配置在web.xml可以吗?

首先要明白Tomcat中各个组件的启动顺序:Listener -> SpringContext -> Filter -> Servlet。在Tomcat读取web.xml的<filter>之前,spring context已经启动,我们配置的各种bean都已初始化完成。

对于第一个问题,如果FilterChainProxy直接配置在<filter>标签里,那么它的初始化就由ServletContext完成(即调用init()方法),这样它的很多属性将无法初始化,包括里面的Filter List,因为FilterChainProxy的init()方法依然是abstract的。而DelegatingFilterProxy的init()已完全实现。换个角度理解,DelegatingFilterProxy是filter,而FilterChainProxy是个spring bean。

再者,如果想用别的安全框架,比如shiro,那么这个时候DelegatingFilterProxy代理的就是shiro的那一套东东了,所以DelegatingFilterProxy是支持插拔式的。

第二个问题,Spring Security是由自定义的filter组成,这些filter由spring初始化,那么由spring自身的bean来管理和代理这些filter会更方便。FilterChainProxy对外提供了统一的入口。当然,如果必须要DelegatingFilterProxy去代理这些filter,笔者认为也可以。

第三个问题个人觉得是可以的,但是这样的话,filter的生命周期就会交给ServletContext了,对于开发者而言,就需要考虑这些filter的初始化参数等很多因素;而且Spring Security的filters是有一定顺序的,这就更提高了使用门槛。

将这些filter完全交由SpringContext来管理,极大地降低使用难度。

具体的代码实现可以参考:https://blog.csdn.net/weixin_34234823/article/details/87568476

 

总结下:

     spring secret 的实现和java sevlet 中的实现,在原理是其实是完全不一样的,但是在一些处理概念上有一些相同的地方,都区分为用户,角色等,不同用户使用不同的角色等控制不同的权限!

      参考spring secret 的实现逻辑,分为两部分实现:  增加filter过滤器,在过滤器中进行安全认证,权限判断等操作!基于此,在非必须spring secret 或者是无法使用spring secret的项目中,同样按照此逻辑实现自己的安全控制!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值