如何使用 Spring Boot 保护单页应用程序

Posted

技术标签:

【中文标题】如何使用 Spring Boot 保护单页应用程序【英文标题】:How to secure single page application with spring boot 【发布时间】:2020-01-08 12:45:08 【问题描述】:

我有一个使用 keycloak(单点登录)进行身份验证的 Spring Boot 应用程序,并且使用 Angular 作为前端。

我只有 1 个 index.html 页面,它是 angular 应用程序的主页,我需要确保它的安全,但我不知道该怎么做

我按照这个例子:https://developers.redhat.com/blog/2017/05/25/easily-secure-your-spring-boot-applications-with-keycloak/

在示例中,他们有不安全的 index.html 页面,当您单击产品链接时,它会转到 spring 控制器并首先针对单点登录进行身份验证,然后转发到产品页面。我验证了这一点,并看到主体被传递给控制器​​。

demo使用的是Thimeleaf框架,所以产品页面在templates文件夹下。

我面临2个问题

1) 我有一个 index.html 页面,除非经过身份验证,否则我根本不希望用户访问它。

2) 当我从角度页面触发对 REST 控制器的调用时,它在没有身份验证的情况下执行它(REST 控制器中的主体为空)

所以我的问题是 1)如何在显示 index.html 之前强制它直接进行身份验证

2) 为什么调用 rest 服务并允许它时没有主体。

这是我的应用配置类

@SpringBootApplication
public class InvoiceSearchApplication 

    public static void main(String[] args) 
        SpringApplication.run(InvoiceSearchApplication.class, args);
    


    @Configuration
    @EnableWebSecurity
    @ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
    class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
    
        /**
         * Registers the KeycloakAuthenticationProvider with the authentication manager.
         */
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
            KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
            keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
            auth.authenticationProvider(keycloakAuthenticationProvider);
        

        /**
         * Defines the session authentication strategy.
         */
        @Bean
        @Override
        protected SessionAuthenticationStrategy sessionAuthenticationStrategy() 
            return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
        

        @Bean
        public KeycloakConfigResolver KeycloakConfigResolver() return new KeycloakSpringBootConfigResolver();

        @Override
        protected void configure(HttpSecurity http) throws Exception
        
            super.configure(http);
            http.cors().and().csrf().disable();
            http.authorizeRequests().antMatchers("/*").authenticated().anyRequest().permitAll()
             .antMatchers("/assets/*").permitAll()
             .antMatchers("/assets/img/*").permitAll() ;


        
       

我把 index.html 放在 src/main/resources/static 下 我把所有的css和js文件放在src/main/resources/static/assets下。

这就是我添加的原因

.antMatchers("/assets/").permitAll() .antMatchers("/assets/img/").permitAll() ;

所以它会加载它们,否则我会在 js 文件上得到 403 禁止。

这是我的 pom.xml


    <groupId>com.example</groupId>
    <artifactId>product-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>product-app</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <keycloak.version>3.1.0.Final</keycloak.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.ws</groupId>
            <artifactId>spring-ws-core</artifactId>
        </dependency> 

       <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
             <version>3.9</version>
        </dependency>  

        <dependency>
            <groupId>com.sun.mail</groupId>
            <artifactId>javax.mail</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>


    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.keycloak.bom</groupId>
                <artifactId>keycloak-adapter-bom</artifactId>
                <version>$keycloak.version</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

application.properties

server.context-path = /invoiceSearch
keycloak.credentials.secret=$keycloak_credentials_secret
keycloak.realm-key=$keycloak_realm_key
keycloak.realm=$keycloak_realm
keycloak.auth-server-url=$keycloak_auth_server_url
keycloak.resource=$keycloak_resource
keycloak.ssl-required = external

我的 index.html 页面:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Invoice Search </title>
   <base href="/invoiceSearch/">  
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="/invoiceSearch/assets/img/mag_glass_icon.png">
<link rel="stylesheet" href="/invoiceSearch/assets/styles.eb6a472291bbfc46890c.css"></head>
<body>
  <app-root></app-root>
<script type="text/javascript" src="/invoiceSearch/assets/runtime.cf9a0e7464e091b22929.js"></script>
<script type="text/javascript" src="/invoiceSearch/assets/es2015-polyfills.bda95d5896422d031328.js" nomodule></script>
<script type="text/javascript" src="/invoiceSearch/assets/polyfills.8bbb231b43165d65d357.js"></script>
<script type="text/javascript" src="/invoiceSearch/assets/scripts.03e042f1f102bf0e2ed8.js"></script>
<script type="text/javascript" src="/invoiceSearch/assets/main.32b645cbed7ce5c0e94f.js"></script></body>
</html>

【问题讨论】:

【参考方案1】:

这里有很多东西要解压。我对keycloak不太熟悉,所以我会说得更笼统。

    为了防止用户访问 index.html,它需要在 Spring Security 之后,并且需要已经进行了身份验证。假设在用户获取 index.html 页面之前确实发生了身份验证,您需要有一个 spring 安全设置来处理它,或者有一个返回 index.html 页面的安全端点。这里的困难在于对 index.html 的 GET 调用没有简单的方法来传递 auth 标头令牌。对于 Angular 应用程序,最佳解决方案是使用反向 Web 代理,不仅可以处理静态内容分发,还可以处理用户页面刷新,您需要从 Angular 路由返回 index.html 页面 (mysite/ng/route1)

    这引出了一个问题,您的用户是如何被标记为已通过身份验证的。我将假设一个令牌,因为 keycloak 使用带有 SSO 的令牌。如果是这种情况,则意味着您必须将该令牌与每个经过身份验证的请求一起传回,通常作为带有 Bearer 的 auth 标头,否则带有 keycloak 的 spring 应用程序将不知道您的用户是谁。除非您的 index.html 页面上确实包含机密信息,否则我必须假设它没有,因为您的内容不安全,否则最好的解决方案是这样做:

一个。让 index.html 不安全 湾。进入时,检查保存的令牌,或使用 keycloak auth 端点处理 SSO/OAuth。您必须以某种方式让您的用户获得令牌.... C。将令牌保存在 sessionStorage 中以确保页面刷新安全。 d。将令牌添加到 Angular 中的全局 html 标头配置中。

您还可以使用身份验证保护基于用户身份验证状态或角色来保护您的角度路由,但请记住,这不是“真正的”安全性,实际上更像是一种门面。

【讨论】:

我们在 Keycloak 中针对 LDAP 进行身份验证,我们没有发送令牌。我刚刚发现演示站点实际上是在 html 页面中使用指向 spring 控制器“/products”的链接。当您单击该链接时,将进行身份验证,因为我可以在控制器中看到主体。但我正在对控制器而不是链接进行 GET/POST。完成后,它不会发生。我在 Get 上得到 404,在 POST 上得到 403。如果我将它设为控制器的链接(href),它会在主体经过身份验证后到达那里。

以上是关于如何使用 Spring Boot 保护单页应用程序的主要内容,如果未能解决你的问题,请参考以下文章

使用 spring-boot OAuth2 服务器保护的 Spring-boot 应用程序

如何使用Azure AD B2C保护Spring Boot REST API?

如何使用 OAuth2 和社交登录保护 Spring Boot RESTful 服务

Spring boot 2.1.x 如何使用基本身份验证保护执行器端点

配置spring boot以将404重定向到单页应用程序[重复]

如何保护 Spring Boot Web 应用程序中的 REST API?