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

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

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

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

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

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

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

提前致谢!

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

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

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

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

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

我建议你看看JHipster 。 它将使用Spring Boot和前端使用AngularJS为您生成一个Web应用程序框架。 生成应用程序框架时,它会要求您在上面描述的3种身份validation机制之间进行选择。 您可以重用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); } } 

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频道的video也很有用: 优秀的单页应用程序需要很好的后端 。 它讨论了一些最佳实践(包括会话管理)和演示工作代码示例。