【安全研究】基于OPA和Spring Security的外部访问控制

news2024/11/23 7:49:41

译者导读

CNCF的毕业项目Open Policy Agent(OPA),
为策略决策需求提供了一个统一的框架与服务。它将策略决策从软件业务逻辑中解耦剥离,将策略定义、决策过程抽象为通用模型,实现为一个通用策略引擎,可适用于广泛的业务场景。

比如:判断某用户可以访问哪些资源、允许哪些子网对外访问、工作负载应该部署在哪个集群、容器能执行哪些操作系统功能、系统能在什么时间被访问。需要注意的是,OPA
本身是将策略决策和策略施行解耦,OPA
负责相应策略规则的评估,即决策过程,业务应用服务需要根据相应的策略评估结果执行后续操作,策略的施行是业务强相关,仍旧由业务应用来实现[1]。

这篇译文是笔者对博文《EXTERNALIZED AUTHORIZATION USING OPA AND SPRING
SECURITY》的汉化[2]。博文主要介绍基于 OPA 和 Spring Security 对Java
WEB应用进行外部访问控制的技术实践(在这里对博文的背景做一定的补充介绍:无论是云原生应用还是传统单体应用,都面临着 “失效的访问控制” 以及
“失效身份认证” 这两类漏洞的严峻风险。

这两类漏洞均“跻身”OWASP TOP10 2017以及OWASP TOP10
2021榜单,可谓“老大难”问题;并且,云原生应用带来了API应用接口爆发式增长、对策略设置的灵活性要求提高的挑战;面对这样的风险与挑战,
OPA可发挥其在业务场景“判断某用户可以访问哪些资源”的优势 ,帮客户缓解该问题)。

下面将进入译文部分。若读者朋友能够在阅读之后能收获对OPA技术思路的初步认识,甚至对OPA相似业务场景下能力和服务的研发有所助益,笔者将不胜荣幸。文中若有错漏之处,恳请读者朋友能帮忙指正。

前言

虽然OAuth2和OIDC已经成为访问控制的事实标准,流行度很广,但现有的访问控制标准(如XACML、UMA)难以落地甚至使用。因此开发人员继续推出自己的解决方案,但实现起来常常费时费力,增加了维护成本。

在本教程中,我们将探究 如何通过使用 Open Policy Agent Spring Security
将决策外部化 简化访问控制 模块

一、目标

通常,任何公开 API
的服务都需要“身份认证”(authentication)和“访问控制”(authorization)。虽然这两个术语听起来很相似,但二者在安全的作用方面有着根本的不同。“身份认证”是确定身份的过程,“访问控制”是确定权限的过程。二者都是非常关键的主题,因为对它们的关注不足可致最常见的漏洞产生(参考OWASP
Top10),今天我们将 重点关注 “访问控制”

“访问控制”大致可以分为两类:粗粒度(如RBAC(Role-Based Access
Control,基于角色的访问控制))和细粒度(如ABAC(Attribute-Based Access
Control,基于属性的访问控制),也称为PBAC(Policy-Based Access Control,基于策略的访问控制))。

通常,在区域边界实施粗粒度访问控制策略,但更细粒度的访问控制策略是需要在服务级别实现和实施的。这导致服务的安全性难以被构建,并且安全策略与业务逻辑的紧密耦合,因此对开发人员的生产力产生负面影响。我们的目标是将访问控制外部化,这允许开发人员简单地实现核心业务功能并重用公共模块进行访问控制,同时访问策略变得集中,因此在更改策略时不需要更改单个服务的代码。

二、开放策略代理

开放策略代理(OPA) 是一个开源策略引擎,它提供了一个简单的 API 便于用户将策略决策委托给它。当服务需要做出策略决策时,它会查询 OPA
并提供结构化数据作为输入。OPA 根据策略和数据评估输入并生成输出,输出也可以是任意数据结构,不限于简单allow/deny响应。OPA 策略用称为
Rego 的高级声明性语言表示。更多关于 OPA 和 Rego 的信息可以在官方文档中找到。

1 运行

若要做演示,只需从GitHub 版本[3]下载 OPA 二进制文件并将其作为服务器运行即可:

./opa run --server

或者使用官方的 OPA Docker镜像运行:

docker run -p 8181:8181 openpolicyagent/opa run --server

2 策略

让我们创建一个名为policy.rego的文件并在其中编写一个简单的策略来拒绝所有请求:

package authz

default allow = false

我们将使用 Policy API 来创建和更新策略:

curl -X PUT --data-binary @policy.rego localhost:8181/v1/policies/authz

3 数据

通常,OPA 策略需要一些数据来做出决策,可以使用各种方法将这些数据加载到 OPA
中。使用哪种方法通常取决于数据的大小和更新的频率。此外,加载数据的方式决定了在编写策略时访问它的方式。通过策略决策请求发送的数据可通过输入变量进行访问。异步加载的数据始终通过数据变量进行访问。

对于我们的示例,我们将使用其中的两种方法。其中一部分数据作为输入,另一部分数据包含组织中用户的层次结构(如图1所示),我们将使用其 Data API 推送到
OPA。

KVjU4qev_dhs3.png
1 组织架构图

为此,我们创建一个名为data.json的文件,其内容如下:

[

{"name": "alice", "subordinates": ["bob", "john"]},

{"name": "bob", "subordinates": ["carol", "david"]},

{"name": "carol", "subordinates": []},

{"name": "david", "subordinates": []},

{"name": "john", "subordinates": []}

]

并将其加载到 OPA 中:

curl -X PUT -H "Content-Type: application/json" -d @data.json localhost:8181/v1/data/users

三、应用程序

现在我们需要构建一个服务,我们将为其进行访问控制。它将是一个简单的基于 Spring Boot 的 Web 应用程序,提供用于访问两种资源的
API:salaries(薪水)和documents(文档)。

1 依赖项

首先,我们添加必要的依赖项:

<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-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>com.h2database</groupId>

<artifactId>h2</artifactId>

<scope>runtime</scope>

</dependency>

2 Salary组件

接下来,我们创建一个实体类和标准的分层架构组件来表示Salary:

@Entity

public class Salary {


@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Long id;


@Column(unique = true)

private String username;

private double amount;


// getters, setters and overriden methods from UserDetails

}


@Repository

interface SalaryRepository extends CrudRepository<Salary, Long> {

Optional<Salary> findByUsername(String username);

}


@Service

public class SalaryService {


private final SalaryRepository repository;


public SalaryService(SalaryRepository repository) {

this.repository = repository;

}


public Salary getSalaryByUsername(String username) {

return repository.findByUsername(username)

.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

}

}

@RestController

@RequestMapping("/salary")

public class SalaryController {

private final SalaryService service;


public SalaryController(SalaryService service) {

this.service = service;

}


@GetMapping("/{username}")

public Salary getSalary(@PathVariable String username) {

return service.getSalaryByUsername(username);

}

}

3 Document组

然后我们创建相同类型的类来表示Document:

@Entity

public class Document {


@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Long id;

private String content;

private String owner;


// getters and setters

}




@Repository

interface DocumentRepository extends CrudRepository<Document, Long> {

}


@Service

public class DocumentService {


private final DocumentRepository repository;




public DocumentService(DocumentRepository repository) {

this.repository = repository;

}


public Document getDocumentById(long id) {

return repository.findById(id)

.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

}

}




@RestController

@RequestMapping("/document")

public class DocumentController {


private final DocumentService service;

public DocumentController(DocumentService service) {

this.service = service;

}


@GetMapping("/{id}")

public Document getDocument(@PathVariable long id) {

return service.getDocumentById(id);

}

}

4 初始化数据

我们需要一些数据,所以我们创建一个 SQL 脚本在启动时对其进行初始化:

-- salaries

INSERT INTO salary (username, amount) VALUES ('alice', 1000);

INSERT INTO salary (username, amount) VALUES ('bob', 800);

INSERT INTO salary (username, amount) VALUES ('carol', 600);

INSERT INTO salary (username, amount) VALUES ('david', 500);

INSERT INTO salary (username, amount) VALUES ('john', 900);


-- documents

INSERT INTO document (content, owner) VALUES ('Alice Document 1', 'alice');

