Spring Security02 - 前言和基本使用

Spring Security前言和基本使用

本系列所有的笔记都整理自《Spring Security实战》

一:Spring Security定义和用途

Spring Security和Spring家族的联系

通过官网https://spring.io/projects/spring-security可以得知,Spring Security被描述为一种用于身份验证和访问控制的强大且高度可定制的框架,简言之,它就是一个极大简化了的让Spring程序具备安全性保障的框架

你可以在https://github.com/spring-projects/spring-security获得其源码

如果你需要使用Spring Security -> 至少需要Java8

如果正在开发Spring的程序,那么Spring Security可能会成为你实现应用程序级别安全性的最佳解决方案,不过Spring Security并不会自动对应用程序进行保护,它取决于我们对Spring Secutiry的配置以及定制,这是我们需要取了解的,并且取决于从功能需求到架构的多种因素

学习过Spring或者Springboot的应该知道,框架的原理是从Spring上下文的管理开始的,我们在context中定义bean,并且根据config来管理这些bean,且我们只需要通过注解来处理这些config即可,无需通过老套的XML

我们使用注解告诉Spring要做什么,比如公开端点,在事务中包装方法,拦截切面中的方法等等。

Spring Security也是如此,我们同样可以通过注解,bean和Spring风格的配置样式自由定制使用Spring Security

那如何在Spring中使用Spring Security呢,我们最常遇到的案例之一就是当我们决定是否允许某人执行操作或使用某些数据的情形

根据上面提到的配置,我们要编写拦截请求以及确保发出请求的人有权访问受保护资源的组件,我们需要配置这些组件精确的完成所需的工作

Spring Security组件的其他职责与数据存储以及系统不同部分之间的数据传输有关。

通过拦截对这些不同部分的调用,组件可以对数据进行操作。

例如,在存储数据时,这些组件可以应用加密或哈希算法数据编码使数据只能被授权实体访问。

在Spring应用程序中,开发人员必须添加和配置组件,以便在需要的地方完成这部分工作

通过上面的讲述,我们都离不开两个字-组件

Spring Security提供了预定义的功能来帮助我们免去编写样板代码或者在应用之间重复编写相同逻辑的枯燥工作,但是即便如此我们也可以取配置它的任何一个组件,因而也就为我们提供了很大的灵活性,而我们后面的一个非常重要的学习就是取认知并且使用其各个主要的组件

二:Web应用程序中的常见安全漏洞

在讨论如何应用安全性之前,首先应知道需要保护应用程序免受哪方面的损害

了解漏洞的一个好的起点就是了解开放式Web应用程序安全项目 -> https://wwww.owasp.org

  • 不完整的身份认证
  • 会话固定
  • 跨站脚本(XSS)
  • 跨站请求伪造(CSRF)
  • 注入
  • 敏感数据暴露
  • 缺乏方法访问控制
  • 使用具有已知漏洞的依赖项

1:不完整的身份验证

在这里插入图片描述

经过身份验证的用户可以访问/products/{name}的接口。而我们的应用程序可以调用这个接口以便从db中检索和显示该用户的产品。

但是,如果应用程序在返回这些产品时没有验证产品属于谁,会发生什么呢?一些用户可以找到另一个用户详细信息的方法,这种情况在设计接口一开始就应该考虑到,才能避免类似情况的发生。

2:会话固定

利用服务器的 session 不变机制,如果存在该漏洞,攻击者就可以通过重用以前生成的会话ID来模拟有效用户。如果在认证过程中,Web应用程序没有分配唯一的会话ID,就会出现此漏洞。

这可能会导致现有会话ID的重用。

这个漏洞的利用过程包括获得一个有效的会话ID并且让目标受害者的浏览器使用它

根据web应用程序实现方式的不同,恶意攻击者可以通过各种方式利用此漏洞。

例如,如果应用程序在URL中提供了会话ID,受害者可能就会被诱骗单击恶意链接。如果应用程序使用了隐藏属性,攻击者就可以欺骗受害者使用外部表单,然后将其操作提交到服务器。如果应用程序将会话的值存储在cookie中,攻击者就可以注入一个脚本并强制受害者的浏览器执行它。
例如:攻击者首先在未登录状态下访问网站得到 sessionid,然后把带有 sessionid 的链接发给受害者,受害者点击链接并登录,而由于 sessionid 是不变的,攻击者就可以用这个 sessionid 来登录,获取受害者的页面。

