Spring Boot整合Spring Security

Spring Boot 专栏:Spring Boot 从零单排
Spring Cloud 专栏:Spring Cloud 从零单排
GitHub:SpringBootDemo
Gitee:SpringBootDemo

Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块的默认技术选型,仅需引入spring-boot-starter-security模块,进行少量配置,即可实现强大的Web安全控制。

Spring Security的两个主要目标是认证授权(访问控制)

官方文档:https://docs.spring.io/spring-security/site/docs/5.2.0.RELEASE/reference/htmlsingle/

0 开发环境

  • JDK:1.8
  • Spring Boot:2.7.18

Spring Boot 版本升级为2.7.18,专栏中其他Spring Boot相关环境同步升级

1 引入依赖

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

2 测试

2.1 新建Controller

@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping(value = "query")
    public String query() {
        return "用户查询成功";
    }
}

2.2 测试

启动服务,浏览器访问 127.0.0.1:8090/user/query,页面自动跳转到授权登录页

在这里插入图片描述

默认用户名为user,控制台上会打印默认密码,默认密码每次启动服务都会刷新

在这里插入图片描述

登录成功后,就可以正常访问了

在这里插入图片描述

3 自定义密码

该部分会使用到Spring Security的几个关键类,如下:

  • WebSecurityConfigurerAdapter 自定义Security策略
  • AuthenticationManagerBuilder 自定义认证策略
  • @EnableWebSecurity 开启WebSecurity模式

3.1 通过application.yml 配置

spring:
  security:
    user:
      name: admin
      password: 123456

3.2 自定义配置类配置

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //密码加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //配置用户名、密码,该配置方式下,用户名和密码保存在内存中
        auth.inMemoryAuthentication()
                //密码加密方式
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password(passwordEncoder.encode("123456")).roles("admin");
    }
}

3.3 自定义实现类配置

3.3.1 编写UserDetailsService实现类

这里我们就直接固定写死用户名和密码,实际生产中可以从数据库中获取

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //设置角色,角色的概念后续介绍
        List<GrantedAuthority> roles = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

        return new User("admin", new BCryptPasswordEncoder().encode("123456"), roles);
    }
}

3.3.2 编写配置类

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用UserDetailsServiceImpl 查询用户名、密码
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
}

分别测试,都通过

4 用户认证和授权/基于角色和权限的访问控制

实际生产中,需要根据用户角色的权限来控制可访问的页面、可执行的操作等

4.1 新建4个页面

level-1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 style="color: red">这是用户等级1可访问的页面</h1>
</body>
</html>

level-2.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 style="color: green">这是用户等级2可访问的页面</h1>
</body>
</html>

level-3.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 style="color: blue">这是用户等级3可访问的页面</h1>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="level-1.html">等级1</a><br><br>
<a href="level-2.html">等级2</a><br><br>
<a href="level-3.html">等级3</a>
</body>
</html>

4.2 修改配置类

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //请求授权的规则
        //开启认证
        http.authorizeRequests()
                //首页所有人可访问
                .antMatchers("/").permitAll()
                //功能页对应角色或权限才能访问
                //hasRole 为角色授权,表示用户拥有指定角色
                //hasAuthority 为权限授权,表示用户拥有指定权限
                .antMatchers("/level-1.html").hasRole("level1")
                .antMatchers("/level-2.html").hasRole("level2")
                .antMatchers("/level-3.html").hasAuthority("level3");

        //开启登录,无权限时进入登录页面
        http.formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //密码加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //配置用户名、密码,该配置方式下,用户名和密码保存在内存中
        auth.inMemoryAuthentication()
                //密码加密方式
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password(passwordEncoder.encode("123456")).roles("admin")
                .and().withUser("admin1").password(passwordEncoder.encode("123456")).roles("level1")
                .and().withUser("admin2").password(passwordEncoder.encode("123456")).roles("level2")
                .and().withUser("admin3").password(passwordEncoder.encode("123456")).authorities("level3")
                .and().withUser("admin0").password(passwordEncoder.encode("123456")).authorities("ROLE_level1", "ROLE_level2", "level3");
    }
}

**hasRole()hasAuthority()**用法是类似的,只不过hasRole()方法会给自定义的角色名前加上 ROLE_ 前缀

在这里插入图片描述