INSERT INTO document (content, owner) VALUES ('Bob Document 1', 'bob');

INSERT INTO document (content, owner) VALUES ('Bob Document 2', 'bob');

INSERT INTO document (content, owner) VALUES ('David Document 1', 'david');

INSERT INTO document (content, owner) VALUES ('David Document 2', 'david');

INSERT INTO document (content, owner) VALUES ('Carol Document 1', 'carol');

INSERT INTO document (content, owner) VALUES ('John Document 1', 'john');

5 安全

我们的应用程序几乎准备就绪,剩下的就是对 Web 安全性进行配置。为了简单起见,我们将用户的凭据存储在内存中并基于 HTTP 协议进行基本的身份验证:

@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {


@Bean

public PasswordEncoder passwordEncoder() {

return PasswordEncoderFactories.createDelegatingPasswordEncoder();

}


@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication()

.withUser("alice").password(passwordEncoder().encode("pass")).roles("CEO")

.and()

.withUser("bob").password(passwordEncoder().encode("pass")).roles("CTO")

.and()

.withUser("carol").password(passwordEncoder().encode("pass")).roles("DEV")

.and()

.withUser("david").password(passwordEncoder().encode("pass")).roles("DEV")

.and()

.withUser("john").password(passwordEncoder().encode("pass")).roles("HR");

}


@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.anyRequest().authenticated()

.and()

.formLogin().disable()

.httpBasic();

}

}

为了确保一切正常,我们可以运行应用程序并尝试发送几个请求:

> curl localhost:8080/salary/david

{"timestamp":"2020-10-30T00:00:00.000+00:00","status":401,"error":"Unauthorized","message":"","path":"/salary/david"}

> curl carol:pass@localhost:8080/salary/david

{"id":4,"username":"david","amount":500.0}

正如我们所料,该应用程序可以工作并且需要身份验证,但是由于缺乏访问控制的检查,任何用户都可以访问其他用户的工资。

四、访问控制

最后,当 OPA 服务器启动并且应用程序准备就绪时,我们可以继续实施访问控制。

1 使用 AccessDecisionVoter

Open Policy Agent 贡献者[为 Spring Security
提供了一个集成方式[4],它提供了](https://github.com/open-policy-
agent/contrib/tree/master/spring_authz)针对[AccessDecisionVoter](https://docs.spring.io/spring-
security/site/docs/current-
SNAPSHOT/api/org/springframework/security/access/AccessDecisionVoter.html)的简单实现,该实现使用
OPA 进行 API 授权决策。由于我们自己实现这个类很容易,并且我们可以更好地做实现,所以我们不会使用这个依赖。

我们需要实现AccessDecisionVoter接口,这将收集可能有助于决策的可用数据,将其发送给 OPA
进行评估,并根据结果,授予访问权限或拒绝访问。我们的实现会将经过身份验证的用户名、他们的权限列表、HTTP
方法和请求路径作为分段分隔的数组发送。将路径分割成段可以让我们在编写策略时更容易访问路径变量。此外,为了更清楚地了解发生了什么,我们将记录所有发送的请求和收到的响应:

public class OpaVoter implements AccessDecisionVoter<FilterInvocation> {


private static final String URI = "http://localhost:8181/v1/data/authz/allow";

private static final Logger LOG = LoggerFactory.getLogger(OpaVoter.class);


private final ObjectMapper objectMapper = new ObjectMapper();

private final RestTemplate restTemplate = new RestTemplate();


@Override

public boolean supports(ConfigAttribute attribute) {

return true;

}


@Override

public boolean supports(Class clazz) {

return FilterInvocation.class.isAssignableFrom(clazz);

}


@Override

public int vote(Authentication authentication, FilterInvocation filterInvocation, Collection<ConfigAttribute> attributes) {

String name = authentication.getName();

List<String> authorities = authentication.getAuthorities()

.stream()

.map(GrantedAuthority::getAuthority)

.collect(Collectors.toUnmodifiableList());

String method = filterInvocation.getRequest().getMethod();

String[] path = filterInvocation.getRequest().getRequestURI().replaceAll("^/|/$", "").split("/");


Map<String, Object> input = Map.of(

"name", name,

"authorities", authorities,

"method", method,

"path", path

);


ObjectNode requestNode = objectMapper.createObjectNode();

requestNode.set("input", objectMapper.valueToTree(input));

LOG.info("Authorization request:\n" + requestNode.toPrettyString());


JsonNode responseNode = Objects.requireNonNull(restTemplate.postForObject(URI, requestNode, JsonNode.class));

LOG.info("Authorization response:\n" + responseNode.toPrettyString());


if (responseNode.has("result") && responseNode.get("result").asBoolean()) {

return ACCESS_GRANTED;

} else {

return ACCESS_DENIED;

}

}

}