更形象一点就是,我在网吧上号,然而忘了取消记住密码,然后我号就没了。

攻击的整个过程,会话ID是没变过的,所以导致此漏洞。

2.1:登录重建会话

每次登录后都重置会话ID,并生成一个新的会话ID,这样攻击者就无法用自己的会话ID来劫持会话

// 会话失效
session.invalidate();
// 会话重建
session=request.getSession(true);
2.2:禁用客户端访问Cookie

此方法也避免了配合XSS攻击来获取Cookie中的会话信息以达成会话固定攻击。

在Http响应头中启用HttpOnly属性,或者在tomcat容器中配置。

关于HttpOnly更多详细说明大家可以自行百度

3:什么是跨站脚本(XSS)

跨站脚本(cross-site-scripting)也称为XSS,是向真实网站添加恶意代码以便恶意收集用户信息的过程。XSS 攻击可能通过 Web 应用程序中的安全漏洞进行,并且通常通过注入客户端脚本来利用。

其潜在的影响可能与账户模拟(结合会话固定)或参与分布式攻击(如DDoS)有关。

举例说明。用户在WEB应用程序中发布消息或者评论。在发布消息后,网站会显示它,以便访问该页面的每个用户都能看到它。每天可能有数百人访问这个页面,这取决于该站点的受欢迎程度。就此处的示例而言,我们将它看作一个知名站点,并且有相当多的人访问它的页面。如果该客户发布了一个恶意脚本,那么在Web页面上出现该脚本时,浏览器执行该脚本又会发生什么呢?

在这里插入图片描述

我们可以看到用户在网络论坛上发布包含脚本的评论

该脚本试图从另一个应用程序(也就是上图的应用程序X)发布或获取大量数据的请求,这个应用程序X就是此次攻击的受害者。

如果该Web论坛应用程序允许跨站脚本XSS,那么所有显示带有该恶意评论的用户都会照单全收地接收这个脚本

在这里插入图片描述

4:跨站请求伪造(CSRF)

跨站请求伪造(CSRF)漏洞在Web应用程序中也很常见。

CSRF攻击会恶意利用可以从应用程序外部提取并重复使用能够调用特定服务器上操作的URL。

如果服务器信任该执行而不检查请求的来源,那么恶意攻击者就可以从任何其他地方执行其操作。

借助CSRF,攻击者可以通过隐藏操作来让用户在服务器上执行非预期的操作。

通常,使用这个漏洞,攻击者会将更改系统中数据的操作作为目标。

在这里插入图片描述

注入攻击很普遍。在注入攻击中,攻击者会利用漏洞将特定数据引入系统。其目的是破坏系统、以非预期的方式更改数据,或者检索不该由攻击者访问的数据

三:各种架构中所应用的安全性

1:单体式Web应用程序

首先我们要开发一个代表Web应用程序的系统组件。在这个应用程序的开发过程中后台和前端没有直接分离。

这类应用程序出现的方式通常是通过普通的servlet流:应用程序接收HTTP请求,然后将HTTP响应发送回客户端

有时候,可为每个客户端提供一个服务器端会话,以便存储更多HTTP请求的特定细节。

在这里插入图片描述

那么只要有一个会话,就需要考虑前文提到的会话固定漏洞以及CSRF的可能性。

还必须考虑在HTTP会话本身中所存储的内容。

服务器端会话是准持久化的。它们是有状态的数据片段,因此他们的生命周期较长。而他们在内存中驻留的时间越长,它们被访问的统计概率就越大。例如,能够访问堆转储的人可以读取应用程序内部内存中的信息。不要认为获取堆转储有太大的难度!特别是当你在使用Spring Boot开发应用程序时,你可能会发现Actuator也是应用程序的一部分。Spring Boot Actuator是一个很好的工具。根据其配置方式的不同,它可以只返回一个端点调用的堆转储,也就是说,并不一定需要root权限来访问VM才能获得转储文件。

回到CSRF方面的漏洞,避免该漏洞的最简单的方法就是使用反CSRF令牌。Spring Security中这个功能可以开箱即用。默认情况下,CSRF保护和原始CORS的验证都是启用了的。如果确定不想使用它们,则必须手动禁用。对于身份验证和授权,可以选择使用来自Spring Security的隐式登录表单配置。这样,我们就只需要重写登录和注销的外观,并且还可以获得与身份验证和授权配置的默认集成。我们还将免受会话固定的困扰。