因此在自定义用户时,如果使用**authorities()给用户设置角色时,需要自行添加上ROLE_**前缀。

roles()authorities()设置的角色或权限,最终都存放在authorities参数中,且这两个方法会互相覆盖彼此的值。

在这里插入图片描述

4.3 测试

浏览器访问

在这里插入图片描述

依次点击等级1、等级2、等级3,均自动跳转到授权登录页面,登录对应权限的用户后,可成功访问。

其中,登录admin用户,无法访问任何页面,登录admin0,可访问所有页面

在这里插入图片描述

登录权限不匹配的用户,拒绝访问

在这里插入图片描述

4.4 使用UserDetailsService类实现

UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        List<GrantedAuthority> roles;
        if ("admin1".equals(s)) {
            roles = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_level1");
        } else if ("admin2".equals(s)) {
            roles = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_level2");
        } else if ("admin3".equals(s)) {
            roles = AuthorityUtils.commaSeparatedStringToAuthorityList("level3");
        } else if ("admin0".equals(s)) {
            roles = AuthorityUtils.createAuthorityList("ROLE_level1", "ROLE_level2", "level3");
        } else {
            roles = AuthorityUtils.createAuthorityList("admin");
        }

        return new User(s, new BCryptPasswordEncoder().encode("123456"), roles);
    }
}

SecurityConfig

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //请求授权的规则
        //开启认证
        http.authorizeRequests()
                //首页所有人可访问
                .antMatchers("/").permitAll()
                //功能页对应角色或权限才能访问
                //hasRole 为角色授权,表示用户拥有指定角色
                //hasAuthority 为权限授权,表示用户拥有指定权限
                .antMatchers("/level-1.html").hasRole("level1")
                .antMatchers("/level-2.html").hasRole("level2")
                .antMatchers("/level-3.html").hasAuthority("level3");

        //开启登录,无权限时进入登录页面
        http.formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用UserDetailsServiceImpl 查询用户名、密码
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
}

浏览器访问,测试,通过

5 常用注解

可控制用户认证访问接口

5.1 @Secured()

校验用户具有某个角色,才可访问接口

需在启动类开启注解

@EnableGlobalMethodSecurity(securedEnabled = true)

然后在接口方法上配置注解

@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping(value = "query")
    @Secured("ROLE_level1")
    public String query() {
        return "用户查询成功";
    }

    @GetMapping(value = "update")
    @Secured({"ROLE_level1", "ROLE_level2"})
    public String update() {
        return "用户更新成功";
    }
}

5.2 @PreAuthorize()

在进入方法前校验用户具有某个权限或角色

需在启动类开启注解

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)

然后在接口方法上配置注解

@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping(value = "delete")
    @PreAuthorize("hasAnyAuthority('ROLE_level1','level3')")
    public String delete() {
        return "用户删除成功";
    }
}

5.3 @PostAuthorize()

在进入方法后校验用户具有某个权限或角色

需在启动类开启注解

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)

然后在接口方法上配置注解

@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping(value = "delete")
    @PostAuthorize("hasAnyAuthority('ROLE_level1','level3')")
    public String delete() {
        return "用户删除成功";
    }
}

5.4 @PostFilter()

校验权限后对数据进行过滤,只返回满足条件的数据

新建实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserModel {

    private String username;
    private String password;
}

然后在方法上加上注解

@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping(value = "queryList")
    @PreAuthorize("hasAnyAuthority('ROLE_level1','level3')")
    @PostFilter("filterObject.username == 'test'")
    public List<UserModel> queryList() {
        List<UserModel> userList = new ArrayList<>();
        userList.add(new UserModel("test", "qwerty"));
        userList.add(new UserModel("test2", "asdfgh"));
        userList.add(new UserModel("test3", "zxcvbn"));

        return userList;
    }
}

测试,权限验证通过后

在这里插入图片描述

5.5 @PreFilter()

校验权限后对数据进行过滤,只有满足条件的数据才能传入接口方法中

@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping(value = "queryUser")
    @PreAuthorize("hasAnyAuthority('ROLE_level1','level3')")
    @PreFilter("filterObject.username == 'test2'")
    public List<UserModel> queryUser(@RequestBody List<UserModel> userModels) {
        return userModels;
    }
}

测试,权限验证通过后

在这里插入图片描述

