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

2022-09-04 01:58:52

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

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

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

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

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

这是基本会话管理的一般思想,在Spring MVC webapp(没有JSP,只有angular和REST控制器)中实现它的最简单方法是什么。

提前致谢!


答案 1

对于其余 API,您有 2 个选项:有状态或无状态。

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

第二个选项:您可以选择 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频道的这段视频也可能很有用:出色的单页应用程序需要出色的后端。它讨论了一些最佳实践(包括会话管理)并演示了工作代码示例。


答案 2

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

Jhipster是一个弹簧靴+角度/角度js生成器。我经常用它来激励我并学习最佳实践。


推荐