如果实现身份验证和授权,这还意味着我们应该拥有一些具有有效凭证的用户。我们可以选择让应用程序管理用户的凭据,或者也可以选择依赖另一个系统来完成这项工作(例如,我们可能希望用户使用其QQ,微信,Gitee凭据进行登录)。在任何一种情况下,Spring Security都可以帮助我们以一种相对简单的方式配置用户管理。可以选择将用户信息存储在数据库中、使用Web服务或者连接到另一个平台。Spring Security架构中使用的抽象使其解耦化了,这样就可以选择适合我们应用程序的任何实现。

2:为前后端分离设计安全性

如今,在Web应用程序的开发中,我们经常看到前端和后端分离的选择

在这些程序中,开发人员使用ReactJS或Vue.js这样的框架开发前端,通过REST端点与后端通信

在这里插入图片描述

在前后端分离架构下,我们通常会避免使用服务器端会话,客户端会话将会替换这些会话,这类系统设计类似与在移动应用中使用的系统设计。运行在Android或iOS操作系统上的应用程序会通过REST端点调用后端。

就安全性而言,还有其他一些方面需要考虑。首先,CSRF和CORS配置通常会更复杂。

我们可能要水平地扩展系统,但并不是必须将前端和后端放在同一个服务源点上。

对于移动应用,我们甚至无法获悉其服务源点。

作为一种实际的解决方案,最简单但最不理想的方法就是使用HTTP BASIC进行端点身份验证,虽然这种方法很容易理解,并且通常会用于初始的理论身份验证示例,但是它确实存在我们希望避免的漏洞。

例如,使用HTTP Basic意味着每次调用时发送凭据。

如果该凭据没有加密,浏览器以BASE64编码的方式发送用户名和密码,这样,就可以在每个端点调用的头信息中获取流经网络的凭据。另外,假设凭据代表了已登录的用户,我们就不会希望用户为每个请求都输入他们的凭据。也不希望必须在客户端存储凭据。这种做法是不可取的。

考虑上述原因,OAuth2应孕而生。

3:OAuth2简单介绍

OAuth2框架定义了两个独立的实体:授权服务器和资源服务器

  • 授权服务器对用户进行授权,并为用户提供一个令牌,该令牌将指定用户可以使用的一组资源权限
  • 实现此功能的后端被称为资源服务器
  • 可以调用的端点则被视作受保护的资源

根据获得的令牌,完成授权后,对于资源的调用则被视作受保护的资源[可能被允许或者拒绝]

  1. 用户访问应用程序中的用例(也称为客户端)。应用程序需要调用后端资源
  2. 为了能够调用资源,应用程序首先必须获得访问令牌,因此它需要调用授权服务器来获得该令牌。在请求中,应用程序会在某些情况下发送用户凭据或一个刷新令牌
  3. 如果凭据或刷新令牌正确,则授权服务器将向客户端返回一个(新的)访问令牌
  4. 在调用所需的资源时,在对资源服务器的请求的头信息中要使用访问令牌。

在这里插入图片描述

令牌其实就是我们老生常谈的token,类似门禁卡,经过身份验证之后,将向调用者提供一个令牌,基于该令牌,调用者可以访问其具有权限的资源。

令牌有固定的生命周期,通常是短期的。

当一个令牌过期时,应用程序需要获得一个新的令牌。

如果需要,服务器可以在令牌过期之前取消其资格。下面列出这种流程的一些优点:

  • 客户端不需要存储用户凭据。访问令牌,以至于刷新令牌是唯一需要保存的访问细节。
  • 应用程序不公开用户凭据,这些凭据常常存在于网络上。
  • 如果有人截获了一个令牌,我们可以取消该令牌的资格,而不需要使用用户凭据失效。
  • 令牌可以由第三方实体使用,从而以用户的名义访问资源,而不必模拟用户。当然,在这种情况下,攻击者可以窃取令牌。但是,由于令牌的生命周期通常较短,因此可以使用此漏洞获取的令牌也是有时间限制的

当然,即使使用OAuth2流程,也不是所有一切都完美了,还需要使其适应应用的程序设计,其中一个问题就是,哪一种是管理令牌的最佳方式?

  • 将令牌持节化到应用程序的内存中
  • 将令牌持久化到数据库中
  • 使用加密签名和JWT(JSON Web Tokens)

四:首个spring Security搭建