6 记住我

配置类中开启记住我

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //请求授权的规则
        //开启认证
        http.authorizeRequests()
                //首页所有人可访问
                .antMatchers("/").permitAll()
                //功能页对应角色或权限才能访问
                //hasRole 为角色授权,表示用户拥有指定角色
                //hasAuthority 为权限授权,表示用户拥有指定权限
                .antMatchers("/level-1.html").hasRole("level1")
                .antMatchers("/level-2.html").hasRole("level2")
                .antMatchers("/level-3.html").hasAuthority("level3");

        //开启登录,无权限时进入登录页面
        http.formLogin();

        //记住我
        http.rememberMe();
    }

启动服务,访问页面,登录页面增加了记住我选择框

在这里插入图片描述

登录成功后,cookie中已保存用户信息,默认时间为2周

在这里插入图片描述

7 注销

7.1 配置类中开启注销

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //请求授权的规则
        //开启认证
        http.authorizeRequests()
                //首页所有人可访问
                .antMatchers("/").permitAll()
                //功能页对应角色或权限才能访问
                //hasRole 为角色授权,表示用户拥有指定角色
                //hasAuthority 为权限授权,表示用户拥有指定权限
                .antMatchers("/level-1.html").hasRole("level1")
                .antMatchers("/level-2.html").hasRole("level2")
                .antMatchers("/level-3.html").hasAuthority("level3");

        //开启登录,无权限时进入登录页面
        http.formLogin();

        //记住我
        http.rememberMe();

        //开启注销,注销成功后回首页
        http.logout().logoutSuccessUrl("/");
    }

7.2 level-* 页面增加注销按钮

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 style="color: red">这是用户等级1可访问的页面</h1>
<br><br>
<a href="/logout">注销</a>
</body>
</html>

其他两个页面做相同修改

启动服务,登录成功后点击注销按钮,注销成功,返回首页,访问页面需再次登录

8 自定义登录页

8.1 新建登录页login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta charset="UTF-8">
    <title>Login Page</title>

    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f5f5f5;
        }

        .container {
            width: 300px;
            margin: auto;
            padding: 40px;
            border: 1px solid #ccc;
            background-color: white;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }

        h2 {
            text-align: center;
        }

        label {
            display: block;
            margin-bottom: 10px;
        }

        input[type="text"],
        input[type="password"] {
            width: 100%;
            padding: 6px;
            border: 1px solid #ccc;
            outline: none;
        }

        button {
            width: 100%;
            padding: 10px;
            color: white;
            background-color: #4CAF50;
            cursor: pointer;
            border: none;
            outline: none;
        }

        button:hover {
            opacity: 0.9;
        }
    </style>
</head>
<body>
<div class="container">
    <h2>登录</h2>
    <form action="/login" method="post">
        <label for="username">用户名</label>
        <input type="text" id="username" name="username"><br><br>

        <label for="password">密码</label>
        <input type="password" id="password" name="password"><br><br>

        <input type="checkbox" name="remember-me" title="记住我">记住我<br><br>

        <button type="submit">登 录</button>
    </form>
</div>
</body>
</html>

8.2 配置类中开启自定义登录页

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //请求授权的规则
        //开启认证
        http.authorizeRequests()
                //首页所有人可访问
                .antMatchers("/").permitAll()
                //功能页对应角色或权限才能访问
                //hasRole 为角色授权,表示用户拥有指定角色
                //hasAuthority 为权限授权,表示用户拥有指定权限
                .antMatchers("/level-1.html").hasRole("level1")
                .antMatchers("/level-2.html").hasRole("level2")
                .antMatchers("/level-3.html").hasAuthority("level3");

        //开启登录,无权限时进入登录页面
        //自定义登录页
        http.formLogin().loginPage("/login.html").loginProcessingUrl("/login");
        //关闭csrf防护
        http.csrf().disable();

        //记住我
        http.rememberMe();

        //开启注销,注销成功后回首页
        http.logout().logoutSuccessUrl("/");
    }

这里,自定义登录,默认用户参数是username,默认密码参数是password,默认记住我参数是remember-me,如果需要自定义登录表单的参数,做如下修改

        //开启登录,无权限时进入登录页面
        http.formLogin().loginPage("/login.html").loginProcessingUrl("/login")
                .usernameParameter("username")
                .passwordParameter("password");
        //关闭csrf防护
        http.csrf().disable();

        //记住我
        http.rememberMe().rememberMeParameter("remember-me");

