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

标签 java spring authentication spring-security

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

我想允许两种身份验证 - 一种使用 JWT token ,另一种使用用户名和密码。以下是 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 token 。有人可以告诉我我的方向是否正确吗?

最佳答案

解决方案概述


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

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

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

Spring Security 对基于表单的用户名-密码身份验证具有开箱即用的支持,因此无论以上两种类别如何,都可以很容易地实现。

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

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

AbstractAuthenticationToken


POJO 需要保存用于验证请求的 JWT token ,因此,最简单的 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


需要一个过滤器来从请求中提取 token 。

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);
  }
}

请注意,过滤器不会尝试执行身份验证;相反,它将实际身份验证委托(delegate)给 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;
    ...
  }
}

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


为每个 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 系列需要不同的身份验证模式,我们需要两个不同的 AuthenticationManager 和两个不同的 http 配置,每个 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="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. token 过滤器插入到表单登录过滤器之后,而不是替换它。这可确保表单登录和 token 登录都有效。
  3. AuthenticationManager 配置为使用多个 AuthenticationProvider。这可确保尝试多种身份验证机制,直到找到请求支持的一种。

关于java - 如何使用 Spring Security 提供多种身份验证方式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49225035/

相关文章:

java - @Bean @Configuration可以继承吗?

java - Spring Boot 数据源数据库连接配置不起作用

c# - 将用户信息保存在 MVC 4 ASP.NET 中的自定义成员资格和角色提供者中

authentication - Neo4j 有哪些高级安全选项?

Java Web 错误 : Error : running Tomcat 7. 0:找不到 jsp 内置 servlet 的配置

java - 从远程计算机使用 Web 服务

java - 在 Jdbc 和 Mysql 中使用 Prepared Statements 批量插入非常慢

java - 如何使用Java在MySQL数据库中插入日期?

Django,创建用户后无法登录

android - 如何从 Android 应用程序向 Rails 应用程序发送 POST 请求?