如何为单页 AngularJS 应用程序实现基本的 Spring 安全性(会话管理)

Posted

技术标签:

【中文标题】如何为单页 AngularJS 应用程序实现基本的 Spring 安全性(会话管理)【英文标题】:How to implement basic Spring security (session management) for Single Page AngularJS application 【发布时间】:2015-07-19 13:00:53 【问题描述】:

我目前正在构建一个单页 AngularJS 应用程序,它通过 REST 与后端通信。结构如下:

一个 Spring MVC WebApp 项目,其中包含所有 AngularJS 页面和资源以及所有 REST 控制器。

一个真正的后端,它有用于后端通信的服务和存储库,如果你愿意的话,一个 API。 REST 调用将与这些服务通信(第二个项目作为第一个项目的依赖项包含在内)。

我一直在思考这个问题,但我似乎找不到任何可以帮助我的东西。基本上我只需要这个应用程序的一些安全性。我想要某种非常简单的会话管理:

用户登录,会话id被创建并存储在JS/cookie中 网站 当用户重新加载页面/稍后返回时,需要检查会话 ID 是否仍然有效 如果会话 ID 无效,则不应有任何调用到达控制器

这是基本会话管理的一般概念,在 Spring MVC Web 应用程序中实现它的最简单方法是什么(没有 JSP,只有 Angular 和 REST 控制器)。

提前致谢!

【问题讨论】:

【参考方案1】:

其余 API 有 2 个选项:有状态或无状态。

第一个选项:HTTP 会话身份验证 - “经典” Spring Security 身份验证机制。如果您计划在多台服务器上扩展您的应用程序,则需要有一个带有粘性会话的负载均衡器,以便每个用户都留在同一台服务器上(或使用带有 Redis 的 Spring Session)。

第二个选项:您可以选择 OAuth 或基于令牌的身份验证。

OAuth2 是一种无状态安全机制,因此如果您想在多台机器上扩展您的应用程序,您可能更喜欢它。 Spring Security 提供了 OAuth2 实现。 OAuth2 的最大问题是需要有多个数据库表才能存储其安全令牌。

基于令牌的身份验证(如 OAuth2)是一种无状态的安全机制,因此如果您想在多个不同的服务器上进行扩展,它是另一个不错的选择。默认情况下,Spring Security 不存在此身份验证机制。它比 OAuth2 更易于使用和实现,因为它不需要持久性机制,因此它适用于所有 SQL 和 NoSQL 选项。此解决方案使用自定义令牌,它是您的用户名、令牌到期日期、密码和密钥的 MD5 哈希。这样可以确保如果有人窃取了您的令牌,他应该无法提取您的用户名和密码。

我建议您查看JHipster。它将使用 Spring Boot 使用 REST API 和使用 AngularJS 的前端为您生成 Web 应用程序框架。在生成应用程序框架时,它会要求您在我上面描述的 3 种身份验证机制之间进行选择。您可以重用 JHipster 将在您的 Spring MVC 应用程序中生成的代码。

下面是JHipster生成的TokenProvider示例:

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

安全配置:

@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 channel 的这个视频也可能有用:Great single page apps need great backends。它讨论了一些最佳实践(包括会话管理)和演示工作代码示例。

【讨论】:

这看起来很有希望,非常感谢!不过我想知道,因为现在工厂中似乎有一个明确的功能来检查是否有有效的令牌,但是我将如何使令牌检查自动进行(例如,在每个请求的标头中发送它)并拒绝没有有效令牌的请求? @E.V.d.B.您在配置块中向 $httpProvider 添加一个拦截器。我用代码示例更新了我的答案。 还有一件事,我怎么能打一些不需要令牌的电话?每个用户都可以调用(通过网站导航)? 你在 Spring Security 配置的后端配置这个。如果您使用 WebSecurityConfigurerAdapter 或 xml 等效项,请使用 .permitAll()。这里的例子spring.io/blog/2013/07/03/… 嗯好吧,那么我还有一个问题,我在哪里/如何配置 Spring Security?我在哪里告诉'这些 REST url's 可以免费访问并且这些需要一个令牌'?因为http拦截器是在客户端,那么后端的spring security怎么应对呢?【参考方案2】:

看看 JHipster https://jhipster.github.io/ 所做的工作。你甚至可以使用它。

Jhipster 是一个 spring boot + angular/angularjs 生成器。我经常用它来激励我并学习最佳实践。

【讨论】:

以上是关于如何为单页 AngularJS 应用程序实现基本的 Spring 安全性(会话管理)的主要内容,如果未能解决你的问题,请参考以下文章

如何为搜索引擎优化 PhantomJS 以索引单页应用程序?

AngularJS:在单页​​应用程序中使用身份验证的基本示例

服务端(前端)| 如何为 SPA(单页应用)做搜索引擎优化

如何为 React Router SPA 实现滚动恢复

如何为(Angularjs)Web 应用程序进行集成测试

Angular单页应用&AngularJS内部实现原理