现在,我们可以使用Vector列表(包括我们的自定义投票器实现类Vector)定义 AccessDecisionManager的bean类,并配置
Spring Security 以使用它:

public class SecurityConfig extends WebSecurityConfigurerAdapter {


// ...


@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.anyRequest().authenticated()

.accessDecisionManager(accessDecisionManager())

.and()

.formLogin().disable()

.httpBasic();

}


@Bean

public AccessDecisionManager accessDecisionManager() {

List<AccessDecisionVoter<?>> decisionVoters = List.of(

new RoleVoter(), new AuthenticatedVoter(), new OpaVoter()

);

return new UnanimousBased(decisionVoters);

}

}

2 策略

由于我们只有一个拒绝所有请求的策略,因此无论哪个用户尝试请求薪水或任何用户,他们都会得到 403:

> curl alice:pass@localhost:8080/salary/alice

{"timestamp":"2020-10-30T00:00:00.000+00:00","status":403,"error":"Forbidden","message":"","path":"/salary/alice"}

> curl alice:pass@localhost:8080/salary/bob

{"timestamp":"2020-10-30T00:00:00.000+00:00","status":403,"error":"Forbidden","message":"","path":"/salary/bob"}

所以,是时候让我们看看 如何编写策略 了。让我们从最简单的开始,让用户访问他们的薪水。

为此,我们需要policy.rego通过添加以下内容进行更新:

allow {

some username

input.method == "GET"

input.path = ["salary", username]

input.name == username

}

现在,如果您没有忘记将策略更新推送到 OPA,所有用户都可以访问他们的薪水,但仍然无法访问其他人的薪水:

> curl alice:pass@localhost:8080/salary/alice

{"id":1,"username":"alice","amount":1000.0}

> curl alice:pass@localhost:8080/salary/bob

{"timestamp":"2020-10-30T00:00:00.000+00:00","status":403,"error":"Forbidden","message":"","path":"/salary/bob"}

那不是很棒吗?我们甚至不必重新启动我们的应用程序。

我们还可以添加基于角色的策略,例如,让具有 HR 角色的用户访问所有薪水:

allow {

input.method == "GET"

input.path = ["salary", _]

input.authorities[_] == "ROLE_HR"

}

现在具有此角色的用户可以访问所有用户的工资:

> curl john:pass@localhost:8080/salary/alice

{"id":1,"username":"alice","amount":1000.0}

> curl john:pass@localhost:8080/salary/david

{"id":4,"username":"david","amount":500.0}

到目前为止,在我们所有的策略中,我们都使用了输入数据,我们将尝试使用加载到 OPA 中的数据来编写策略。例如,我们可以让用户访问其直接下属的薪水:

allow {

some username, i

input.method == "GET"

input.path = ["salary", username]

data.users[i].name == input.name

data.users[i].subordinates[_] == username

}

用户现在可以访问其直接下属的薪水,但尚不支持刻画组织的完整层次结构:

> curl bob:pass@localhost:8080/salary/david

{"id":4,"username":"david","amount":500.0}

> curl alice:pass@localhost:8080/salary/bob

{"id":2,"username":"bob","amount":800.0}

> curl alice:pass@localhost:8080/salary/david

{"timestamp":"2020-10-30T00:00:00.000+00:00","status":403,"error":"Forbidden","message":"","path":"/salary/david"}

