如何使用Spring Security提供多种身份验证方式

时间:2018-03-11 20:59:25

标签: java spring authentication spring-security

我从各种资源中了解了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我想要基于用户名密码的身份验证&amp;通过自定义过滤器,我想检查JWT令牌。如果我朝着正确的方向前进,有人可以告诉我吗?

2 个答案:

答案 0 :(得分:4)

解决方案概述

从广义上讲,要求多个AuthenticationProvider分为两类:

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

    Spring Security对基于表单的用户名 - 密码身份验证提供了开箱即用的支持,因此无论上述两个类别如何,都可以非常轻松地实现。

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

    1. 扩展AbstractAuthenticationToken的POJO,用于保存令牌以进行身份​​验证。
    2. 扩展AbstractAuthenticationProcessingFilter的过滤器,它将从请求中提取令牌值并填充上面步骤1中创建的POJO。
    3. AuthenticationProvider实施,将使用令牌验证请求。
    4. 上面选项1或2的Spring Security配置,具体取决于要求。
    5. 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配置

      为每个网址系列使用不同的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系列需要不同的身份验证模式,我们需要两个不同的AuthenticationManager和两个不同的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配置为使用多个AuthenticationProvider。这可以确保尝试多种身份验证机制,直到找到支持请求的机制。

答案 1 :(得分:0)

我这样做的方式是使用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。