web-dev-qa-db-ja.com

セキュア化RESTカスタムトークンを使用したAPI(ステートレス、UIなし、Cookieなし、基本認証なし、OAuthなし、ログインページなし)

REST APIとSpring Securityをセキュリティで保護する方法を示すサンプルコードがたくさんありますが、それらのほとんどはWebクライアントを想定し、ログインページ、リダイレクト、Cookieの使用などについて話します。 HTTPヘッダーのカスタムトークンをチェックする単純なフィルターでも十分かもしれません。以下の要件のセキュリティを実装するにはどうすればよいですか?Gist/githubプロジェクトで同じことを行いますか?春のセキュリティに関する私の知識は限られているため、 Spring Securityでこれを実装する簡単な方法を教えてください。

  • HTTPSを介したステートレスバックエンドによって提供されるREST API
  • クライアントは、Webアプリ、モバイルアプリ、任意のSPAスタイルアプリ、サードパーティAPIである可能性があります
  • 基本認証なし、Cookieなし、UIなし(JSP/HTML/static-resourcesなし)、リダイレクトなし、OAuthプロバイダー。
  • hTTPSヘッダーに設定されたカスタムトークン
  • 外部ストア(MemCached/Redis /またはRDBMSなど)に対して行われるトークン検証
  • 選択したパス(/ login、/ signup、/ publicなど)を除き、すべてのAPIを認証する必要があります。

Springboot、Spring Securityなどを使用します。Java config(no XML)

37

私の サンプルアプリ はまさにこれを行います-RESTステートレスシナリオでSpring Securityを使用してエンドポイントを保護します。個々のREST呼び出しはHTTPヘッダー:認証情報はサーバー側のメモリ内キャッシュに保存され、一般的なWebアプリケーションのHTTPセッションで提供されるものと同じセマンティクスを提供しますアプリは、最小限のカスタムコードで完全なSpring Securityインフラストラクチャを使用します。裸のフィルター、Spring Securityインフラストラクチャの外部にコードはありません。

基本的な考え方は、次の4つのSpring Securityコンポーネントを実装することです。

  1. org.springframework.security.web.AuthenticationEntryPoint to trap REST認証を要求しているが、必要な認証トークンが欠落しているため、要求を拒否する呼び出し。
  2. org.springframework.security.core.Authentication REST APIに必要な認証情報を保持します。
  3. org.springframework.security.authentication.AuthenticationProvider(データベース、LDAPサーバー、Webサービスなどに対する)実際の認証を実行します。
  4. org.springframework.security.web.context.SecurityContextRepositoryは、HTTP要求の間に認証トークンを保持します。サンプルでは、​​実装はトークンをEHCACHEインスタンスに保存します。

サンプルではXML構成を使用していますが、同等のJava config。

33
manish

あなたは正しい、それは簡単ではなく、そこに多くの良い例はありません。私が見た例は、他の春のセキュリティのものを並べて使用できないように作られました。私は最近似たようなことをしました、ここに私がしたことです。

ヘッダー値を保持するにはカスタムトークンが必要です

public class CustomToken extends AbstractAuthenticationToken {
  private final String value;

  //Getters and Constructor.  Make sure getAutheticated returns false at first.
  //I made mine "immutable" via:

      @Override
public void setAuthenticated(boolean isAuthenticated) {
    //It doesn't make sense to let just anyone set this token to authenticated, so we block it
    //Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
    if (isAuthenticated) {

        throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
    }

    super.setAuthenticated(false);
}
}

ヘッダーを抽出し、マネージャーに認証を依頼するには、次のようなスプリングセキュリティフィルターが必要です。emphasized text

public class CustomFilter extends AbstractAuthenticationProcessingFilter {


    public CustomFilter(RequestMatcher requestMatcher) {
        super(requestMatcher);

        this.setAuthenticationSuccessHandler((request, response, authentication) -> {
        /*
         * On success the desired action is to chain through the remaining filters.
         * Chaining is not possible through the success handlers, because the chain is not accessible in this method.
         * As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
         * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
         * "Subclasses can override this method to continue the FilterChain after successful authentication."
         */
        });

    }



    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {


        String tokenValue = request.getHeader("SOMEHEADER");

        if(StringUtils.isEmpty(tokenValue)) {
            //Doing this check is kinda dumb because we check for it up above in doFilter
            //..but this is a public method and we can't do much if we don't have the header
            //also we can't do the check only here because we don't have the chain available
           return null;
        }


        CustomToken token = new CustomToken(tokenValue);
        token.setDetails(authenticationDetailsSource.buildDetails(request));

        return this.getAuthenticationManager().authenticate(token);
    }