为了让用户能够访问所有级别下属的相关信息,我们需要编写更复杂的策略,幸运的是,Rego
通过提供大量有用的功能为我们创造了这个机会。由于我们的层次结构是一个图,我们的任务是为每个顶点找到可到达的顶点,我们可以使用具备该特殊功能的函数graph.reachable。使用某些功能可能需要对数据进行预处理,例如,在这种情况下,我们需要将数据“展平”,将其描绘为一张图,其中“用户”是“键”,“直接下属”是“值”,即:

{

"alice": ["bob", "john"],

"bob": ["carol", "david"],

"carol": [],

"david": [],

"john": []

}

Rego 允许我们定义支持数据自动迭代的规则,并生成可以像 JSON 对象一样访问的输出。

首先,我们添加一个规则来通过展平我们的数据来生成图:

users_graph[data.users[username].name] = edges {

edges := data.users[username].subordinates

}

接下来,我们添加一个规则,该规则将适当的函数应用到由前一个规则生成的图上,以获取每个用户的所有可达用户:

users_access[username] = access {

users_graph[username]

access := graph.reachable(users_graph, {username})

}

结果,我们得到如下所示的数据:

{

"alice": ["alice", "bob", "john", "carol", "david"],

"bob": ["bob", "carol", "david"],

"carol": ["carol"],

"david": ["david"],

"john": ["john"]

}

现在我们可以根据这些数据编写一个策略,因为它已经为用户提供了访问他们的薪水和直接下属的薪水的权限,我们可以删除之前的策略:

package authz


default allow = false


allow {

input.method == "GET"

input.path = ["salary", _]

input.authorities[_] == "ROLE_HR"

}




allow {

some username

input.method == "GET"

input.path = ["salary", username]

username == users_access[input.name][_]

}




users_graph[data.users[username].name] = edges {

edges := data.users[username].subordinates

}




users_access[username] = access {

users_graph[username]

access := graph.reachable(users_graph, {username})

}

让我们检查访问控制是否符合我们的策略:

> curl alice:pass@localhost:8080/salary/alice

{"id":1,"username":"alice","amount":1000.0}

> curl alice:pass@localhost:8080/salary/david

{"id":4,"username":"david","amount":500.0}

> curl david:pass@localhost:8080/salary/alice

{"timestamp":"2020-10-30T00:00:00.000+00:00","status":403,"error":"Forbidden","message":"","path":"/salary/alice"}

> curl john:pass@localhost:8080/salary/alice

{"id":1,"username":"alice","amount":1000.0}

3 使用注解

您可能已经注意到,当前我们所有的策略都只适用于获取工资,而“文档组件”被放在一边,没有用户仍然可以访问它们。问题是,当前存储在OPA中并从AccessDecisionVoter发送的数据可能不足以做出访问控制的决策。我们无法将所有数据加载到
OPA 中,因为数据可能是高度动态的或海量的,如果我们开始为AccessDecisionVoter的每个请求添加单独的逻辑,这个类的代码将造成一定的混乱度。

这个问题的一个解决方案可能是使用内置函数使OPA在评估期间提取数据。在实现边缘访问控制时,这可能是一个很好的解决方案,但由于我们现在正在服务本身中实现访问控制,因此我们更容易在最初将所有必要的数据发送到OPA,并将服务之间的请求数量降至最低。此外,我们可能希望对何时调用
OPA 有更多的控制权,而不是对每个请求都进行控制。

除了使用 Spring Security 提供的权限管理的投票器之外,另一种访问控制机制是使用 SpEL 表达式来实现 Web
和方法安全的能力。我们将通过一些支持表达式的注解在方法级进行应用。要开启全局性的基于方法的安全认证机制及相应的pre/post注解,我们需要将注解添加到一个配置文件中:

@EnableGlobalMethodSecurity(prePostEnabled = true)

我们可以通过实现PermissionEvaluator接口来使用常见的hasPermission()表达式,但这将使我们不得不使用这个接口的方法,相反,我们将利用表达式引用和使用任何SpringBean的方法的能力。让我们摆脱AccessDecisionVoter,创建一个用于表达式的组件:

@Component

