当前位置: 首页 > news >正文

构建安全稳定的应用:Spring Security 实用指南

前言

在现代 Web 应用程序中,安全性是至关重要的一个方面。Spring Security 作为一个功能强大且广泛使用的安全框架,为 Java 应用程序提供了全面的安全解决方案。本文将深入介绍 Spring Security 的基本概念、核心功能以及如何在应用程序中使用它来实现认证和授权。

一、Spring Security

Spring Security 是一个基于 Spring 的安全性框架,用于提供身份验证、授权、攻击防护等安全服务。它构建在 Spring 框架之上,利用依赖注入和 AOP 等功能,使得集成到现有的 Spring 应用程序中非常简单。

在开始深入了解 Spring Security 之前,我们需要了解几个核心概念:

  1. Authentication(认证):验证用户的身份,通常是通过用户名和密码进行。
  2. Authorization(授权):确定用户是否有权限执行特定操作或访问特定资源。
  3. Principal(主体):代表当前用户的抽象概念,通常是一个实现了 UserDetails 接口的对象。
  4. Granted Authority(授权权限):表示用户具有的权限,通常是角色或权限的集合。
  5. Access Control(访问控制):定义了哪些用户可以访问应用程序的哪些部分以及如何限制对资源的访问。

二、快速入门

  1. 添加 Spring Security 依赖项:首先,在 Spring Boot 项目中添加 Spring Security 的依赖项。

    <dependencies><!-- Spring Boot Web Starter 依赖,包含了开发 web 应用所需的所有基础依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Boot Security Starter 依赖,包含了开发安全应用所需的所有基础依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
    </dependencies><dependencyManagement><dependencies><!-- Spring Boot Dependencies POM,用于管理 Spring Boot 项目的所有依赖的版本 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.7.5</version><type>pom</type><scope>import</scope></dependency></dependencies>
    </dependencyManagement>
    
  2. 配置安全策略:创建一个配置类来配置 Spring Security 的行为。

    @Configuration
    @EnableWebSecurity // 注解启用 Spring Security 的 web 安全支持
    public class SecurityConfig {
    }
    
  3. 定义一个访问端点:定义一个测试使用的访问端点

    @RestController
    @RequestMapping("/test")
    public class TestController {@RequestMapping("/hello")public String hello() {return "hello spring security";}
    }
    
  4. 运行应用程序:运行 Spring Boot 应用程序,并尝试访问端点 localhost:8080/test/hello
    image.png

    默认账号是 user,密码如下图所示:

    image.png

    输入默认账号、密码之后:

    image.png

三、核心功能

3.1 身份验证(Authentication)

Spring Security 提供多种身份验证机制,包括基本认证、表单认证、OAuth、LDAP 等。支持自定义身份验证流程,可以根据应用程序的需求进行定制。开发人员可以根据需要配置请求路径是否需要认证才能访问。例如:

@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests().requestMatchers(new AntPathRequestMatcher("/test/hello")).permitAll() // 允许所有用户访问 "/test/hello" 路径.anyRequest().authenticated(); // 表示所有其他的请求都需要经过认证return http.build();}
}

3.2 授权(Authorization)

Spring Security 可以基于角色(Role-Based Access Control)和权限(Permission-Based Access Control)的访问控制。

@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests()// 只有具有 "add" 权限的用户才能访问 "/test/hello" 路径.requestMatchers(new AntPathRequestMatcher("/test/hello")).hasAuthority("add");return http.build();}
}

3.3 漏洞防护(Protection Against Exploits)

Spring Security 可以防范常见的攻击,如跨站点请求伪造(CSRF)、点击劫持等。例如,通过如下配置可开启 CSRF 保护:

@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {// 配置跨站请求伪造(CSRF)的保护http.csrf();return http.build();}
}

四、高级功能

4.1 会话管理(Session Management)

Spring Security 提供了会话管理的功能,包括会话超时、并发登录限制、会话固定攻击防护等功能。例如,可以通过如下配置开启并发登录限制:

@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.formLogin() // 启用了表单登录.and().authorizeRequests().anyRequest().authenticated() // 所有请求都需要经过认证.and().sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 设置会话创建策略为无状态,即 Spring Security 不会创建会话.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true); // 设置每个用户的最大并发会话数为 1,并且当达到最大并发会话数时,阻止新的登录请求return http.build();}
}

4.2 密码编码(Password Encoding)

Spring Security 提供密码加密和验证机制,确保用户密码的安全性。例如,要使用 BCrypt 加密,只需进行如下配置即可:

@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

4.3 基于注解的方法级安全控制

Spring Security 允许在方法上使用注解的方式进行访问控制。

@Secured("ROLE_ADMIN")
public void secureMethod() {// 只有具有 ROLE_ADMIN 角色的用户可以访问
}@PreAuthorize("hasRole('ADMIN')")
public void preAuthorizeMethod() {// 在方法调用之前进行授权检查
}

要使用注解的方式,需要使用 @EnableMethodSecurity 注解开启这一功能

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启基于注解的方法级安全控制
public class SecurityConfig {}

除此之外,Spring Security 支持使用表达式语言 (SpEL) 来定义复杂的访问控制规则。

@PreAuthorize("hasRole('ROLE_USER') and #id == principal.id")
public void updateUser(Long id) {// 只有具有 ROLE_USER 角色且 id 等于当前用户的 principal.id 的用户可以访问
}

4.4 事件监听

Spring Security 允许监听安全事件,例如登录成功、失败、登出等,以便记录日志或执行其他操作。

@Component
public class AuthenticationEventListener implements ApplicationListener<AuthenticationSuccessEvent> {@Overridepublic void onApplicationEvent(AuthenticationSuccessEvent event) {// 处理认证成功事件}
}

五、工作原理

Spring Security 的设计有那么亿点复杂,我们通过分开描述其核心工作流程、认证工作流程、鉴权工作流程来阐述其工作原理。

5.1 核心工作流程

Spring Security 的核心工作流程如下:

  1. 用户发送请求:用户通过浏览器或客户端发送请求到应用程序。
  2. 请求进入过滤器链代理(FilterChainProxy):所有请求首先进入 FilterChainProxy,它是一个标准的 Servlet 过滤器 (javax.servlet.Filter)。FilterChainProxy 的作用是根据请求的路径(URL)匹配合适的 SecurityFilterChain。
  3. 请求进入安全过滤器链(SecurityFilterChain):匹配到相应的 SecurityFilterChain 后,请求进入该安全过滤器链。SecurityFilterChain 是由多个安全过滤器(SecurityFilter)组成的序列,每个安全过滤器按顺序处理请求。
  4. 执行安全过滤器(SecurityFilter):每个安全过滤器 (SecurityFilter) 负责执行特定的安全操作和策略,例如身份认证、授权、会话管理等。
  5. 调用认证管理器(AuthenticationManager)AuthenticationManager 是 Spring Security 的核心接口之一,负责处理认证请求。在认证过程中,AuthenticationManager 通常会使用 AuthenticationProvider 来进行具体的认证操作。
  6. 返回认证结果:AuthenticationManager 返回认证结果给 SecurityFilter
  7. 返回过滤器链:认证完成后,SecurityFilter 可能会进行一些额外的安全处理,并将请求继续传递给下一个安全过滤器或者返回给 SecurityFilterChain
  8. 返回过滤器链代理:处理完所有安全过滤器后,请求最终返回到 FilterChainProxy
  9. 返回响应FilterChainProxy 将最终的响应返回给用户,完成整个请求-响应周期。

在这里插入图片描述

FilterChainProxy

  • 作用:FilterChainProxy 是 Spring Security 中的一个核心组件,它负责管理 Spring Security 中的各种过滤器链。当一个 HTTP 请求到达应用程序时,FilterChainProxy 会将该请求传递给一个或多个 SecurityFilterChain 实例进行处理。如果需要,FilterChainProxy 还可以重定向请求或返回错误信息。
  • 设计目的:FilterChainProxy 的设计目的是提供一个统一的入口点,用于管理和协调 Spring Security 中的所有过滤器链。
  • 例子:假设我们正在开发一个 Web 应用程序,该应用程序有多个端点,如 /public/user/admin。我们可能希望 /public 端点对所有人开放,/user 端点只对已登录用户开放,而 /admin 端点只对管理员开放。在这种情况下,你可以使用 FilterChainProxy 来管理三个不同的过滤器链,每个过滤器链负责一个特定的端点。

SecurityFilterChain

  • 作用:SecurityFilterChain 是一个顶层接口。SecurityFilterChain 和 Servlet 中的 FilterChain 一样,同样维护了很多 Filter,这些 Filter 由 Spring Security 提供,每个 Filter 具有不同的职能。
  • 设计目的:SecurityFilterChain 的设计目的是为了支持添加一个或多个 SecurityFilterChain,每个SecurityFilterChain 负责不同的请求(比如依据请求地址进行区分),这样可以为不同的请求设置不同的认证规则
  • 例子:继续上面的例子,我们可能会为 /public 端点创建一个 SecurityFilterChain,该过滤器链包含一个检查请求是否为 GET 的过滤器。对于 /user 和 /admin 端点,我们可能会创建包含身份验证过滤器的 SecurityFilterChain,该过滤器检查用户是否已登录,并根据用户的角色(用户或管理员)授予相应的权限。

SecurityFilter

  • 作用:SecurityFilter 是 Spring Security 的过滤器,每个过滤器负责处理特定的安全任务。当请求到达应用程序时,它会依次通过过滤器链中的每个过滤器,直到到达目标资源。在过滤器链中,每个过滤器都可以对请求进行拦截、修改或执行其他操作,以确保应用程序的安全性。
  • 设计目的:SecurityFilter 的设计目的是为了提高 Web 应用程序的安全性、可维护性和可扩展性。
  • 例子:在上述 SecurityFilterChain 中,我们可能会使用多个 SecurityFilter。例如,一个 SecurityFilter 可能会检查请求是否为 GET,另一个 SecurityFilter 可能会检查用户是否已登录,还有一个 SecurityFilter 可能会根据用户的角色授予相应的权限。

AuthenticationManager

  • 作用:AuthenticationManager 是 Spring Security 中的认证管理器,用来对登录请求进行处理。当处理用户的登录请求时,例如在使用表单登录时,AuthenticationManager 的 authenticate 方法会被调用来处理请求。
  • 设计目的:AuthenticationManager 这个接口的设计目的是对用户的未授信凭据进行认证,认证通过则返回授信状态的凭据,否则将抛出认证异常 AuthenticationException。
  • 例子:当用户尝试登录时,在登录表单中输入用户名和密码,这些凭据将被传递给 AuthenticationManager。AuthenticationManager 会检查这些凭据是否有效。如果凭据有效,AuthenticationManager 将创建一个已认证的 Authentication 对象,该对象包含用户的详细信息和授权。如果凭据无效,AuthenticationManager 将抛出一个异常。

在这里插入图片描述

5.2 认证工作流程

认证是验证用户身份的过程,通常通过用户名和密码、数字证书或生物特征等手段进行。通过 Spring Security 的核心工作流程我们可以知道:Spring Security 具体的认证工作是交由 AuthenticationManager 执行的

AuthenticationManager 的认证流程如下:

  1. 用户提交凭证:用户向系统提交用户名和密码等凭证信息。
  2. 认证管理器 (AuthenticationManager):接收到凭证后,AuthenticationManager 负责进行认证。
  3. 认证提供者 (AuthenticationProvider):AuthenticationManager 调用认证方法,该方法会委托给配置的 AuthenticationProvider(认证提供者)。
  4. 用户详情服务 (UserDetailsService):认证提供者通过调用 UserDetailsService 加载用户的详细信息,通常是根据用户名加载用户对象。
  5. 用户详情 (UserDetails):UserDetailsService 返回一个实现了 UserDetails 接口的用户详情对象,其中包含了用户的详细信息和权限。
  6. 创建认证信息 (Authentication):认证提供者使用 UserDetails 对象创建一个 Authentication 对象,表示成功的认证。
  7. 安全上下文持有者 (SecurityContextHolder):创建的 Authentication 对象被存储到 SecurityContextHolder 中,以便后续的访问控制和安全操作使用。
  8. 返回认证结果:最终,认证结果以 Authentication 对象的形式返回给用户,表示用户已经成功通过认证。

在这里插入图片描述

Spring Security 的认证流程看似蛮复杂的。其实,它的认证流程和我们常规的认证方式是类似的。

  • AuthenticationManager:不进行具体的认证处理,负责管理多个 AuthenticatonProvider,具体的认证交由能够处理当前认证的 AuthenticationProvider。
  • AuthenticationProvider:进行具体认证,通过调用 UserDetailService 从数据库或者其他地方加载用户信息。之后通过与用户提交的凭证信息进行匹配,若成功则生成 Authentication 对象,表示认证成功,反之,返回认证失败信息。
  • UserDetailsService:它的主要作用是根据用户名加载用户的详细信息。加载的具体逻辑一般由开发人员实现
  • UserDetails:用于表示用户的基本身份和授权信息(代表一个用户)。
  • Authentication:用于表示用户在系统中的身份认证信息。具体来说,Authentication 接口主要用于封装认证过程中的关键信息,如认证的主体(Principal)、凭证(Credentials)、授权信息(Authorities)等。
  • SecurityContextHolder:主要用于存储和访问当前用户的 Authentication 对象,即表示当前用户身份认证信息的实例。

在这里插入图片描述

5.3 鉴权工作流程

鉴权是在确认用户身份后,决定用户是否有权访问特定资源或执行特定操作的过程。Spring Security 鉴权流程通常是在认证完成之后,即生成了 Authentication 对象之后进行的。其具体流程如下:

  1. AuthorizationFilter 授权过滤器:通过认证的请求被送到 AuthorizationFilter,它是 Spring Security 中的一个过滤器,负责处理所有的请求,并开启鉴权过程
  2. 获取 Authentication 认证信息:通过 SecurityContextHolder 从当前的安全上下文中获取用户的认证信息 (Authentication),这包括用户的身份凭证和权限信息。
  3. 调用 AccessDecisionManager 访问决策管理器:AuthorizationFilter 调用 AccessDecisionManager 进行实际的访问决策。AccessDecisionManager 是一个核心组件,负责确定是否允许用户访问请求的资源或操作。AccessDecisionManager 可能会使用多个 AccessDecisionVoter 进行投票。AccessDecisionVoter 是决策的实际执行者,根据用户的认证信息和访问请求,投票是否允许访问资源或执行操作。
  4. 返回投票结果:每个 AccessDecisionVoter 根据自身的逻辑判断是否允许访问。投票结果将汇总给 AccessDecisionManager
  5. 返回访问决策结果AccessDecisionManager 将所有 AccessDecisionVoter 的投票结果综合起来,最终决定是否允许用户访问请求的资源或操作。
  6. 返回响应AuthorizationFilter 将处理的结果返回给用户,响应用户的请求,这可能包括成功的访问授权或者拒绝访问的信息。

在这里插入图片描述

其实,鉴权的逻辑还是比较简单的,只是流程比较多,可以概括为:

  1. 首先,AuthorizationFilter 获取登录用户的认证信息(Authentication
  2. 然后,AuthorizationFilter 调用 AccessDecisionManager 判断权限

六、前后端分离

在之前的快速入门中,我们发现 Spring Security 默认情况下是采用前后端不分离的方式进行认证,而现在我们的项目一般都是前后端分离的方式(即: 前端通过 RESTful API 与后端进行通信,后端负责处理认证和授权,而前端则通过获取后端返回的 JWT(JSON Web Token)来管理用户的身份验证和授权状态)。要实现这一需求,我们可以参考如下步骤:

  1. 添加依赖:首先,在 pom.xml 文件中添加 Spring Security 和 JWT 的依赖:

    <dependencies><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><!-- JWT 依赖 --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> <!-- 根据需要选择合适的版本 --></dependency>
    </dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.7.5</version><type>pom</type><scope>import</scope></dependency></dependencies>
    </dependencyManagement>
    
  2. 创建用户服务实现类:创建一个实现 UserDetailsService 接口的服务类,用于从数据库加载用户信息,并将其返回给 Spring Security 进行认证和授权。

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {// 模拟从数据库中查找用户@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 检查传入的用户名是否为"zs"if (!"zs".equals(username)) {// 如果用户名不是"zs",返回 null,表示没有找到对应的用户return null;}// 创建并返回一个 UserDetails 对象,表示用户的详细信息return User.builder().username("zs") // 设置用户名为"zs".password("$2a$16$RBoXNEqVxxtZ5l1QrJaMPub32Z8Q/e01tIG1Irs9ThxfXgeWxV1jq") // 设置加密后的密码.authorities("add") // 设置用户的权限为"add".build();}
    }
    
  3. 创建 JWT 工具类: 创建一个 JWT 工具类来生成和验证 JWT。

    @Component
    public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private long expiration;// 从令牌中提取用户名public String extractUsername(String token) {return extractClaim(token, Claims::getSubject);}// 从令牌中提取过期时间public Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration);}// 提取令牌中的声明public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {final Claims claims = extractAllClaims(token);return claimsResolver.apply(claims);}// 解析令牌private Claims extractAllClaims(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}// 验证令牌是否过期private Boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}// 生成令牌public String generateToken(UserDetails userDetails) {return Jwts.builder().setSubject(userDetails.getUsername()).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)).signWith(SignatureAlgorithm.HS256, secret).compact();}// 验证令牌public Boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}
    }
    
