简体   繁体   English

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

[英]How to implement basic Spring security (session management) for Single Page AngularJS application

I am currently building a single page AngularJS application which communicates via REST to a backend. 我目前正在构建一个单页AngularJS应用程序,它通过REST与后端进行通信。 The structure is as follow: 结构如下:

One Spring MVC WebApp project which contains all AngularJS pages and resources and all REST controllers. One Spring MVC WebApp项目,包含所有AngularJS页面和资源以及所有REST控制器。

A true backend which has services and repositories for backend communication, an API if you will. 一个真正的后端,具有用于后端通信的服务和存储库,如果您愿意,还可以使用API​​。 The REST calls will talk to these service (the second project is included as a dependency of the first one). REST调用将与这些服务进行通信(第二个项目作为第一个项目的依赖项包含在内)。

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: 我想要某种非常简单的会话管理:

  • user logs in, session id is created and stored in JS/cookie on website 用户登录,会话ID创建并存储在网站上的JS / cookie中
  • when user would reload page/ come back later a check needs to be done to see if the session id is still valid 当用户重新加载页面/稍后返回时,需要进行检查以查看会话ID是否仍然有效
  • no calls should reach the controllers if the session id is not valid 如果会话ID无效,则不会呼叫到达控制器

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

Thanks in advance! 提前致谢!

You have 2 options for the rest API: stateful or stateless. 对于其余API,您有2个选项:有状态或无状态。

1st option: HTTP session authentication - the "classical" Spring Security authentication mechanism. 第一种选择:HTTP会话身份验证 - “经典”Spring Security身份验证机制。 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). 如果您计划在多个服务器上扩展应用程序,则需要使用具有粘性会话的负载均衡器,以便每个用户都驻留在同一服务器上(或使用带Redis的Spring Session)。

2nd option: you have the choice of OAuth or token-based authentication. 第二个选项:您可以选择OAuth或基于令牌的身份验证。

OAuth2 is a stateless security mechanism, so you might prefer it if you want to scale your application across several machines. OAuth2是一种无状态安全机制,因此如果要跨多台计算机扩展应用程序,可能更喜欢它。 Spring Security provides an OAuth2 implementation. Spring Security提供OAuth2实现。 The biggest issue with OAuth2 is that requires to have several database tables in order to store its security tokens. OAuth2的最大问题是需要拥有多个数据库表才能存储其安全性令牌。

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. 基于令牌的身份验证(如OAuth2)是一种无状态安全机制,因此如果您想在多个不同的服务器上进行扩展,这是另一个不错的选择。 This authentication mechanism doesn't exist by default with Spring Security. 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. 它比OAuth2更容易使用和实现,因为它不需要持久性机制,因此它适用于所有SQL和NoSQL选项。 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. 此解决方案使用自定义令牌,该令牌是用户名的MD5哈希值,令牌的到期日期,密码和密钥。 This ensures that if someone steals your token, he should not be able to extract your username and password. 这可以确保如果有人窃取您的令牌,他就无法提取您的用户名和密码。

I recommend you to look into JHipster . 我建议你看看JHipster It will generate a web app skeleton for you with REST API using Spring Boot and the front end using AngularJS. 它将使用Spring Boot和前端使用AngularJS为您生成一个Web应用程序框架。 When generating the application skeleton it will ask you to choose between the 3 authentication mechanisms that I described above. 生成应用程序框架时,它会要求您在上面描述的3种身份验证机制之间进行选择。 You can reuse the code that JHipster will generate in your Spring MVC application. 您可以重用JHipster将在Spring MVC应用程序中生成的代码。

Here is an example of TokenProvider generated by JHipster: 以下是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: 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()) {};
            }
        };
    }

}

And the respective AngularJS configuration: 以及相应的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: 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;
        }
    };
})

Add authInterceptor to $httpProvider: 将authInterceptor添加到$ httpProvider:

.config(function ($httpProvider) {

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

})

Hope this is helpful! 希望这有用!

This video from SpringDeveloper channel may be useful too: Great single page apps need great backends . 来自SpringDeveloper频道的视频也很有用: 优秀的单页应用需要很好的后端 It talks about some best practices (including session management) and demos working code examples. 它讨论了一些最佳实践(包括会话管理)和演示工作代码示例。

Take a look from what has been done in JHipster https://jhipster.github.io/ . 看看JHipster https://jhipster.github.io/所做的工作。 You can even use it. 你甚至可以使用它。

Jhipster is a spring boot + angular/angularjs generator. Jhipster是一个弹簧靴+角度/角度发生器。 I often use it to inspire me and learn best practice. 我经常用它来激励我并学习最佳实践。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM