如何为单页 AngularJS 应用程序实现基本的 Spring 安全性(会话管理) [英] How to implement basic Spring security (session management) for Single Page AngularJS application

查看:29
本文介绍了如何为单页 AngularJS 应用程序实现基本的 Spring 安全性(会话管理)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am currently building a single page AngularJS application which communicates via REST to a backend. The structure is as follow:

一个包含所有 AngularJS 页面和资源以及所有 REST 控制器的 Spring MVC WebApp 项目.

One Spring MVC WebApp project which contains all AngularJS pages and resources and all REST controllers.

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

A true backend which has services and repositories for backend communication, an API if you will. The REST calls will talk to these service (the second project is included as a dependency of the first one).

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

I have been thinking about this a lot but I can't seem to find anything that can help me. Basically I just need some security on this application. I'd like some kind of session management which is extremely simple:

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

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

This is the general idea of basic session managament, what would be the easiest way to get this implemented in a Spring MVC webapp (no JSP's, just angular and REST controllers).

提前致谢!

推荐答案

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

You have 2 options for the rest API: stateful or stateless.

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

1st option: HTTP session authentication - the "classical" Spring Security authentication mechanism. If you plan to scale your application on multiple servers, you need to have a load balancer with sticky sessions so that each user stays on the same server (or use Spring Session with Redis).

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

2nd option: you have the choice of OAuth or token-based authentication.

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

OAuth2 is a stateless security mechanism, so you might prefer it if you want to scale your application across several machines. Spring Security provides an OAuth2 implementation. The biggest issue with OAuth2 is that requires to have several database tables in order to store its security tokens.

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

Token-based authentication, like OAuth2, is a stateless security mechanism, so it's another good option if you want to scale on several different servers. This authentication mechanism doesn't exist by default with Spring Security. It is easier to use and implement than OAuth2, as it does not require a persistence mechanism, so it works on all SQL and NoSQL options. This solution uses a custom token, which is a MD5 hash of your user name, the expiration date of the token, your password, and a secret key. This ensures that if someone steals your token, he should not be able to extract your username and password.

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

I recommend you to look into JHipster. It will generate a web app skeleton for you with REST API using Spring Boot and the front end using AngularJS. When generating the application skeleton it will ask you to choose between the 3 authentication mechanisms that I described above. You can reuse the code that JHipster will generate in your Spring MVC application.

以下是 JHipster 生成的 TokenProvider 示例:

Here is an example of TokenProvider generated by 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);
    }
}

安全配置:

@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 配置:

And the respective AngularJS configuration:

'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:

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:

Add authInterceptor to $httpProvider:

.config(function ($httpProvider) {

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

})

希望对您有所帮助!

来自 SpringDeveloper 频道的这个视频也可能有用:出色的单页应用需要出色的后端.它讨论了一些最佳实践(包括会话管理)和演示工作代码示例.

This video from SpringDeveloper channel may be useful too: Great single page apps need great backends. It talks about some best practices (including session management) and demos working code examples.

这篇关于如何为单页 AngularJS 应用程序实现基本的 Spring 安全性(会话管理)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