  4. 配置 JWT 相关属性:在 application.propertiesapplication.yml 中配置 JWT 的密钥和过期时间:

    jwt: # 配置JWT相关的属性secret: secretKey # 用于签名和验证JWT令牌的密钥expiration: 86400 # JWT令牌的有效期,以秒为单位。这里设置为86400秒(即24小时)
    
  5. 创建 JWT 认证过滤器:创建一个 JWT 认证过滤器来拦截每个请求,并验证 JWT。

    // 这个类继承自 OncePerRequestFilter,确保在每次请求时只调用一次过滤器
    @Component
    public class JwtRequestFilter extends OncePerRequestFilter {// 注入JwtUtil工具类@Resourceprivate JwtUtil jwtUtil;// 注入UserDetailsServiceImpl类,用于加载用户详细信息@Resourceprivate UserDetailsServiceImpl userDetailsService;// 重写OncePerRequestFilter的doFilterInternal方法,用于处理每个HTTP请求@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {// 从请求头中获取Authorization信息final String authorizationHeader = request.getHeader("Authorization");// 初始化用户名和JWT令牌变量String username = null;String jwt = null;// 检查Authorization头是否以"Bearer "开头if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {// 提取JWT令牌(去掉"Bearer "部分)jwt = authorizationHeader.substring(7);// 使用jwtUtil从令牌中提取用户名username = jwtUtil.extractUsername(jwt);}// 如果用户名存在且当前没有已认证的用户if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {// 加载用户详细信息UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);// 验证JWT令牌if (jwtUtil.validateToken(jwt, userDetails)) {// 创建UsernamePasswordAuthenticationToken对象,包含用户详细信息和权限UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());// 将认证信息设置到SecurityContextHolder中SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}}// 继续过滤链,处理下一个过滤器或目标资源chain.doFilter(request, response);}
    }
    
  6. 配置 Spring Security:开启 Spring Security 验证,配置 SecurityFilterChain。

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {// 注入JwtRequestFilter对象,用于处理JWT认证@Resourceprivate JwtRequestFilter jwtRequestFilter;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf().disable() // 禁用CSRF保护.authorizeRequests() // 配置请求授权.antMatchers("/login").permitAll() // 对于"/login"路径,允许所有请求(无需认证).anyRequest().authenticated() // 对于所有其他请求,需要认证.and()// 在UsernamePasswordAuthenticationFilter之前添加JwtRequestFilter.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}@Beanpublic AuthenticationManager authenticationManager(UserDetailsService userDetailsService,PasswordEncoder passwordEncoder) {// 创建一个DaoAuthenticationProviderDaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();// 设置UserDetailsServiceauthenticationProvider.setUserDetailsService(userDetailsService);// 设置PasswordEncoderauthenticationProvider.setPasswordEncoder(passwordEncoder);// 返回包含这个认证提供者的ProviderManagerreturn new ProviderManager(authenticationProvider);}@Beanpublic BCryptPasswordEncoder passwordEncoder() {// 创建一个强度为16的BCryptPasswordEncoderreturn new BCryptPasswordEncoder(16);}
    }
    
  7. 创建认证接口和控制器:创建一个认证控制器来处理用户登录请求,并返回 JWT 给前端。

    @RestController
    public class AuthController {@Resourceprivate AuthenticationManager authenticationManager;@Resourceprivate JwtUtil jwtUtil;@Resourceprivate UserDetailsServiceImpl userDetailsService;@PostMapping("/login")public ResponseEntity<String> createAuthenticationToken(@RequestBody LoginRequest loginRequest) {// 创建一个未认证的UsernamePasswordAuthenticationToken对象UsernamePasswordAuthenticationToken authenticationToken =UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.getUsername(), loginRequest.getPassword());// 调用AuthenticationManager的authenticate方法进行用户认证authenticationManager.authenticate(authenticationToken);// 加载用户详细信息UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());// 生成JWT令牌String jwt = jwtUtil.generateToken(userDetails);// 返回包含JWT令牌的响应return ResponseEntity.ok(jwt);}
    }
    
  8. 测试效果

    首先,访问 localhost:8080/login 获取到 token

    image.png

    然后,使用 token 访问 localhost:8080/test/hello

    image.png

七、小结

Spring Security 提供了强大而灵活的安全解决方案,可以轻松集成到 Spring 应用程序中。但是我们不难发现 Spring Security 设计得确实复杂了那么亿点点 (¬‿¬)。Spring Security 的使用门槛虽然较高,但是如果明白了它的原理便可以无缝与 Spring 结合使用,在日常的开发中可以极大的提高开发效率,增强应用的安全性。

推荐阅读

