Защита REST API с использованием пользовательских токенов (без сохранения состояния, без пользовательского интерфейса, без файлов cookie, без базовой аутентификации, без OAuth, без страницы входа)

Существует множество рекомендаций, примеров кодов, которые показывают, как защитить REST API с помощью Spring Security, но большинство из них предполагают наличие веб-клиента и говорят о странице входа, перенаправлении, использовании файлов cookie и т. Д. Может быть даже простым фильтром, который проверяет наличие Пользовательский токен в заголовке HTTP может быть достаточно. Как реализовать безопасность для следующих требований? Есть ли какой-нибудь проект gist/github, делающий то же самое? Мои знания в области весенней безопасности ограничены, поэтому, если есть более простой способ реализовать это в весенней безопасности, пожалуйста, дайте мне знать.

  • REST API, обслуживаемый сервером без сохранения состояния через HTTPS
  • клиент может быть веб-приложением, мобильным приложением, любым приложением в стиле SPA, сторонними API
  • нет базовой аутентификации, нет файлов cookie, нет пользовательского интерфейса (нет JSP/HTML/ статических ресурсов), нет перенаправлений, нет поставщика OAuth.
  • пользовательский токен, установленный на заголовках HTTPS
  • Проверка токена выполняется в отношении внешнего хранилища (например, MemCached/Redis/ или даже любой СУБД)
  • Все API должны проходить проверку подлинности, кроме выбранных путей (например, /login, /signup, /public и т. Д.)

Я использую Springboot, Spring Security и т. Д. Предпочитаю решение с конфигурацией Java (без XML)

5 ответов

Мой пример приложения делает именно это - защита конечных точек REST с использованием Spring Security в сценарии без сохранения состояния. Индивидуальные вызовы REST аутентифицируются с использованием заголовка HTTP. Информация аутентификации хранится на стороне сервера в кеше в памяти и обеспечивает ту же семантику, что и сеанс HTTP, предлагаемый в типичном веб-приложении. Приложение использует полную инфраструктуру Spring Security с минимальным количеством настраиваемого кода. Нет пустых фильтров, нет кода вне инфраструктуры Spring Security.

Основная идея заключается в реализации следующих четырех компонентов Spring Security:

  1. org.springframework.security.web.AuthenticationEntryPoint перехватывать вызовы REST, требующие аутентификации, но не имеющие требуемого токена аутентификации, и, таким образом, отклонять запросы.
  2. org.springframework.security.core.Authentication хранить информацию аутентификации, необходимую для REST API.
  3. org.springframework.security.authentication.AuthenticationProvider выполнить фактическую аутентификацию (для базы данных, сервера LDAP, веб-службы и т. д.).
  4. org.springframework.security.web.context.SecurityContextRepository хранить токен аутентификации между HTTP-запросами. В этом примере реализация сохраняет токен в экземпляре EHCACHE.

В примере используется конфигурация XML, но вы можете легко придумать эквивалентную конфигурацию Java.

Вы правы, это нелегко и хороших примеров не так много. Примеры, которые я видел, сделали это, чтобы вы не могли использовать другие пружинные средства безопасности рядом. Я недавно сделал нечто подобное, вот что я сделал.

Вам нужен собственный токен для хранения значения вашего заголовка

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

Вам нужен весенний фильтр безопасности, чтобы извлечь заголовок и попросить менеджера аутентифицировать его, что-то вроде этоговыделенного текста

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

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


}

Наконец, зарегистрируйте своего провайдера как компонент, чтобы менеджер аутентификации нашел его в каком-то классе @Configuration. Вы, вероятно, могли бы просто @Component это, я предпочитаю этот метод

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

Код защищает все конечные точки - но я уверен, что вы можете играть с этим:). Токен хранится в Redis с помощью Spring Boot Starter Security, и вы должны определить наш собственный UserDetailsService который вы передаете в AuthenticationManagerBuilder,

Короче говоря - копировать вставить EmbeddedRedisConfiguration а также SecurityConfig и заменить 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.

ДЖАВА

Я добавил эти зависимости в 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 Config

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

Другой пример проекта, который использует JWT - Jhipster

Попробуйте сгенерировать приложение Microservice с помощью JHipster. Он генерирует шаблон с готовой интеграцией Spring Security и JWT.

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

Я рекомендую JSON Web Tokens http://jwt.io/, он не имеет состояния и масштабируется.

Вот пример проекта, https://github.com/brahalla/Cerberus

Другие вопросы по тегам