public class OpaClient {




private static final String URI = "http://localhost:8181/v1/data/authz/allow";

private static final Logger LOG = LoggerFactory.getLogger(OpaClient.class);




private final ObjectMapper objectMapper = new ObjectMapper();

private final RestTemplate restTemplate = new RestTemplate();




public boolean allow(String action, Map<String, Object> resourceAttributes) {

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || !authentication.isAuthenticated() || action == null || resourceAttributes == null || resourceAttributes.isEmpty()) {

return false;

}




String name = authentication.getName();

List<String> authorities = authentication.getAuthorities()

.stream()

.map(GrantedAuthority::getAuthority)

.collect(Collectors.toUnmodifiableList());

Map<String, Object> subjectAttributes = Map.of(

"name", name,

"authorities", authorities

);







Map<String, Object> input = Map.of(

"subject", subjectAttributes,

"resource", resourceAttributes,

"action", action

);




ObjectNode requestNode = objectMapper.createObjectNode();

requestNode.set("input", objectMapper.valueToTree(input));

LOG.info("Authorization request:\n" + requestNode.toPrettyString());




JsonNode responseNode = Objects.requireNonNull(restTemplate.postForObject(URI, requestNode, JsonNode.class));

LOG.info("Authorization response:\n" + responseNode.toPrettyString());




return responseNode.has("result") && responseNode.get("result").asBoolean();

}

}

我们实现的组件只有一个方法,该方法将请求的操作和请求访问的资源的属性作为参数,并将所有这些数据与身份验证数据一起发送到 OPA
以供决策,方法的返回值是布尔型。

我们现在可以在SalaryService中使用@PreAuthroze注解:

@PreAuthorize("@opaClient.allow('read', T(java.util.Map).of('type', 'salary', 'user', #username))")

public Salary getSalaryByUsername(String username) {

return repository.findByUsername(username)

.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

}

并在DocumentService中使用@PostAuthorize注解:

@PostAuthorize("@opaClient.allow('read', T(java.util.Map).of('type', 'document', 'owner', returnObject.owner))")

public Document getDocumentById(long id) {

return repository.findById(id)

.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

}

只需调整以前的策略以适应新的数据格式,并为文档添加策略,例如,配置用户对文档的访问控制权限:

package authz




default allow = false




allow {

input.action == "read"

input.resource.type == "salary"

input.subject.authorities[_] == "ROLE_HR"

}




allow {

input.action == "read"

input.resource.type == "salary"

input.resource.user == users_access[input.subject.name][_]

}




allow {

input.action == "read"

input.resource.type == "document"

input.resource.owner == input.subject.name

}




users_graph[data.users[username].name] = edges {

edges := data.users[username].subordinates

}




users_access[username] = access {

users_graph[username]

access := graph.reachable(users_graph, {username})

}

注:为了便于读者朋友理解,笔者增补了图2的应用改造示意图以及图3的鉴权方案对比图。

u3xKUqyj_KvSp.png
2 应用改造示意图

amR3kfRP_Km7m.png
3 鉴权方案对比图

最后,让我们验证所做的工作对于Document组件和Salary组件生效:

> curl alice:pass@localhost:8080/salary/bob

{"id":2,"username":"bob","amount":800.0}

> curl john:pass@localhost:8080/salary/bob

{"id":2,"username":"bob","amount":800.0}

> curl bob:pass@localhost:8080/salary/john

{"timestamp":"2020-10-30T00:00:00.000+00:00","status":403,"error":"Forbidden","message":"","path":"/salary/john"}

> curl alice:pass@localhost:8080/document/1

{"id":1,"content":"Alice Document 1","owner":"alice"}

> curl alice:pass@localhost:8080/document/2

{"timestamp":"2020-10-30T00:00:00.000+00:00","status":403,"error":"Forbidden","message":"","path":"/document/2"}

> curl bob:pass@localhost:8080/document/2

{"id":2,"content":"Bob Document 1","owner":"bob"}

最后

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:


当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。

因篇幅有限,仅展示部分资料,有需要的小伙伴,可以【扫下方二维码】免费领取:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/169971.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阿里云对话 Tapdata:「开发者优先」正在影响商业化软件的开源选择

