【问题标题】:How to provide multiple ways of authentication with Spring Security如何使用 Spring Security 提供多种身份验证方式
【发布时间】:2018-03-12 14:47:30
【问题描述】:

我从各种资源中获得了有关 Spring Security 的信息,并且我知道过滤器和身份验证管理器是如何分开工作的,但我不确定请求与它们一起工作的确切顺序。如果我没记错的话,简而言之,请求首先通过过滤器,过滤器调用它们各自的身份验证管理器。

我想允许两种身份验证 - 一种使用 JWT 令牌,另一种使用用户名和密码。以下是 security.xml

的摘录

Security.xml

<http pattern="/api/**" create-session="stateless" realm="protected-apis" authentication-manager-ref="myAuthenticationManager" >
        <csrf disabled="true"/>
        <http-basic entry-point-ref="apiEntryPoint" />
        <intercept-url pattern="/api/my_api/**" requires-channel="any" access="isAuthenticated()" />  <!-- make https only. -->
        <custom-filter ref="authenticationTokenProcessingFilter" position = "FORM_LOGIN_FILTER"/>
</http>

<beans:bean id="authenticationTokenProcessingFilter"
                class="security.authentication.TokenAuthenticationFilter">
    <beans:constructor-arg value="/api/my_api/**" type="java.lang.String"/>
</beans:bean>

<authentication-manager id="myAuthenticationManager">
    <authentication-provider ref="myAuthenticationProvider" />
</authentication-manager>   

<beans:bean id="myAuthenticationProvider"
                class="security.authentication.myAuthenticationProvider" />

MyAuthenticationProvider.java

public class MyAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication)
                                      throws AuthenticationException {
        // Code
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
}

TokenAuthenticationFilter.java

public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
    protected TokenAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl); //defaultFilterProcessesUrl - specified in applicationContext.xml.
        super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl)); //Authentication will only be initiated for the request url matching this pattern
        setAuthenticationManager(new NoOpAuthenticationManager());
        setAuthenticationSuccessHandler(new TokenSimpleUrlAuthenticationSuccessHandler());
        setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
    }

    /**
     * Attempt to authenticate request
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException,
                                 IOException,
                                 ServletException {
        String tid = request.getHeader("authorization");
        logger.info("token found:"+tid);
        AbstractAuthenticationToken userAuthenticationToken = authUserByToken(tid,request);
        if(userAuthenticationToken == null) throw new AuthenticationServiceException("Invalid Token");
        return userAuthenticationToken;
    }

    /**
     * authenticate the user based on token
     * @return
     */
    private AbstractAuthenticationToken authUserByToken(String token,HttpServletRequest request) throws
                                                                                               JsonProcessingException {
        if(token==null) return null;

        AbstractAuthenticationToken authToken =null;

        boolean isValidToken = validate(token);
        if(isValidToken){
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            authToken = new UsernamePasswordAuthenticationToken("", token, authorities);
        }
        else{
            BaseError error = new BaseError(401, "UNAUNTHORIZED");
            throw new AuthenticationServiceException(error.getStatusMessage());

        }
        return authToken;
    }

    private boolean validate(String token) {
        if(token.startsWith("TOKEN ")) return true;
        return false;
    }


    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {
        super.doFilter(req, res, chain);
        }
    }

通过myAuthenticationProvider 我想要基于用户名密码的身份验证并通过我想要检查 JWT 令牌的自定义过滤器。有人可以告诉我我的方向是否正确吗?