我们本次demo均是在Spring boot环境下搭建Spring Security的项目,因为Spring boot让我们不需要编写所有的配置,它自己提供了一些预配置项,因此我们可以只重写与实现不匹配的配置

我们也称这种方法为约定大于配置原则!

1:项目构建

  1. 构建父pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>spring-boot-security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-security</name>
    <description>spring-boot-security</description>
    <packaging>pom</packaging>

    <modules>
        <module>web-module-hello</module>
    </modules>

    <!-- 版本,属性管理 -->
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
        <lombok.version>1.18.22</lombok.version>
        <hutool.version>5.5.8</hutool.version>
    </properties>

    <!-- 依赖说明 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

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

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
    </dependencies>

    <!-- 依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>
  1. 构建子pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>web-module-hello</artifactId>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>spring-boot-security</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <!-- 属性和依赖版本 -->
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
    </properties>


    <!-- 依赖说明 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </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>
    </dependencies>

</project>
  1. 创建controller包
package com.example.webmodulehello.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: cui haida
 */
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "hello world";
    }
}
  1. 为了防止端口号占用问题,我们在yaml文件中配置一下port
server:
	port: 9090
  1. 启动之后就可以发现:

在这里插入图片描述

我们发现,每次运行该应用程序时,他都会给我们生成一个新密码,并在控制台中打印此密码,如前面的代码片段所示

必须使用此密码调用该应用程序的任何一个具有HTTP Basic身份验证的端点

在这里插入图片描述

http basic验证

由RFC7617定义的HTTP Basic认证是一种非常基础而简单的认证模式,因此叫他Basic认证。他本质上就是浏览器提供的一个接口,能够根据HTTP返回值,自动弹出一个登录框,让用户输入ID和密码,最后发给服务器校验,如若成功,此后每次请求都会携带这个头部

在这里插入图片描述

在postman测试中,如果没有输入basic auth -> 将会返回403

HTTP 403意味着服务器识别了请求的调用者,但他们并不具有试图进行的调用所需的权限

一旦发送了正确的凭据,就可以在响应体中准确地看到我们早先定义地HelloController方法所返回地内容

2:了解Spring Security的默认配置(重中之重)

我们一开始就知道,Springboot帮我们预配置了组件,但是实际开发我们必须重写这些预配置的组件来满足应用程序的需求,所以我们暂时先高度概括地讨论每个组件

从之前的demo我们了解到一些逻辑:有一个默认用户,每次启动程序时我们都会得到一个随机密码,可以使用这个默认的用户和密码来调用端点,而这些都是Spring boot给我们设置的预配置项

我们可以通过下图先了解下Spring Security架构中主要参与者的总体概况以及它们之间的关系:

在这里插入图片描述

上图主要就是参与Spring Security身份验证过程中的主要组件以及它们之间的关系

这个架构代表了使用SpringSecurity实现身份验证的骨架主干,在讨论身份验证和授权的不同实现时,该架构将会被经常提起

  • 身份验证过滤器将身份验证请求委托给身份验证管理器,并根据响应配置安全上下文
  • 身份验证管理器使用身份验证提供程序处理身份验证
  • 身份验证提供程序实现了身份验证逻辑
  • 用户详情服务实现了用户管理职能,身份验证提供程序将在身份验证逻辑中使用它
  • 密码编码器实现了密码管理,身份验证提供程序将在身份验证逻辑中使用它
  • 安全上下文在身份验证过程结束后保留身份验证数据

用户详情服务对应的Bean就是UserDetailsService, 密码编码器对应的Bean为PasswordEncoder

身份验证提供程序会使用这两个bean查找用户并检查他们的密码

使用Spring Security实现UserDetailsService契约的对象会管理用户的详细信息,到目前为止,我们使用的都是Spring Boot默认的实现,这个实现即具有默认密码的"user",这个密码是在加载Spring上下文时随机生成的,是一个UUID,此时,应用程序会将密码写入控制台,我们可以看到它

然后看看PasswordEncoder.它主要承担着两个任务:

  • 将密码进行编码
  • 验证密码是否与现有编码相匹配。

尽管不像UserDetailsService对象那样明显,但PasswordEncoder对于基本的身份验证流程也是必需的

目前我们只了解PasswordEncoder和默认的UserDetailsService是同时存在的。

在替换UserDetailsServices的默认实现时,还必须指定一个PasswordEncoder.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值