在刚刚过去的2022年&#xff0c;Tapdata 带着开源项目 PDK&#xff08;Plugin Development Kit&#xff09;及 Tapdata Community 和大家见面&#xff0c;兑现了我们对自己以及开发者们的开源承诺&#xff0c;同时与阿里云等生态伙伴联合&#xff0c;加速构建更加开放的数据生态…

Linux基础 - DNS服务进阶

‍‍&#x1f3e1;博客主页&#xff1a; Passerby_Wang的博客_CSDN博客-系统运维,云计算,Linux基础领域博主&#x1f310;所属专栏&#xff1a;『Linux基础』&#x1f30c;上期文章&#xff1a; Linux基础 - DNS服务基础&#x1f4f0;如觉得博主文章写的不错或对你有所帮助的话…

贪心策略(三)多机调度问题、活动选择(库函数sort的整理)

把sort库函数的使用总结一下&#xff1a; 1、头文件#include<algorithm> 时间复杂度nlog(n) 2、使用格式 sort&#xff08;arr.begin(), arr.end()&#xff09;&#xff1b; 3、默认使用升序排序&#xff0c;第三个参数默认使用less<T>() 4、如果需要进行降序排序…

springcloud + nacos多环境联调、本地联调(即灰度版本)

背景&#xff1a;当我们使用nacos为注册中心注册微服务时&#xff0c;想本地环境和测试环境公用一个nacos&#xff0c;即注册中心等基础服务共用。当我们在服务A开发时&#xff0c;本地服务和测试环境服务都是注册到同一个nacos&#xff0c;由于nacos自带负载均衡策略&#xff…

小程序开发经验分享(9)小程序快速上线汇总

微信小程序申请 开发中的Appid 需要从“微信公众平台”中获取 如果是直接从git上拉取的话 直接项目导入就可以了(名称可以是中文) 小程序基础配置 如果需要修改显示的名称和appid可以去生成的配置文件project.config.json里面修改

前端特效之毛玻璃-倾斜-日历

前端特效之毛玻璃-倾斜-日历描述项目效果index.htmlindex.css描述 项目描述开发语言HTML、JavaScript、CSS库dyCalendarJS、vanilla-tilt 该项目中需要使用到的库有&#xff1a; dyCalendarJS vanilla-tilt.js 是 JavaScript 中的一个平滑的 3D 倾斜库。vanilla-tilt dyCalen…

MS SQL Server 日志审核工具

手动审核数据库活动是一项艰巨的任务。有效实现这一目标的最佳方法是使用全面的解决方案来简化和自动化数据库和活动监控。该解决方案还应使数据库管理员能够监控、跟踪、即时识别任何操作问题的根本原因&#xff0c;并实时检测对机密数据的未经授权的访问。 审核 Microsoft S…

Bootstrap踩坑笔记(记录Bootstrap当中的核心知识点)

目录 1.Bootstrap官网 2.核心1:布局&#xff08;栅格系统&#xff09; 3.核心知识点3:响应式布局 A.容器 B.行 C.列 注意: D.案例: E.列偏移 F.列排序 4. 样式 A.按钮 B.图片 C.表格 5.组件 A.导航条 B.分页条 C.js插件 1.Bootstrap官网 Bootsrap官网 2.核心1…

docker入门(二):docker的常用命令

文章目录前言docker常用命令1 启停类/帮助 命令2镜像命令3 容器命令结语前言 大家好&#xff0c;这是我学习docker系列的笔记文章&#xff0c;目标是掌握docker,为后续学习K8s做准备。本文列举了docker的常用命令&#xff0c;感兴趣的朋友可以看一下以前的文章。 上一篇&#…

处理Springboot项目启动时streamBridge.send导致的卡住卡死问题

现象 我们的Spring Boot 项目启动时&#xff0c;偶现卡死的现象&#xff0c;启动到一半卡主不动了 2023-01-16 10:23:10.338 INFO 1 --- [ restartedMain] com.hazelcast.core.LifecycleService : [172.18.0.14]:5701 [dev] [4.2.4] [172.18.0.14]:5701 is STARTED 20…

Unity脚本 --- 常用API(常用类) --- Component类和Transform类