  1. 深入探究 Spring Boot Starter:从概念到实践
  2. 深入理解 Java 中的 volatile 关键字
  3. OAuth 2.0:现代应用程序的授权标准
  4. Spring 三级缓存
  5. 深入了解 MyBatis 插件:定制化你的持久层框架

相关文章:

构建安全稳定的应用:Spring Security 实用指南

前言 在现代 Web 应用程序中&#xff0c;安全性是至关重要的一个方面。Spring Security 作为一个功能强大且广泛使用的安全框架&#xff0c;为 Java 应用程序提供了全面的安全解决方案。本文将深入介绍 Spring Security 的基本概念、核心功能以及如何在应用程序中使用它来实现…...

嵌入式STM32F103项目实例可以按照以下步骤进行构建和实现

嵌入式STM32F103项目实例可以按照以下步骤进行构建和实现&#xff1a; 1. 项目概述 目标&#xff1a;演示STM32F103开发板的基本功能&#xff0c;通过LED闪烁来实现。硬件需求&#xff1a;STM32F103开发板、LED灯、杜邦线、USB转串口模块&#xff08;可选&#xff0c;用于调试…...

2024最新Stable Diffusion【插件篇】:SD提示词智能生成插件教程!

前言 今天我们介绍几款可以自动生成提示词的插件。所谓智能生成提示词&#xff0c;就是我们只需要输入非常少量的关键字&#xff0c;插件就会根据关键词提示信息帮助我们生成一系列关键字或者句子作为提示词。下面来和我一起看看吧。 一. SD智能提示词工具 之前的文章中和大…...

彻底学会Gradle插件版本和Gradle版本及对应关系

看完这篇&#xff0c;保你彻底学会Gradle插件版本和Gradle版本及对应关系&#xff0c;超详细超全的对应关系表 需要知道Gradle插件版本和Gradle版本的对应关系&#xff0c;其实就是需要知道Gradle插件版本对应所需的gradle最低版本&#xff0c;详细对应关系如下表格&#xff0…...

p2p、分布式,区块链笔记: 通过libp2p的Kademlia网络协议实现kv-store

Kademlia 网络协议 Kademlia 是一种分布式哈希表协议和算法&#xff0c;用于构建去中心化的对等网络&#xff0c;核心思想是通过分布式的网络结构来实现高效的数据查找和存储。在这个学习项目里&#xff0c;Kademlia 作为 libp2p 中的 NetworkBehaviour的组成。 以下这些函数或…...

ShareSDK iOS端如何实现小红书分享

下载SDK 请登陆官网 &#xff0c;找到SDK下载&#xff0c;勾选需要的平台下载 导入SDK &#xff08;1&#xff09;离线导入将上述下载到的SDK&#xff0c;直接将整个SDK资源文件拖进项目里&#xff0c;如下图&#xff1a; 并且勾选以下3个选项 在点击Finish&#xff0c;…...

算法day1 两数之和 两数相加 冒泡排序 快速排序

两数之和 最简单的思维方式肯定是去凑两个数&#xff0c;两个数的和是目标值就ok。这里两遍for循环解决。 两数相加 敲了一晚上哈哈&#xff0c;结果超过int范围捏&#xff0c;难受捏。 public class Test2 {public static void main(String[] args) { // ListNode l1 …...

Rust监控可观测性

可观测性 在监控章节的引言中&#xff0c;我们提到了老板、前端、后端眼中的监控是各不相同的&#xff0c;那么有没有办法将监控模型进行抽象、统一呢&#xff1f; 来简单分析一下&#xff1a; 业务指标实时展示&#xff0c;这是一个指标型的数据( metric )手机 APP 上传的数…...

SVN 的忽略(Ignore)和递归(Recursively)以及忽略部分

SVN中忽略大家经常用到&#xff0c;但总是似懂非懂&#xff0c;下面就详细展开说明一下忽略如何设置。 两个忽略 通常设置忽略都是文件夹和里面的文件都忽略。 设置忽略我们通常只需要鼠标右键点击忽略就可以了&#xff0c;如图&#xff1a; 第一个忽略用的最多&#xff0c;…...

vue3开发过程中遇到的一些问题记录

问题&#xff1a; vue3在使用 defineProps、defineEmits、defineExpose 时不需要import&#xff0c;但是 eslint会报错error defineProps is not defined no-undef 解决方法&#xff1a; 安装 vue-eslint-parser 插件&#xff0c;在 .eslintrc.js 文件中添加配置 parser: vue-e…...

Jedis、Lettuce、RedisTemplate连接中间件

jedis就像jdbc一样&#xff0c;用于两个端直接的连接。 1.创建Spring项目 这里不过多赘述... 2.导入连接工具jedis 在pom文件中导入jedis的依赖。 <dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version&…...

【C++】继承(详解)

前言&#xff1a;今天我们正式的步入C进阶内容的学习了&#xff0c;当然了既然是进阶意味着学习难度的不断提升&#xff0c;各位一起努力呐。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质量&#xff23;学习 &#x1f448; &#…...

网络io与select,poll,epoll

前言 网络 IO&#xff0c;会涉及到两个系统对象&#xff0c;一个是用户空间调用 IO 的进程或者线程&#xff0c;另一个是内核空间的内核系统&#xff0c;比如发生 IO 操作 read 时&#xff0c;它会经历两个阶段&#xff1a; 1. 等待数据准备就绪 2. 将数据从内核拷贝到进程或…...

【Linux】多线程(一万六千字)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 线程的概念 线程的理解(Linux系统为例) 在Linux系统里如何保证让正文部分的代码可以并发的去跑呢&#xff1f; 为什么要有多进程呢&#xff1f; 为…...

sh脚本笔记2

test条件测试 语法 条件测试语法说明语法1&#xff1a;test <测试表达式>这是利用test命令进行条件测试表达式的方法。test命令和“<测试表达式>”之间至少有一个空格语法2&#xff1a;[ <测试表达式> ]这是通过[]&#xff08;单中括号&#xff09;进行条件…...

js替换对象里面的对象名称

data为数组&#xff0c;val为修改前的名称&#xff0c;name为修改后的名称 JSON.parse(JSON.stringify(data).replace(/val/g, name)) &#xff1b; 1.替换data里面的对象tenantInfoRespVO名称替换成tenantInfoUpdateReqVO 2.替换语句&#xff1a; 代码可复制 let tenantInf…...

鸿蒙开发设备管理:【@ohos.settings (设置数据项名称)】

设置数据项名称 说明&#xff1a; 本模块首批接口从API version 8开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 本模块提供设置数据项的访问功能相关接口的说明及示例。 导入模块 import settings from ohos.settings;settings.getUri…...

STM32之五:TIM定时器(2-通用定时器)

目录 通用定时器&#xff08;TIM2~5&#xff09;框图 1、 输入时钟源选择 2、 时基单元 3 、输入捕获&#xff1a;&#xff08;IC—Input Capture&#xff09; 3.1 输入捕获通道框图&#xff08;TI1为例&#xff09; 3.1.1 滤波器&#xff1a; 3.1.2 边沿检测器&#xf…...

【分布式系统】监控平台Zabbix对接grafana

以前两篇博客为基础 【分布式系统】监控平台Zabbix介绍与部署&#xff08;命令截图版&#xff09;-CSDN博客 【分布式系统】监控平台Zabbix自定义模版配置-CSDN博客 一.安装grafana并启动 添加一台服务器192.168.80.104 初始化操作 systemctl disable --now firewalld set…...

操作系统真象还原:编写硬盘驱动程序

第13章-编写硬盘驱动程序 这是一个网站有所有小节的代码实现&#xff0c;同时也包含了Bochs等文件 13.1 硬盘及分区表 13.1.1 创建从盘及获取安装的磁盘数 要实现文件系统&#xff0c;必须先有个磁盘介质&#xff0c;虽然咱们己经有个虚拟磁盘 hd60M.img&#xff0c;但它只…...

firewalld防火墙(二)

一&#xff1a;firewalld高级配置 1&#xff1a;关于iptables的知识 iptables 是Linux系统中传统的命令行防火墙管理工具&#xff0c;它基于内核的netfilter框架工作&#xff0c;用于配置和管理网络规则集&#xff0c;比如过滤&#xff08;允许/拒绝&#xff09;进出的数据包…...

Android-悬浮窗口

在Android系统中&#xff0c;如果应用需要弹出一个悬浮窗口&#xff0c;就需要申请一项特殊权限 <uses-permission android:name"android.permission.SYSTEM_ALERT_WINDOW"/>在Android O之前的系统中申请了该权限后&#xff0c;再给对应的window设置 WindowM…...

打破僵局:Foxit Reader无法打开的终极解决方案

打破僵局&#xff1a;Foxit Reader无法打开的终极解决方案 在数字化阅读时代&#xff0c;Foxit Reader作为一款广受欢迎的PDF阅读器&#xff0c;其打不开的问题无疑会给用户带来诸多不便。本文将为您提供全面的解决方案&#xff0c;从基础检查到高级技巧&#xff0c;确保您能够…...

[调试] JTAG下运行正常,从QSPI或者SD卡启动则无响应,如何查找问题

[调试] JTAG下运行正常&#xff0c;从QSPI或者SD卡启动则无响应&#xff0c;如何查找问题 一、问题现象二、用自定义fsbl替代系统默认的fsbl1. 新建fsbl_new2. 如果提示缺少xilffs库3. 使能调试信息输出 三. 启动成功和失败情况下的典型输出1. JTAG启动模式: 正常加载2. QSPI启…...

Linux内核 -- 多线程之wait_event用法

Linux Kernel 中 wait_event 的高级用法及注意事项 在Linux内核编程中&#xff0c;wait_event 系列函数是用于实现进程等待和事件通知机制的重要工具。本文将详细介绍 wait_event 的高级用法以及注意事项。 1. 基本用法 wait_event 系列宏主要包括以下几种形式&#xff1a; …...

双指针系列第 8 篇:盛水最多的容器。几句话讲明白!

Leetcode 题目链接 思路 取首尾双指针和水量如下所示&#xff0c;设高度函数为 h ( i ) h(i) h(i)&#xff0c;在下图中 h ( l ) < h ( r ) h(l) < h(r) h(l)<h(r)。 观察以 l l l 为左边界所能构成的其他水量&#xff0c;与矮的右边界搭配结果如下。 与高的…...

c++高阶-1-模板

文章目录 模板一、模板基本语法二、函数模板1.基本语法2.函数模板注意事项3.普通函数和函数模板区别4.普通函数和函数模板调用规则 三、类模板1.基本语法2.类模板和函数模板的区别3.类模板中成员函数调用时机4.类模板对象做函数参数5.类模板与继承6.成员函数的类外实现 模板 一…...

.net core 的 winform 的 浏览器控件 WebView2

在.NET Core WinForms应用程序中&#xff0c;没有直接的“浏览器控件”&#xff0c;因为WinForms不支持像WebBrowser控件那样的功能。但是&#xff0c;你可以使用WebView2控件&#xff0c;它是一个基于Chromium的浏览器内核&#xff0c;可以在WinForms应用程序中嵌入Web内容。 …...

Django QuerySet对象,all()方法

all()方法 在Django中&#xff0c;all()方法是QuerySet对象的一个方法&#xff0c;用于获取模型的所有实例。 当你调用ModelName.objects.all()时&#xff0c;Django会生成一个SQL查询&#xff0c;从数据库中获取该模型的所有记录&#xff0c;并返回一个QuerySet对象&#xf…...

自动生成网站sitemap

要在 Next.js 和 Contentlayer 项目中实现自动生成 Sitemap 的功能&#xff0c;你可以编写一个脚本&#xff0c;在每次生成文档后自动生成 Sitemap。以下是一个示例脚本&#xff0c;你可以根据自己的需求进行调整。 步骤 1&#xff1a;安装必要的依赖 首先&#xff0c;你需要…...

中国经济昆虫志(55卷)

中国经济昆虫志&#xff0c;共55卷&#xff0c;内容包括概述、形态特征、分类等。各级分类单元均编有检索表&#xff0c;每个种有特征描述、地理分布&#xff0c;有的还记载有生活习性和防治方法。为便于鉴定&#xff0c;绘制有特征图和彩色图。 包括鞘翅目天牛科、半翅目蝽科、…...

linux环境安装elasticsearch缓存数据库和Kibana客户端

linux环境安装elasticsearch缓存数据库&#xff0c;今天我们安装7.17.18版本&#xff0c;并分析遇到的问题。 一、elasticsearch安装运行 1、直接下载 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.18-linux-x86_64.tar.gz2、解压 tar -…...

OpenSSL的一些使用案例

目录 一、介绍 二、基本使用 1、Shell &#xff08;1&#xff09;文件加解密 &#xff08;2&#xff09;生成密钥文件 2、API &#xff08;1&#xff09;md5sum &#xff08;2&#xff09;AES256加解密 一、介绍 本篇博客重点不是详细描述 OpenSSL 的用法&#xff0c;只…...

常用字符串方法<python>

导言 在python中内置了许多的字符串方法&#xff0c;使用字符串方法可以方便快捷解决很多问题&#xff0c;所以本文将要介绍一些常用的字符串方法。 目录 导言 string.center(width[,fillchar]) string.capitalize() string.count(sub[,start[,end]]) string.join(iterabl…...

线程池666666

1. 作用 线程池内部维护了多个工作线程&#xff0c;每个工作线程都会去任务队列中拿取任务并执行&#xff0c;当执行完一个任务后不是马上销毁&#xff0c;而是继续保留执行其它任务。显然&#xff0c;线程池提高了多线程的复用率&#xff0c;减少了创建和销毁线程的时间。 2…...

Python28-5 k-means算法

k-means 算法介绍 k-means 算法是一种经典的聚类算法&#xff0c;其目的是将数据集分成 ( k ) 个不同的簇&#xff0c;每个簇内的数据点尽可能接近。算法的基本思想是通过反复迭代优化簇中心的位置&#xff0c;使得每个簇内的点与簇中心的距离之和最小。k-means 算法的具体步骤…...

主流国产服务器操作系统技术分析

主流国产服务器操作系统 信创 "信创"&#xff0c;即信息技术应用创新&#xff0c;作为科技自立自强的核心词汇&#xff0c;在我国信息化建设的进程中扮演着至关重要的角色。自2016年起步&#xff0c;2020年开始蓬勃兴起&#xff0c;信创的浪潮正席卷整个信息与通信技…...

【Linux】线程封装与互斥(万字)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 C多线程的用法 对原生线程进行一次封装 理解pthread线程 Linux线程互斥 进程线程间的互斥相关背景概念 互斥量mutex 操作共享变量会有问题的售票…...

5分钟教你部署MySQL8.0环境

此方法基于Windows操作系统&#xff01; 一、在MySQL官网单击downloads&#xff08;下载&#xff09;MySQLhttps://www.mysql.com/cn/ 选择在Windows操作系统下载 二、选择合适的版本 推荐下载第二种&#xff0c;安装时离线安装即可 三、安装MySQL8.0 1、找到MySQL下载完成…...

LLM应用:传统NLP任务

LLM出来以后&#xff0c;知乎上就出现了“传统NLP已死”的言论&#xff0c;但是传统NLP真的就被扔进历史的垃圾桶了吗&#xff1f; 其实&#xff0c;尽管LLM具有出色的通用能力&#xff0c;但仍然无法有效应对低资源领域的自然语言处理任务&#xff0c;如小语种翻译。为了更好地…...

基于Hadoop平台的电信客服数据的处理与分析③项目开发:搭建Kafka大数据运算环境---任务11:基础环境准备

任务描述 任务主要是安装配置基础环境&#xff0c;主要内容包括&#xff1a; 1、安装java Kafka和ZooKeeper都需要安装Java环境&#xff0c;推荐至少Java8及以上版本 2、安装ZooKeeper ZooKeeper是Kafka集群的必要组件 3、安装kafka Kafka版本包括使用的scala语言版本和kafka版…...

Golang中swtich中如何强制执行下一个代码块

switch 语句中的 case 代码块会默认带上 break&#xff0c;但可以使用 fallthrough 来强制执行下一个 case 代码块。 package mainimport ("fmt" )func main() {isSpace : func(char byte) bool {switch char {case : // 空格符会直接 break&#xff0c;返回 false…...

读书笔记-Java并发编程的艺术-第4章(Java并发编程基础)-第2节(启动和终止线程)

文章目录 4.2 启动和终止线程4.2.1 构造线程4.2.2 启动线程4.2.3 理解中断4.2.4 过期的suspend()、resume()和stop()4.2.5 安全地终止线程 4.2 启动和终止线程 在前面章节的示例中通过调用线程的start()方法进行启动&#xff0c;随着run()方法的执行完毕&#xff0c;线程也随之…...

通俗大白话理解Docker

什么是Docker Docker本质上是一种容器化技术&#xff0c;用于将应用程序及其所有依赖打包到一个标准化的单元中。这些单元&#xff08;容器&#xff09;可以在任何运行Docker的机器上运行。每个容器是相互隔离的&#xff0c;具有自己的文件系统、网络和进程空间。 以下是大白话…...

题解:CF1981C(Turtle and an Incomplete Sequence)

题解&#xff1a;CF1981C&#xff08;Turtle and an Incomplete Sequence&#xff09; Part 1&#xff1a;题意理解 地址链接&#xff1a;CF、洛谷。题面翻译&#xff1a;给定一个长度为 n n n 的序列 a a a&#xff0c;其中有一些元素未知&#xff0c;用 − 1 -1 −1 表示…...

Swift 中强大的 Key Paths(键路径)机制趣谈(上)

概览 小伙伴们可能不知道&#xff1a;在 Swift 语言中隐藏着大量看似“其貌不扬”实则却让秃头码农们“高世骇俗”&#xff0c;堪称卧虎藏龙的各种秘技。 其中&#xff0c;有一枚“不起眼”的小家伙称之为键路径&#xff08;Key Paths&#xff09;。如若将其善加利用&#xff…...

(十二)纹理和采样

纹理 在绘制三角形的过程中&#xff0c;将图片贴到三角形上进行显示的过程&#xff0c;就是纹理贴图的过程 uv坐标 如果如果图片尺寸和实际贴图尺寸不一致&#xff0c;就会导致像素不够用了的问题 纹理与采样 纹理对象(Texture)&#xff1a;在GPU端&#xff0c;用来以一…...

QT创建地理信息shp文件编辑器shp_editor

空闲之余创建一个简单的矢量shp文件编辑器&#xff0c;加深对shp文件的理解。 一、启动程序 二、打开shp文件 三、显示shp文件的几何图形 四、双击右边表格中的feature&#xff0c;主窗体显示选中feature的各个节点。 五、鼠标在主窗体中选中feature的节点&#xff0c;按鼠标左…...

解析Kotlin中扩展函数与扩展属性【笔记摘要】

1.扩展函数 1.1 作用域&#xff1a;扩展函数写的位置不同&#xff0c;作用域就也不同 扩展函数可以写成顶层函数&#xff08;Top-level Function&#xff09;&#xff0c;此时它只属于它所在的 package。这样你就能在任何类里使用它&#xff1a; package com.rengwuxianfun …...

【Java学习笔记】java图形界面编程

在前面的章节中&#xff0c;我们开发运行的应用程序都没有图形界面&#xff0c;但是很多应用软件&#xff0c;如Windows下的Office办公软件、扑克牌接龙游戏软件、企业进销存ERP系统等&#xff0c;都有很漂亮的图形界面。素以需要我们开发具有图形界面的软件。 Java图形界面编程…...

Vue的服务器代理如何配置

在Vue项目中配置服务器代理&#xff0c;主要是为了解决开发过程中的跨域问题&#xff0c;以及方便地将前端请求转发到后端服务器。以下是在Vue项目中配置服务器代理的详细步骤和注意事项&#xff0c;主要基于Vue CLI进行说明&#xff1a; 一、配置步骤 1. 确认项目环境 确保…...

maven项目使用netty,前端是vue2,实现通讯

引入的java包 <!-- 以下是即时通讯--><!-- Netty core modules --><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.76.Final</version> <!-- 使用最新的稳定版本…...

【MindSpore学习打卡】应用实践-计算机视觉-深入解析 Vision Transformer(ViT):从原理到实践

在近年来的深度学习领域&#xff0c;Transformer模型凭借其在自然语言处理&#xff08;NLP&#xff09;中的卓越表现&#xff0c;迅速成为研究热点。尤其是基于自注意力&#xff08;Self-Attention&#xff09;机制的模型&#xff0c;更是推动了NLP的飞速发展。然而&#xff0c…...

马拉松报名小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;赛事信息管理&#xff0c;赛事报名管理&#xff0c;活动商城管理&#xff0c;留言板管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;赛事信息&…...

Linux Shell 脚本入门教程:开启你的自动化之旅

目录 一、什么是Shell&#xff1f; 二、 编写第一个Shell脚本 ​编辑 2.2 变量 2.3 功能语句 2.4 数组 一、什么是Shell&#xff1f; Shell是一种计算机程序&#xff0c;它充当了用户与操作系统之间的接口。在Linux系统中&#xff0c;Shell允许用户通过命令行界面&#x…...

半导体光电

《半导体光电》创刊于1976年&#xff0c;是由中国电子科技集团公司主管、重庆光电技术研究所&#xff08;中国电子科技集团公司第四十四研究所&#xff09;主办的中文科技期刊。本刊国内外公开发行&#xff0c;经过四十余年的发展已经成为我国光电子专业领域有代表性的刊物。 …...

彻底乱了!5米长豪华车,原价近30万,现仅售20万出头,还要啥凯美瑞?

今天说的这款车就是凯迪拉克CT5。凯迪拉克CT5车长接近5米,全系标配2.0T+10AT动力系统,整体性价比超高,实力剑指宝马3系、奥迪A4L。作为一款新美式豪华格调轿车——凯迪拉克CT5在中国市场销量超20万辆,运动轿车价值新标杆诞生!作为一款美系豪华中级轿车,凯迪拉克CT5近年来…...

刚上市订单破5万,奇瑞风云T9开启家用SUV豪华平权时代

5月21日,奇瑞风云T9正式上市,共推出4款车型,售价12.99-16.99万元,相比此前的预售价便宜了足足3万元。值得一提的是,在上市发布会结束时,新车的累计订单量已经突破了5万台。与此同时,奇瑞还为购车用户准备了至高价值34000元的惊喜6重礼,包括至高15000元置换补贴、价值60…...

富格林:借助正规技巧实现出金

富格林悉知&#xff0c;现货黄金近年来的表现相当出众&#xff0c;相信上车交易现货黄金的投资者&#xff0c;或多或少都在市场中分得一块蛋糕。不过也并不代表所有人都可以轻松在现货黄金中获利&#xff0c;尤其是投资新手。如果没有正规的投资经验观念&#xff0c;就很难实现…...

【Zotero】【MacOS】Zotero6常用插件总结

因为目前MacOS只支持Zotero6&#xff0c;所以我将网上找到的教程以及自己找到适应Zotero6版本的插件做了个整合 教程地址&#xff1a;Zotero6安装/插件安装教程 插件地址&#xff1a;Zotero6_Plugs...

在Spring中自定义事件及发布与监听

在Spring框架中&#xff0c;自定义事件及其发布与监听是一个涉及Spring事件机制的过程。Spring提供了一个基于观察者模式的事件发布和监听机制&#xff0c;允许在Spring容器中的组件之间进行松耦合的通信。以下是如何自定义事件以及如何发布和监听这些事件的步骤&#xff1a; …...

AI数字人及其应用

本文将简单了解下AI数字人、应用场景。 一、基本认识 AI数字人&#xff1a;使用人工智能技术创建的虚拟数字化人物。这些数字人物可以被设计成具有人类般的外观、行为和交互能力。它们通常用于虚拟现实、视频、游戏、培训模拟、客户服务等领域。 其中AI数字人在视频制作中是…...