SpringによるRESTful認証


262

問題:
機密情報を含むSpring MVCベースのRESTful APIがあります。APIは保護する必要がありますが、各リクエストでユーザーの資格情報(ユーザー/パスの組み合わせ)を送信することは望ましくありません。RESTガイドライン(および内部のビジネス要件)に従って、サーバーはステートレスのままである必要があります。APIは、マッシュアップスタイルのアプローチで別のサーバーによって使用されます。

要件:

  • クライアントは、.../authenticate資格情報を使用して(保護されていないURL)に要求を行います。サーバーは、サーバーが将来のリクエストを検証してステートレスを維持するのに十分な情報を含む安全なトークンを返します。これはおそらく、Spring SecurityのRemember-Meトークンと同じ情報で構成されます。

  • クライアントは、さまざまな(保護された)URLへの後続の要求を作成し、以前に取得したトークンをクエリパラメーター(または、あまり望ましくないが、HTTP要求ヘッダー)として追加します。

  • クライアントはクッキーを保存することを期待できません。

  • すでにSpringを使用しているため、このソリューションではSpring Securityを利用する必要があります。

私たちはこの仕事をするために頭を壁にぶつけてきましたので、うまくいけば誰かがすでにこの問題を解決しているでしょう。

上記のシナリオで、この特定のニーズをどのように解決できますか?


49
こんにちはクリス、クエリパラメータでそのトークンを渡すのが最善の方法かどうかはわかりません。HTTPSまたはHTTPに関係なく、ログに表示されます。ヘッダーはおそらくより安全です。参考までに。素晴らしい質問です。+1
jmort253 2012年

1
ステートレスについてどのように理解していますか?あなたのトークン要件は、ステートレスに関する私の理解と衝突します。Http認証の答えは、私には唯一のステートレス実装のようです。
Markus Malkusch、2014年

9
@MarkusMalkuschステートレスとは、特定のクライアントとの以前の通信に関するサーバーの知識を指します。HTTPは本質的にステートレスであり、セッションCookieはステートフルにします。トークンの存続期間(およびソースについては)は無関係です。サーバーは、それが有効であり、ユーザー(セッションではない)に結び付けることができることのみを考慮します。したがって、識別トークンを渡しても、ステートフル性が妨げられることはありません。
Chris Cashwell、2014年

1
@ChrisCashwellトークンがクライアントによって偽装/生成されていないことをどのように確認しますか?サーバー側で秘密鍵を使用してトークンを暗号化し、それをクライアントに提供してから、同じ鍵を使用して将来のリクエスト時にそれを復号化しますか?明らかに、Base64やその他の難読化では不十分です。これらのトークンの「検証」の手法について詳しく説明できますか?
Craig Otis

6
これは日付があり、2年以上コードに触れたり更新したりしていませんが、これらの概念をさらに拡張するためにGistを作成しました。gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

回答:


190

OPで説明されているとおりにこれを機能させることができました。うまくいけば、他の誰かがソリューションを利用できるようになるでしょう。これが私たちがしたことです:

次のようにセキュリティコンテキストを設定します。

<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
    <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
    <security:intercept-url pattern="/authenticate" access="permitAll"/>
    <security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>

<bean id="CustomAuthenticationEntryPoint"
    class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />

<bean id="authenticationTokenProcessingFilter"
    class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
    <constructor-arg ref="authenticationManager" />
</bean>

ご覧のとおり、カスタムを作成しましたAuthenticationEntryPoint。これは基本的に401 Unauthorized、リクエストがによってフィルターチェーンで認証されなかった場合にを返すだけAuthenticationTokenProcessingFilterです。

CustomAuthenticationEntryPoint

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
    }
}

AuthenticationTokenProcessingFilter

public class AuthenticationTokenProcessingFilter extends GenericFilterBean {

    @Autowired UserService userService;
    @Autowired TokenUtils tokenUtils;
    AuthenticationManager authManager;

    public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Map<String, String[]> parms = request.getParameterMap();

        if(parms.containsKey("token")) {
            String token = parms.get("token")[0]; // grab the first "token" parameter

            // validate the token
            if (tokenUtils.validate(token)) {
                // determine the user based on the (already validated) token
                UserDetails userDetails = tokenUtils.getUserFromToken(token);
                // build an Authentication object with the user's info
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
                // set the authentication into the SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));         
            }
        }
        // continue thru the filter chain
        chain.doFilter(request, response);
    }
}

明らかに、TokenUtilsいくつかの重要な(そして非常にケース固有の)コードが含まれており、簡単に共有することはできません。ここにそのインターフェースがあります:

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

それであなたは良いスタートを切るはずです。ハッピーコーディング。:)


トークンがリクエストと共に送信されるときに、トークンを認証する必要がありますか?ユーザー名情報を直接取得して、現在のコンテキスト/リクエストに設定するのはどうですか?
フィッシャー

1
@Springそれらをどこにも保存しません...トークンの全体的な考え方は、すべてのリクエストで渡す必要があるということです。また、トークンを(部分的に)分解して、その有効性を決定することができます(つまりvalidate(...)メソッド)。サーバーがステートレスのままであるため、これは重要です。Springを使用せずにこのアプローチを使用できると思います。
Chris Cashwell 2013年

1
クライアントがブラウザの場合、トークンはどのように保存できますか?または、リクエストごとに認証をやり直す必要がありますか?
beginner_

2
素晴らしいヒント。@ChrisCashwell-私が見つけることができない部分は、ユーザーの資格情報を検証してトークンを送り返す場所ですか?/ authenticateエンドポイントの実装のどこかにあると思います。私は正しいですか?そうでない場合/ authenticateの目的は何ですか?
ヨナタンママン2014年

3
AuthenticationManager内には何がありますか?
MoienGK 2015

25

あなたはダイジェストアクセス認証を検討するかもしれません。基本的に、プロトコルは次のとおりです。

  1. クライアントからのリクエスト
  2. サーバーは一意のナンス文字列で応答します
  3. クライアントは、ナンスでハッシュされたユーザー名とパスワード(およびその他のいくつかの値)md5を提供します。このハッシュはHA1として知られています
  4. サーバーはクライアントの身元を確認し、要求された資料を提供することができます
  5. nonceとの通信は、サーバーが新しいnonceを提供するまで継続できます(カウンターを使用してリプレイ攻撃を排除します)

このすべての通信はヘッダーを介して行われ、jmort253が指摘しているように、URLパラメーターで機密情報を通信するよりも一般的に安全です。

ダイジェストアクセス認証は、Spring Securityでサポートされています。クライアントのプレーンテキストパスワードにアクセスする必要があるとドキュメントに記載されていますが、クライアントのHA1ハッシュがあれば認証に成功することに注意してください。


1
これは可能なアプローチですが、トークンを取得するためにいくつかのラウンドトリップを行う必要があるため、トークンは少し望ましくありません。
Chris Cashwell、2012年

クライアントがHTTP認証仕様に準拠している場合、これらのラウンドトリップは最初の呼び出しと5.が発生したときにのみ発生します。
Markus Malkusch、2014年

5

情報を運ぶトークンに関しては、JSON Web Tokens(http://jwt.io)は素晴らしい技術です。主な概念は、情報要素(クレーム)をトークンに埋め込み、トークン全体に署名して、検証側がクレームが本当に信頼できることを確認できるようにすることです。

私はこのJava実装を使用しています:https : //bitbucket.org/b_c/jose4j/wiki/Home

Springモジュール(spring-security-jwt)もありますが、それがサポートするものについてはまだ調べていません。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.