上面这个是Unity核心类图 第一部分 --- Component类 提供了查找组件的方法&#xff08;当前物体的&#xff0c;子物体&#xff0c;父物体的&#xff09; 1.颜色&#xff08;color&#xff09;是材质&#xff08;meterial&#xff09;控制的&#xff0c;meterial&#xff08…

【FreeRTOS】在Cortex-M4开发板上移植FreeRTOS并且实现LED灯闪烁(保姆级教程)

相信有不少小伙伴手上只有M4的开发板&#xff0c;想要移植FreeRTOS&#xff1b;但是&#xff0c;网上大部分都是M3移植教程。因此&#xff0c;陷入深深的迷茫中&#xff0c;难不成只能使用仿真了&#xff1f;&#xff1f;&#xff1f;&#x1f914;因此&#xff0c;小编特意写了…

【Linux】静态库和动态库

Linux为什么不允许普通用户给目录建立硬链接呢&#xff1f; 系统层面上有.和…硬链接指向目录。假设我们是超级用户&#xff0c;允许给目录建立硬链接&#xff0c;给根目录建立硬链接&#xff0c;从根目录开始查找&#xff0c;当查找硬链接的时候就是根目录&#xff0c;这时候递…

面试官让我聊聊 MQ 的数据丢失问题,没想到水这么深。。。

目录 一、背景引入二、Kafka分布式存储架构三、Kafka高可用架构四、画图复现Kafka的写入数据丢失问题五、Kafka的ISR机制是什么&#xff1f;六、Kafka写入的数据如何保证不丢失&#xff1f;七、总结 一、背景引入 这篇文章&#xff0c;给大家聊一下写入Kafka的数据该如何保证…

Git常用命令(全局设置获取仓库)

新建仓库: 填写名称等信息&#xff0c;根据需要选择私有&#xff0c;开源等选项。 创建完成。 邀请成员&#xff1a; Git常用命令 Git全局设置 首先要做的是设置用户名和email地址。这是非常重要的&#xff0c;每次Git提交都会使用该用户信息。 设置用户信息&#xff1a; …

岁末年初捷报频传 HashData斩获多项行业殊荣

凯歌高奏辞旧岁&#xff0c;数据赋智谱新篇。 刚刚过去的2022年&#xff0c;面对充满变数的外部环境&#xff0c;HashData始终坚持以技术为本&#xff0c;持续全面创新&#xff0c;适应数字经济发展趋势&#xff0c;笃行致远&#xff0c;砥砺前行&#xff0c;积极推动企业“上…

VPS融合怪测评脚本(主体已完善,历史遗留问题解决时间未知)(VPS fusion monster evaluation script)

ecs 原仓库链接&#xff1a;https://github.com/spiritLHLS/ecs 支持系统&#xff1a;Ubuntu 18&#xff0c;Debian 8&#xff0c;centos 7&#xff0c;Fedora&#xff0c;Almalinux 8.5, Arch 融合怪测评脚本 bash <(wget -qO- --no-check-certificate https://gitlab.…

Sealer 0.9 :帮助集群和分布式应用实现 Build、 Share、Run

作者&#xff1a;sealer 社区 新春之际&#xff0c;很高兴在此时宣布 Sealer [ 1] 0.9 版本的正式发布。Sealer 是一款致力于探索面向分布式应用的快速打包、交付和运行的解决方案。2021 年5月 Sealer 项目正式开源&#xff1b;短短一年时间&#xff0c;Sealer 在 2022 年 4 月…

人工智能图像识别四大算子

文章目录背景引入图像识别发展简介边缘检测算法*Prewitt算子**Sobel算子**Laplace算子**Conny算子** 文末寄语*背景引入 图像识别是当今计算机科学最热门的研究方向之一。随着科学技术的发展和人类社会的不断进步&#xff0c;图像识别技术在很多行业得到了广泛的应用。本章除了…

【单链表】数据结构,详解单链表,java实现代码

前言&#xff1a; 大家好&#xff0c;我是良辰丫&#x1f353;&#x1f353;&#x1f353;&#xff0c;今天我和大家一起了解一下数据结构中的链表&#xff0c;链表&#xff0c;顾名思义是用链子把一个个数据串连起了的&#xff0c;那么链表和顺序表又有什么不同呢&#xff1f;…