Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

Вы правы, это непросто, и примеров не так много. Примеры, которые я видел, сделали так, чтобы вы не могли использовать другие элементы безопасности spring бок о бок. Недавно я сделал что-то подобное, вот что я сделал.

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

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

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

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

Пользовательский поставщик auth для проверки этого токена, извлеченного с помощью фильтра.

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

Ответ 3

Код защищает все конечные точки - но я уверен, что вы можете играть с этим:). Маркер хранится в 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.

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

Ответ 5

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

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

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