【问题标题】:Spring Security - Token based API auth & user/password authenticationSpring Security - 基于令牌的 API 身份验证和用户/密码身份验证
【发布时间】:2014-03-26 12:06:23
【问题描述】:

我正在尝试创建一个主要使用 Spring 提供 REST API 的 webapp,并尝试配置安全方面。

我正在尝试实现这种模式:https://developers.google.com/accounts/docs/MobileApps(Google 已经完全改变了该页面,所以不再有意义 - 请参阅我在这里引用的页面:http://web.archive.org/web/20130822184827/https://developers.google.com/accounts/docs/MobileApps

这是我需要完成的:

  • Web 应用程序具有简单的登录/注册表单,可以使用普通的 spring 用户/密码身份验证(之前使用 dao/authenticationmanager/userdetailsservice 等做过此类事情)
  • REST api 端点是无状态会话,每个请求都根据请求提供的令牌进行身份验证

(例如,用户使用普通表单登录/注册,webapp 提供带有令牌的安全 cookie,然后可用于后续 API 请求)

我有一个正常的身份验证设置如下:

@Override protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf()
            .disable()
        .authorizeRequests()
            .antMatchers("/resources/**").permitAll()
            .antMatchers("/mobile/app/sign-up").permitAll()
            .antMatchers("/v1/**").permitAll()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/")
            .loginProcessingUrl("/loginprocess")
            .failureUrl("/?loginFailure=true")
            .permitAll();
}

我正在考虑添加一个预身份验证过滤器,它检查请求中的令牌,然后设置安全上下文(这是否意味着将跳过正常的后续身份验证?),但是,超出了普通用户/密码我没有做太多基于令牌的安全性,但基于其他一些示例,我想出了以下内容:

安全配置:

@Override protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .disable()
            .addFilter(restAuthenticationFilter())
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint()).and()
                .antMatcher("/v1/**")
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/mobile/app/sign-up").permitAll()
                .antMatchers("/v1/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/")
                .loginProcessingUrl("/loginprocess")
                .failureUrl("/?loginFailure=true")
                .permitAll();
    }

我的自定义休息过滤器:

public class RestAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public RestAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

    private final String HEADER_SECURITY_TOKEN = "X-Token"; 
    private String token = "";


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

        this.token = request.getHeader(HEADER_SECURITY_TOKEN);

        //If we have already applied this filter - not sure how that would happen? - then just continue chain
        if (request.getAttribute(FILTER_APPLIED) != null) {
            chain.doFilter(request, response);
            return;
        }

        //Now mark request as completing this filter
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

        //Attempt to authenticate
        Authentication authResult;
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            unsuccessfulAuthentication(request, response, new LockedException("Forbidden"));
        } else {
            successfulAuthentication(request, response, chain, authResult);
        }
    }

    /**
     * Attempt to authenticate request - basically just pass over to another method to authenticate request headers 
     */
    @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        AbstractAuthenticationToken userAuthenticationToken = authUserByToken();
        if(userAuthenticationToken == null) throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
        return userAuthenticationToken;
    }


    /**
     * authenticate the user based on token, mobile app secret & user agent
     * @return
     */
    private AbstractAuthenticationToken authUserByToken() {
        AbstractAuthenticationToken authToken = null;
        try {
            // TODO - just return null - always fail auth just to test spring setup ok
            return null;
        } catch (Exception e) {
            logger.error("Authenticate user by token error: ", e);
        }
        return authToken;
    }

以上内容实际上导致应用启动时出错:authenticationManager must be specified 谁能告诉我如何最好地做到这一点 - pre_auth 过滤器是最好的方法吗?


编辑

我写下了我的发现以及我如何使用 Spring-security(包括代码)实现标准令牌实现(不是 OAuth)

Overview of the problem and approach/solution

Implementing the solution with Spring-security

希望它对其他人有所帮助..

【问题讨论】:

  • 我会推荐 Spring Security OAuth(2) 而不是自定义实现。恕我直言,我会尽量避免实施自定义解决方案。大多数情况下,它容易出错且不安全。特别是如果您使用 Spring MVC,您可以考虑将 Spring Security 和 Spring Security OAuth(2) 作为基于令牌的身份验证流程的有效替代方案。
  • 我最初计划使用 OAuth2 来确保安全 - 但我质疑 API 仅计划由我正在构建的应用程序使用(例如,没有其他计划的客户端/消费者等),然后我看到了上面的链接:developers.google.com/accounts/docs/MobileApps,Google 推荐了上面概述的方法,另外对于单个客户端,我不知道 OAuth2 是否会过大。请参阅我之前关于安全性的问题:stackoverflow.com/q/21461223/258813
  • 我也看过这样的实现:thebuzzmedia.com/… - 但这非常接近两腿 OAuth 1 模式
  • 另外,您还需要配置基于表单的身份验证。

标签: spring rest authentication spring-mvc spring-security


【解决方案1】:

我相信您提到的错误只是因为您使用的AbstractAuthenticationProcessingFilter 基类需要AuthenticationManager。如果你不打算使用它,你可以将它设置为无操作,或者直接实现Filter。如果您的Filter 可以验证请求并设置SecurityContext,则通常会跳过下游处理(这取决于下游过滤器的实现,但我在您的应用程序中看不到任何奇怪的东西,所以它们可能都这样)。

如果我是你,我可能会考虑将 API 端点放在一个完全独立的过滤器链中(另一个 WebSecurityConfigurerAdapter bean)。但这只会使事情更容易阅读,不一定很重要。

您可能会发现(如 cmets 中所建议的那样)您最终会重新发明轮子,但尝试并没有什么坏处,并且您可能会在此过程中了解更多有关 Spring 和 Security 的信息。

补充:github approach 很有趣:用户只需在基本身份验证中使用令牌作为密码,服务器不需要自定义过滤器(BasicAuthenticationFilter 很好)。

【讨论】:

  • 太好了 - 谢谢。我并没有真正考虑过拥有多个配置,但这是个好主意。有什么原因我不能在两个单独的文件中配置 API/webapp,每个文件都定义了自己的 URL 来拦截和自己的 AuthenticationProvider?您认为这是一种合理的方法吗?
  • 它必须是 2 个类(WebSecurityConfigurerAdapter 的 2 个实例),所以我猜它也可能是 2 个文件。 AuthenticationProvider 不一定合适 (YMMV),但每个人都需要一个 AuthenticationManager。我想他们可以通过ProviderManager 分享它,但我认为这没有多大价值,因为它们是具有不同身份验证要求的不同资源。
猜你喜欢
  • 2017-07-10
  • 2021-01-25
  • 2016-04-19
  • 1970-01-01
  • 2020-03-03
  • 2013-02-11
  • 2013-09-25
  • 2017-04-19
  • 1970-01-01
相关资源
最近更新 更多