启动服务,访问地址,跳转到自定义登录页

在这里插入图片描述

9 自定义403页面

9.1 新建403.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
权限不足,无法访问
</body>
</html>

9.2 配置类中开启自定义403页面

    @Override
    protected void configure(HttpSecurity http) throws Exception {
		//...其他代码...

        //自定义403页面
        http.exceptionHandling().accessDeniedPage("/403.html");
    }

启动服务,浏览器访问,登录无权限用户后提示

在这里插入图片描述

至此,Spring Boot整合Spring Security实现用户认证和授权基本用法已讲解完毕,且测试通过。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MRC是谁?- 媒体评级委员会 Media Rating Council

在在线广告的世界里&#xff0c;有许多不同的技术和实践用于提供和衡量广告。对于广告商、出版商和营销人员来说&#xff0c;了解这些技术是如何工作的以及如何有效使用这些技术很重要。在这方面发挥关键作用的一个组织是媒体评级委员会&#xff08;MRC&#xff09;。 1. 了解…

[Linux]文件系统

1.理解文件系统 Linux磁盘文件特性&#xff1a;内容加属性&#xff0c;内容大小是不确定的&#xff0c;但是属性大小是一定的&#xff0c;并且内容和属性是分开存储的。文件属性是用一个结构体来定义的&#xff0c;在Linux中&#xff0c;该结构体是固定128字节大小如下代码: …

LC 98.验证二叉搜索树

98.验证二叉搜索树 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。 示例…

Vue cli创建项目时键盘操作无效;vue3.0项目搭建自定义配置

一. 问题描述 在创建vue3.0项目时&#xff0c;在建好的文件夹&#xff0c;鼠标右键 git bash 使用 vue create my-vue3.0创建新项目时&#xff0c;键盘方向键失效&#xff0c;无法选中对应的选项&#xff08;交互提示符不工作&#xff09; 解决方案&#xff1a; 方案一 使用…

架构评估方法相关知识总结

一、架构评估中的重要概念 定义&#xff1a;软件架构评估是在对架构分析、评估的基础上&#xff0c;对架构策略的选取进行决策。 常用系统架构评估的方式&#xff1a; 1. 基于调查问卷或检查表的方法&#xff1a;该方法的关键是设计好问卷或检查表。缺点是在很大 程度上依赖于评…

华为北向网管NCE开发教程(5)打包org.omg.CosNotification找不到

1问题描述 在IDE中&#xff0c;代码能正常运行&#xff0c;但是打包的时候&#xff0c;会抱不到一些类 2问题原因 导入的本地包中&#xff0c;能在IDE中找到&#xff0c;但是在使用maven打包时&#xff0c;maven找不到这些依赖包 3解决办法 将依赖包通过maven安装到maven…

算法沉淀 —— 动态规划篇(斐波那契数列模型)

算法沉淀 —— 动态规划篇&#xff08;斐波那契数列模型&#xff09; 前言一、第 N 个泰波那契数二、三步问题三、使用最小花费爬楼梯四、解码方法 前言 几乎所有的动态规划问题大致可分为以下5个步骤&#xff0c;后续所有问题分析都将基于此 1.、状态表示&#xff1a;通常状态…

Matlab|【免费】基于数据驱动的模型预测控制电力系统机组组合优化

目录 1 主要内容 2 部分代码 3 程序结果 4 下载链接 1 主要内容 该程序复现文章《Feature-Driven Economic Improvement for Network-Constrained Unit Commitment: A Closed-Loop Predict-and-Optimize Framework》&#xff0c;程序主要做的是一个基于数据驱动的电力系统机…

YOLO算法改进Backbone系列之:CoaT

在本文中&#xff0c;我们提出了co-scale conv-attention image transformer&#xff08;CoaT&#xff09;&#xff0c;这是一种基于Transformer的图像分类器&#xff0c;配备了co-scale和conv-attention机制。首先&#xff0c;co-scale机制在各个尺度上保持Transformer编码器支…

09、ArrayList