【问题讨论】:

    标签: java spring authentication spring-security


    【解决方案1】:

    解决方案概述


    一般来说,拥有多个AuthenticationProviders 的要求分为两类:

    1. 用不同的认证方式对不同类型的URL的请求进行认证,例如:
      1. 使用基于表单的用户名-密码身份验证对/web/** 的所有请求进行身份验证;
      2. 使用基于令牌的身份验证对/api/** 的所有请求进行身份验证。
    2. 使用多种支持的身份验证模式之一对所有请求进行身份验证。

    每个解决方案都略有不同,但它们基于一个共同的基础。

    Spring Security 对基于表单的用户名-密码认证提供了开箱即用的支持,所以不管上面的两个类别,这都可以很容易地实现。

    但是,不支持开箱即用的基于令牌的身份验证,因此需要自定义代码来添加必要的支持。添加此支持需要以下组件:

    1. 一个扩展 AbstractAuthenticationToken 的 POJO,它将持有用于身份验证的令牌。
    2. 扩展 AbstractAuthenticationProcessingFilter 的过滤器将从请求中提取令牌值并填充在上述步骤 1 中创建的 POJO。
    3. AuthenticationProvider 实现,将使用令牌对请求进行身份验证。
    4. 上述选项 1 或 2 的 Spring Security 配置,具体取决于要求。

    AbstractAuthenticationToken


    需要一个 POJO 来保存用于验证请求的 JWT 令牌,因此,最简单的 AbstractAuthenticationToken 实现可能如下所示:

    public JWTAuthenticationToken extends AbstractAuthenticationToken {
      private final String token;
    
      JWTAuthenticationToken(final String token, final Object details) {
        super(new ArrayList<>());
    
        this.token = token;
    
        setAuthenticated(false);
        setDetails(details);
      }
    
      @Override
      public Object getCredentials() { return null; }
    
      @Override
      public String getPrincipal() { return token; }
    }
    

    AbstractAuthenticationProcessingFilter


    需要过滤器才能从请求中提取令牌。

    public class JWTTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
      public JWTTokenAuthenticationFilter (String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
      }
    
      @Override
      public Authentication attemptAuthentication(final HttpServletRequest request
      , final HttpServletResponse response)
      throws AuthenticationException {
        final JWTAuthenticationToken token = new JWTAuthenticationToken(/* Get token from request */
        , authenticationDetailsSource.buildDetails(request));
    
        return getAuthenticationManager().authenticate(token);
      }
    }
    

    请注意,过滤器不会尝试执行身份验证;相反,它将实际的身份验证委托给AuthenticationManager,这确保了任何预身份验证步骤和后身份验证步骤也都能正确执行。

    AuthenticationProvider


    AuthenticationProvider 是负责执行身份验证的实际组件。如果配置正确,它会由AuthenticationManager 自动调用。一个简单的实现如下所示:

    public class JWTAuthenticationProvider implements AuthenticationProvider {
      @Override
      public boolean supports(final Class<?> authentication) {
        return (JWTAuthenticationToken.class.isAssignableFrom(authentication));
      }
    
      @Override
      public Authentication authenticate(final Authentication authentication)
             throws AuthenticationException {
        final JWTAuthenticationToken token = (JWTAuthenticationToken) authentication;
        ...
      }
    }
    

    不同URL不同认证方式的Spring Security配置


    为每个 URL 系列使用不同的 http 元素,例如:

    <bean class="com.domain.path.to.provider.FormAuthenticationProvider" "formAuthenticationProvider" />
    <bean class="com.domain.path.to.provider.JWTAuthenticationProvider" "jwtAuthenticationProvider" />
    
    <authentication-manager id="apiAuthenticationManager">
      <authentication-provider ref="jwtAuthenticationProvider" />
    </authentication-manager>
    
    <authentication-manager id="formAuthenticationManager">
      <authentication-provider ref="formAuthenticationProvider" />
    </authentication-manager>
    
    <bean class="com.domain.path.to.filter.JWTAuthenticationFilter" id="jwtAuthenticationFilter">
      <property name="authenticationManager" ref="apiAuthenticationManager" />
    </bean>
    
    <http pattern="/api/**" authentication-manager-red="apiAuthenticationManager">
      <security:custom-filter position="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter"/>
    
      ...
    </http>
    
    <http pattern="/web/**" authentication-manager-red="formAuthenticationManager">
      ...
    </http>
    

    由于不同的 URL 系列需要不同的身份验证模式,我们需要两个不同的 AuthenticationManagers 和两个不同的 http 配置,每个 URL 系列一个。对于每个,我们选择支持哪种身份验证模式。

    相同 URL 的多种身份验证模式的 Spring Security 配置


    使用单个http 元素,如下所示:

    <bean class="com.domain.path.to.provider.FormAuthenticationProvider" "formAuthenticationProvider" />
    <bean class="com.domain.path.to.provider.JWTAuthenticationProvider" "jwtAuthenticationProvider" />
    
    <authentication-manager id="authenticationManager">
      <authentication-provider ref="formAuthenticationProvider" />
      <authentication-provider ref="jwtAuthenticationProvider" />
    </authentication-manager>
    
    <bean class="com.domain.path.to.filter.JWTAuthenticationFilter" id="jwtAuthenticationFilter">
      <property name="authenticationManager" ref="authenticationManager" />
    </bean>
    
    <http pattern="/**">
      <security:custom-filter after="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter"/>
    
      ...
    </http>
    

    注意以下几点:

    1. AuthenticationManager 不需要为 http 元素显式指定,因为配置中只有一个,其标识符为 authenticationManager,这是默认值。
    2. 令牌过滤器插入到表单登录过滤器之后,而不是替换它。这样可以确保表单登录和令牌登录都能正常工作。
    3. AuthenticationManager 配置为使用多个AuthenticationProviders。这可确保尝试多种身份验证机制,直到找到支持请求的一种。

    【讨论】:

      【解决方案2】:

      我这样做的方式是使用 2 个安全配置器。我有一个 Java 配置示例,但如果你理解它,你可以将它移植到 xml。请注意,这只是其中一种方式,并非唯一方式。

      @Configuration
          @Order(1)                                                        
          public static class LoginSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
      
              @Override       
              public void configure(AuthenticationManagerBuilder auth) 
                throws Exception {            
                  auth.inMemoryAuthentication().withUser("user").password("user").roles("USER");
                  auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
              }
      
              protected void configure(HttpSecurity http) throws Exception {
                  http
                      .antMatcher("/api/login/**")                               
                      .authorizeRequests()
                      .antMatchers("/api/login/**").authenticated()
                          .and()
                      .httpBasic();
              }
          }
      
          @Configuration
          @Order(2)
          public static class JWTSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
      
              @Override       
              public void configure(AuthenticationManagerBuilder auth) 
                throws Exception {
      
                  auth.inMemoryAuthentication().withUser("user1").password("user").roles("USER");
                  auth.inMemoryAuthentication().withUser("admin1").password("admin").roles("ADMIN");
              }
      
              @Override
              protected void configure(HttpSecurity http) throws Exception {
                  http
                      .antMatcher("/api/**")
                      .authorizeRequests()
                      .antMatchers("/api/**").authenticated()
                          .and()          
                  .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
              }
          }
      

      解释:

      LoginSecurityConfigurerAdapter 中,我只拦截api/login 网址。因此,第一次,登录请求将被捕获,并且在成功验证后,您可以发出 JWT。 现在在JWTSecurityConfigurerAdapter,我正在处理所有其他请求。使用 tokenauthenticationfilter 它将验证 JWT,并且只有在有效 JWT 的情况下,它才会允许访问 API。

      【讨论】:

      • 我不能对 2 WebSecurityConfigurerAdapter 使用相同的 JWT 身份验证过滤器吗?
      猜你喜欢
      • 2012-02-18
      • 2020-11-09
      • 2016-04-22
      • 2012-03-07
      • 1970-01-01
      • 2016-01-11
      • 2016-05-23
      • 2018-05-07
      • 2014-10-07
      相关资源
      最近更新 更多