web-dev-qa-db-ja.com

Spring Security-トークンベースのAPI認証とユーザー/パスワード認証

主にREST APIをSpringを使用して提供する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フィルターがこれを行うための最良の方法ですか?


[〜#〜] edit [〜#〜]

私が見つけたものと、OAuthではなく標準トークン実装を実装するSpring-security(コードを含む)でそれをどのように行ったかを書きました

問題とアプローチ/解決策の概要

Spring-securityを使用したソリューションの実装

他の人にも役立つことを願っています。

44
rhinds

あなたが言及しているエラーは、使用しているAbstractAuthenticationProcessingFilter基本クラスがAuthenticationManagerを必要とするからだと思います。使用しない場合は、no-opに設定するか、Filterを直接実装します。 Filterがリクエストを認証し、SecurityContextをセットアップできる場合、通常、ダウンストリーム処理はスキップされます(ダウンストリームフィルターの実装に依存しますが、あなたのアプリなので、おそらくそれらはすべてそのように動作します)。

私なら、APIエンドポイントを完全に別のフィルターチェーン(別のWebSecurityConfigurerAdapter Bean)に配置することを検討できます。しかし、それは物事を読みやすくするだけで、必ずしも重要ではありません。

(コメントで示唆されているように)車輪を再発明することに気付くかもしれませんが、試してみても害はありません。おそらく、その過程でSpringとSecurityについてさらに学ぶでしょう。

追加: githubアプローチ は非常に興味深い:ユーザーは基本認証でトークンをパスワードとして使用するだけで、サーバーはカスタムフィルターを必要としません(BasicAuthenticationFilterは問題ありません)。

7
Dave Syer