ArrayList 文章目录 ArrayList集合与数组ArrayList集合进阶集合体系结构Collection集合List集合&#xff08;接口&#xff09;数据结构ArrayList集合LinkedList集合 Set集合HashSet 双列集合创建不可变集合 集合与数组 自动扩容 无法存储基本数据类型&#xff0c;只能将其变为…

【C++】CC++内存管理

目录 一、C/C内存分布二 、C语言中动态内存管理方式&#xff1a;malloc/calloc/realloc/free三、 C内存管理方式3.1 new/delete操作内置类型3.2 new和delete操作自定义类型3.3 长度域 四、operator new与operator delete函数五、new和delete的实现原理5.1 内置类型5.2 自定义类…

K8S--水平自动扩缩容实战演练

原文网址&#xff1a;K8S--水平自动扩缩容实战演练-CSDN博客 简介 本文用实例来展示K8S的自动扩缩容&#xff08;水平方向&#xff09;。 官网网址 HorizontalPodAutoscaler 演练 | Kubernetes 为 Pod 和容器管理资源 | Kubernetes 水平扩缩的原理 水平扩缩容&#xff…

阿里云-零基础入门NLP【基于深度学习的文本分类3-BERT】

文章目录 学习过程赛题理解学习目标赛题数据数据标签评测指标解题思路BERT代码 学习过程 20年当时自身功底是比较零基础(会写些基础的Python[三个科学计算包]数据分析)&#xff0c;一开始看这块其实挺懵的&#xff0c;不会就去问百度或其他人&#xff0c;当时遇见困难挺害怕的…

NKCTF2024-Eznative

首先使用blutter解析&#xff0c;拿到如上的output文件 先看看asm 都被混淆了&#xff0c;真的是太可恶了。 查看libapp.so的内容 一点符号都不给&#xff0c;首先我们使用LoadScript File去添加一部分符号 加载之前解析的 恢复了一部分&#xff0c;但是没有什么乱用啊 这个时候…

微服务(基础篇-002-Ribbon)

目录 Ribbon负载均衡&#xff08;1&#xff09; 负载均衡的原理&#xff08;1.1&#xff09; 负载均衡策略&#xff08;1.2&#xff09; Ribbon-IRule(1.2.1) 修改负载均衡的方法&#xff08;1.2.2&#xff09; 懒加载&#xff08;1.3&#xff09; 饥饿加载&#xff08;1…

吴恩达2022机器学习专项课程(一) 3.3 成本函数的公式

问题预览 模型的参数&#xff08;w和b&#xff09;有什么作用&#xff1f;不同的w和b对线性回归模型有什么影响&#xff1f;训练集里的y和线性回归模型预测的y&#xff08;y帽&#xff09;的区别是什么&#xff1f;成本函数的作用是什么&#xff1f;成本函数的公式是什么&…

【PyQt】19-数据操作

数据表 前言一、显示二维表数据&#xff08;QTableView控件&#xff09;扩展知识---MVC模式1.1 代码1.2 运行结果 二、显示列数据&#xff08;QListView控件&#xff09;2.1 代码2.2 运行结果2.3 扩展---列表控件&#xff08;QListWidget&#xff09;运行结果 总结 前言 一、显…

C语言中的联合和枚举

1、联合体 联合体类型的声明 像结构体⼀样&#xff0c;联合体也是由⼀个或者多个成员构成&#xff0c;这些成员可以不同的类型。但是编译器只为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间。所以联合体也叫&#xff1a;共⽤体。因为所有变量公用…

UE5 LiveLink 自动连接数据源,以及打包后不能收到udp消息的解决办法

为什么要自动连接数据源&#xff0c;因为方便打包后接收数据&#xff0c;这里我是写在了Game Instance,也可以写在其他地方&#xff0c;自行替换成Beginplay和Endplay 关于编辑器模式下能收到udp消息&#xff0c;打包后不能收到消息的问题有两点需要排查&#xff0c;启动打包后…

2024年阿里云轻量应用服务器优惠价格_2核2G_2核4G报价

阿里云轻量应用服务器2核2G和2核4G配置优惠价格表&#xff0c;轻量2核2G3M带宽61元一年&#xff0c;轻量2核4G4M带宽165元1年&#xff0c;均不限制月流量&#xff0c;阿里云活动链接 aliyunfuwuqi.com/go/aliyun 活动打开如下图&#xff1a; 阿里云轻量应用服务器价格 61元/年…