1. ホーム

[解決済み】Spring経由のRESTful認証

2022-04-05 16:14:07

質問

問題あり。

私たちは、機密情報を含むSpring MVCベースのRESTful APIを持っています。APIは安全であるべきですが、各リクエストでユーザーの認証情報(ユーザー/パスのコンボ)を送信することは望ましくありません。RESTのガイドライン(および社内のビジネス要件)により、サーバーはステートレスのままでなければなりません。APIはマッシュアップ方式で他のサーバーから消費される。

要求事項

  • クライアントが .../authenticate (保護されていない URL) に認証情報を入力すると、サーバーは安全なトークンを返します。このトークンには、サーバーが今後のリクエストを検証し、ステートレスを維持するのに十分な情報が含まれています。これはおそらく、Spring Securityの リメンバーミートークン .

  • クライアントは、様々な(保護された)URLに対して、前回取得したトークンをクエリパラメータ(または、あまり望ましくないが、HTTPリクエストヘッダ)として付加して、以降リクエストを行う。

  • クライアントがクッキーを保存することは期待できない。

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

この問題を解決するために、私たちは壁に頭を打ち付けています。

上記のシナリオを想定した場合、この特殊なニーズをどのように解決しますか?

どのように解決するのか?

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

これで、いいスタートが切れるはずだ。