    /*
     * Overriding this method to maintain the chaining on authentication success.
     * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
     * "Subclasses can override this method to continue the FilterChain after successful authentication."
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {


        //if this isn't called, then no auth is set in the security context holder
        //and subsequent security filters can still execute.  
        //so in SOME cases you might want to conditionally call this
        super.successfulAuthentication(request, response, chain, authResult);

        //Continue the chain
        chain.doFilter(request, response);

    }


}

Spring Securityチェーンにカスタムフィルターを登録する

 @Configuration
 public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {        

      //Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
      protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
        CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
        filter.setAuthenticationManager(this.authenticationManagerBean());
        return filter;
      }

       @Override
       protected void configure(HttpSecurity http) throws Exception {                  

            http
            //fyi: This adds it to the spring security proxy filter chain
            .addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
       }
}

フィルターで抽出されたトークンを検証するカスタム認証プロバイダー。

public class CustomAuthenticationProvider implements AuthenticationProvider {


    @Override
    public Authentication authenticate(Authentication auth)
            throws AuthenticationException {

        CustomToken token = (CustomToken)auth;

        try{
           //Authenticate token against redis or whatever you want

            //This i found weird, you need a Principal in your Token...I use User
            //I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
            org.springframework.security.core.userdetails.User principal = new User(...); 

            //Our token resolved to a username so i went with this token...you could make your CustomToken take the principal.  getCredentials returns "NO_PASSWORD"..it gets cleared out anyways.  also the getAuthenticated for the thing you return should return true now
            return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
        } catch(Expection e){
            //TODO throw appropriate AuthenticationException types
            throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
        }


    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomToken.class.isAssignableFrom(authentication);
    }


}

最後に、プロバイダーをBeanとして登録し、認証マネージャーが@Configurationクラスでそれを見つけるようにします。あなたはおそらくそれを@Componentにすることもできます、私はこの方法を好む

@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies)  {
    return new CustomAuthenticationProvider(injectedDependencies);
}
9
Chris DaMour

コードはすべてのエンドポイントを保護します-しかし、私はあなたがそれで遊ぶことができると確信しています:)。トークンは、Spring Boot Starter Securityを使用してRedisに保存され、UserDetailsServiceに渡す独自のAuthenticationManagerBuilderを定義する必要があります。

短い話-EmbeddedRedisConfigurationSecurityConfigをコピーして貼り付け、AuthenticationManagerBuilderをロジックに置き換えます。

HTTP:

トークンのリクエスト-基本的なHTTP認証コンテンツをリクエストヘッダーで送信します。トークンは応答ヘッダーで返されます。

http --print=hH -a user:password localhost:8080/v1/users

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Basic dXNlcjpwYXNzd29yZA==
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:23 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af

同じリクエストですが、トークンを使用しています:

http --print=hH localhost:8080/v1/users 'x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af'

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
x-auth-token:  cacf4a97-75fe-464d-b499-fcfacb31c8af

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:58 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

間違ったユーザー名/パスワードまたはトークンを渡すと、401になります。

Java

これらの依存関係をbuild.gradleに追加しました

compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-web")
compile("com.github.kstyrc:embedded-redis:0.6")

その後、Redisの構成

@Configuration
@EnableRedisHttpSession
public class EmbeddedRedisConfiguration {

    private static RedisServer redisServer;

    @Bean
    public JedisConnectionFactory connectionFactory() throws IOException {
        redisServer = new RedisServer(Protocol.DEFAULT_PORT);
        redisServer.start();
        return new JedisConnectionFactory();
    }

    @PreDestroy
    public void destroy() {
        redisServer.stop();
    }

}

セキュリティ構成:

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .requestCache()
                .requestCache(new NullRequestCache())
                .and()
                .httpBasic();
    }

    @Bean
    public HttpSessionStrategy httpSessionStrategy() {
        return new HeaderHttpSessionStrategy();
    }
}

通常、チュートリアルではAuthenticationManagerBuilderを使用してinMemoryAuthenticationを見つけますが、さらに多くの選択肢があります(LDAPなど)、クラス定義を調べてください。 userDetailsServiceオブジェクトを必要とするUserDetailsServiceを使用しています。

最後に、CrudRepositoryを使用したユーザーサービス。

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount userAccount = userRepository.findByEmail(username);
        if (userAccount == null) {
            return null;
        }
        return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}
4
radeklos

JWTを使用する別のサンプルプロジェクト-Jhipster

JHipsterを使用してマイクロサービスアプリケーションを生成してみてください。 Spring SecurityとJWTをすぐに統合できるテンプレートを生成します。

https://jhipster.github.io/security/

0
Dhananjay