Как реализовать базовую безопасность Spring (управление сеансом) для приложения Single Page AngularJS

В настоящее время я создаю одностраничное приложение AngularJS, которое связывается через REST с бэкэнд. Структура выглядит следующим образом:

Один проект Spring MVC WebApp, содержащий все страницы и ресурсы AngularJS и все контроллеры REST.

Настоящий бэкэнд, который имеет службы и репозитории для бэкэнд-коммуникаций, API, если хотите. Звонки REST будут разговаривать с этими службами (второй проект включен как зависимость от первого).

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

  • пользователь регистрируется, идентификатор сеанса создается и сохраняется в JS/cookie сайт
  • когда пользователь перезагрузит страницу/вернется позже, необходимо проверить, проверяет ли идентификатор сеанса
  • никакие вызовы не должны доходить до контроллеров, если идентификатор сеанса недействителен.

Это общая идея базового управления сеансом, что было бы самым простым способом реализовать это в Spring MVC webapp (без JSP, только angular и REST-контроллеров).

Спасибо заранее!

Ответ 1

У вас есть 2 варианта для остального API: stateful или stateeless.

1-й вариант: аутентификация сеанса HTTP - "классический" Spring механизм проверки подлинности. Если вы планируете масштабировать свое приложение на нескольких серверах, вам нужно иметь балансировщик нагрузки с липкими сеансами, чтобы каждый пользователь оставался на одном сервере (или использовал Spring Session с Redis).

Вторая опция: у вас есть выбор аутентификации OAuth или токена.

OAuth2 - механизм безопасности без состояния, поэтому вы можете предпочесть его, если хотите масштабировать приложение на нескольких машинах. Spring Безопасность обеспечивает реализацию OAuth2. Самая большая проблема с OAuth2 заключается в том, что для хранения своих токенов безопасности требуется наличие нескольких таблиц базы данных.

Аутентификация на основе токена, такая как OAuth2, является механизмом безопасности без состояния, поэтому это еще один хороший вариант, если вы хотите масштабировать на нескольких разных серверах. Этот механизм аутентификации по умолчанию не существует с помощью Spring Security. Его проще использовать и реализовывать, чем OAuth2, поскольку он не требует механизма персистентности, поэтому он работает со всеми параметрами SQL и NoSQL. Это решение использует пользовательский токен, который представляет собой хеш MD5 вашего имени пользователя, дату истечения срока действия токена, пароль и секретный ключ. Это гарантирует, что если кто-то украдет ваш токен, он не сможет извлечь ваше имя пользователя и пароль.

Я рекомендую вам изучить JHipster. Он создаст скелет веб-приложения для вас с помощью REST API с помощью Spring Boot и переднего конца с помощью AngularJS. При создании скелета приложения он попросит вас выбрать между тремя механизмами аутентификации, описанными выше. Вы можете повторно использовать код, который JHipster будет генерировать в вашем приложении Spring MVC.

Вот пример TokenProvider, сгенерированный JHipster:

public class TokenProvider {

    private final String secretKey;
    private final int tokenValidity;

    public TokenProvider(String secretKey, int tokenValidity) {
        this.secretKey = secretKey;
        this.tokenValidity = tokenValidity;
    }

    public Token createToken(UserDetails userDetails) {
        long expires = System.currentTimeMillis() + 1000L * tokenValidity;
        String token = userDetails.getUsername() + ":" + expires + ":" + computeSignature(userDetails, expires);
        return new Token(token, expires);
    }

    public String computeSignature(UserDetails userDetails, long expires) {
        StringBuilder signatureBuilder = new StringBuilder();
        signatureBuilder.append(userDetails.getUsername()).append(":");
        signatureBuilder.append(expires).append(":");
        signatureBuilder.append(userDetails.getPassword()).append(":");
        signatureBuilder.append(secretKey);

        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("No MD5 algorithm available!");
        }
        return new String(Hex.encode(digest.digest(signatureBuilder.toString().getBytes())));
    }

    public String getUserNameFromToken(String authToken) {
        if (null == authToken) {
            return null;
        }
        String[] parts = authToken.split(":");
        return parts[0];
    }

    public boolean validateToken(String authToken, UserDetails userDetails) {
        String[] parts = authToken.split(":");
        long expires = Long.parseLong(parts[1]);
        String signature = parts[2];
        String signatureToMatch = computeSignature(userDetails, expires);
        return expires >= System.currentTimeMillis() && signature.equals(signatureToMatch);
    }
}

SecurityConfiguration:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Inject
    private Http401UnauthorizedEntryPoint authenticationEntryPoint;

    @Inject
    private UserDetailsService userDetailsService;

    @Inject
    private TokenProvider tokenProvider;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Inject
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
            .antMatchers("/scripts/**/*.{js,html}");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint)
        .and()
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
                .antMatchers("/api/register").permitAll()
                .antMatchers("/api/activate").permitAll()
                .antMatchers("/api/authenticate").permitAll()
                .antMatchers("/protected/**").authenticated()
        .and()
            .apply(securityConfigurerAdapter());

    }

    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
    private static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
    }

    private XAuthTokenConfigurer securityConfigurerAdapter() {
      return new XAuthTokenConfigurer(userDetailsService, tokenProvider);
    }

    /**
     * This allows SpEL support in Spring Data JPA @Query definitions.
     *
     * See https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions
     */
    @Bean
    EvaluationContextExtension securityExtension() {
        return new EvaluationContextExtensionSupport() {
            @Override
            public String getExtensionId() {
                return "security";
            }

            @Override
            public SecurityExpressionRoot getRootObject() {
                return new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};
            }
        };
    }

}

И соответствующая конфигурация AngularJS:

'use strict';

angular.module('jhipsterApp')
    .factory('AuthServerProvider', function loginService($http, localStorageService, Base64) {
        return {
            login: function(credentials) {
                var data = "username=" + credentials.username + "&password="
                    + credentials.password;
                return $http.post('api/authenticate', data, {
                    headers: {
                        "Content-Type": "application/x-www-form-urlencoded",
                        "Accept": "application/json"
                    }
                }).success(function (response) {
                    localStorageService.set('token', response);
                    return response;
                });
            },
            logout: function() {
                //Stateless API : No server logout
                localStorageService.clearAll();
            },
            getToken: function () {
                return localStorageService.get('token');
            },
            hasValidToken: function () {
                var token = this.getToken();
                return token && token.expires && token.expires > new Date().getTime();
            }
        };
    });

authInterceptor:

.factory('authInterceptor', function ($rootScope, $q, $location, localStorageService) {
    return {
        // Add authorization token to headers
        request: function (config) {
            config.headers = config.headers || {};
            var token = localStorageService.get('token');

            if (token && token.expires && token.expires > new Date().getTime()) {
              config.headers['x-auth-token'] = token.token;
            }

            return config;
        }
    };
})

Добавьте authInterceptor в $httpProvider:

.config(function ($httpProvider) {

    $httpProvider.interceptors.push('authInterceptor');

})

Надеюсь, что это будет полезно!

Это видео из канала SpringDeveloper может быть полезно также: Отличная отдельная страница приложениям нужны большие бэкэнды. В нем рассказывается о некоторых передовых методах (включая управление сессиями) и примерах рабочих примеров демонстраций.

Ответ 2

Взгляните на то, что было сделано в JHipster https://jhipster.github.io/. Вы даже можете использовать это.

Jhipster - это генератор с пружинными башмаками + угловой/угловой. Я часто использую это, чтобы вдохновить меня и изучить лучшие практики.