一文上手SpringSecurity【五】

news2024/9/28 14:13:52

对于前后端不分离的项目,我们可以采用一文上手SpringSecurity【四】当中的方式来自定义用户的登录页面和数据源,数据源目前采用的是模拟的方式来实现的,本篇内容主要介绍一下spring security对于前后端分离项目如何实现认证和授权的.

一、前后端分离的认证面对的问题

1.1 传统的前后端不分离认证特点

  • 传统前后端不分离项目,Spring Security认证通常基于会话(session-based authentication),即用户登录成功后,服务器会在服务器端创建一个用户会话,并生成一个唯一的会话ID(JSESSIONID),然后通过Cookie发送给客户端。客户端的每次请求都会携带这个会话ID,服务器通过该ID在内存中查找用户的会话信息。这种方式的核心在于服务器要维护用户的会话状态,因此属于有状态认证。
  • 从身份认证流程上来说,用户通过浏览器访问服务器渲染的登录页面,提交用户名和密码后,Spring Security会处理表单请求(POST /login)。登录成功后,服务器会自动重定向到一个页面或返回相应的视图(例如首页),整个过程通过HTML表单、会话和页面跳转来完成。服务器不仅处理业务逻辑,还负责渲染页面【采用了模板引擎的项目】。
  • 跨域问题, 由于前端和后端在同一个服务器上运行,通常不会涉及跨域问题
  • 登录和登出的机制, 用户登录后,Spring Security会在服务器端维护会话,用户注销时,后端通过销毁会话来完成登出操作。这个过程通过Spring Security自带的表单登录机制进行。登出后,通常会通过服务器端重定向到登录页面或其他页面。
  • CSRF(跨站请求伪造)防护上, Spring Security默认启用了CSRF防护机制。在基于会话的认证中,服务器通过生成CSRF token并将其嵌入到HTML表单中,来防止CSRF攻击。当用户提交表单时,服务器验证CSRF token是否正确。
  • 登录成功或失败后,服务器通常会返回一个完整的HTML页面(或通过重定向实现),用户可以看到对应的视图。所有页面跳转和导航由后端控制,通过服务器端渲染实现页面切换.
  • 用户信息存储去传递, 用户信息通常存储在服务器的会话中,浏览器的每次请求都会自动携带会话ID,通过该ID服务器可以找到用户的身份信息。
    1

1.2 现代的前后端分离认证特点

  • 在前后端分离的项目中,通常使用JWT(JSON Web Token)或其他Token-based认证(如OAuth2)。用户登录成功后,服务器会生成一个Token(通常是JWT),客户端在每次请求时将Token放入HTTP请求头中(如Authorization: Bearer ),然后服务器通过解析Token来验证用户身份。这种方式不依赖于服务器的会话机制,服务器不会存储用户的状态信息(无状态认证),因此更适合前后端分离的场景。
  • 在前后端分离项目中,前端与后端之间通过API进行交互。用户提交用户名和密码时,通常会通过Ajax调用一个API端点(例如POST /api/login),Spring Security在后端验证后返回一个Token(例如JWT)。
    • 客户端(通常是单页应用,如React或Vue.js)会将Token保存在浏览器的localStorage或sessionStorage中,并在后续的请求中将其附加到HTTP头部。后端API通过验证Token来识别用户。
    • 前后端分离的项目通常不会依赖于服务器端的页面重定向,而是由前端框架自行控制页面跳转。
  • 前后端分离项目中,前端和后端通常运行在不同的域名或端口下,导致浏览器的同源策略限制。为了允许跨域请求,必须配置CORS(Cross-Origin Resource Sharing)策略。Spring Security需要通过配置允许特定的域名进行跨域访问,尤其是登录和获取资源的请求。
  • 用户登录后,Token会被保存在客户端(例如localStorage或sessionStorage)。登出时,客户端只需要删除存储的Token,并通知后端(如果需要),可以通过API让后端使Token失效(如Token黑名单)或删除服务器端的相关记录(如刷新Token)。
  • 由于Token-based认证(如JWT)使用的是无状态的HTTP头部认证,不依赖于Cookie和会话,因此通常不需要CSRF防护。可以通过禁用Spring Security的CSRF防护(http.csrf().disable())来适配前后端分离的项目。
  • 登录成功后,后端不会返回HTML页面,而是返回一个Token(例如JWT)或JSON格式的用户信息,前端通过处理API返回的结果进行页面的跳转和状态管理。
    • 所有的页面跳转和视图渲染由前端框架(如React、Vue、Angular)处理,后端只负责返回数据而不关心页面渲染。
  • 用户信息通常通过Token(如JWT)在客户端存储。Token中包含了用户的身份信息(如sub,exp等字段),后端通过解析Token来验证用户身份。Token无需存储在服务器,客户端携带Token的每次请求可以在无状态下进行身份认证。

2

1.3 前后端分离认证需要的准备工作

  • 前端工作, 采用vue3进行开发一个简单的页面
  • 认证凭证JWT,这里不做详细介绍,直接使用,默认已经熟悉
  • 跨域处理,直接在后端配置即可
  • 自定义认证流程逻辑编写
  • 测试

二、前端端分离认证实操

2.1 前端工程准备

  • 创建vue3 工程,别太复杂了.够用就行了.
  • 引入路由组件、Axios、Element Plus 按照官方文档配置
  • 相应的工程目录如下所示
    参考
    一共两个路由组件,登录和主页.效果展示:
    login
    目前没有登录逻辑,点击登录,直接进入主页,之后会在这里进行登录的认证请求.
    home
    具体内容填充,之后RBAC的时候再填充相应的内容.

2.2 搭建后端工程

  • JDK17
  • Spring boot 3.3.4

2.2.1 引入JWT工具类

关于jwt自行度娘之,这里jwt核心的作用就是: 当服务器认证完客户端之后, 发给客户端存储的用户凭证,认证通过之后,客户端的每次请求都要携带这个凭证

引入jwt需要的依赖,这里要注意,由于我们使用的是JDK 17,所以必须得引入JWT兼容高版本的依赖.

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt-api</artifactId>
   <version>0.11.2</version>
</dependency>
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt-impl</artifactId>
   <version>0.11.2</version>
   <scope>runtime</scope>
</dependency>
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
   <version>0.11.2</version>
   <scope>runtime</scope>
</dependency>
<!--解决高版本JDK问题-->
<!--javax.xml.bind.DatatypeConverter错误-->
<dependency>
   <groupId>javax.xml.bind</groupId>
   <artifactId>jaxb-api</artifactId>
   <version>2.3.0</version>
</dependency>
<dependency>
   <groupId>com.sun.xml.bind</groupId>
   <artifactId>jaxb-impl</artifactId>
   <version>2.3.0</version>
</dependency>
<dependency>
   <groupId>com.sun.xml.bind</groupId>
   <artifactId>jaxb-core</artifactId>
   <version>2.3.0</version>
</dependency>
<dependency>
   <groupId>javax.activation</groupId>
   <artifactId>activation</artifactId>
   <version>1.1.1</version>
</dependency>

JWT工具类封装,你也可以自己找找,实现功能即可.这玩意一找一大大堆

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.UUID;

public class JwtUtil {
    /**
     * jwt过期时间
     */
    public static final Long EXP_TTL = 60 * 60 * 1000L;

    /**
     * jwt使用的密钥
     */
    public static final String JWT_KEY = "c3R1ZHkgaGFyZCBhbmQgbWFrZSBwcm9ncmVzcyBldmVyeSBkYXku";

    /**
     * 创建jwt字符串
     * @param id id
     * @param issuer 创建的作者
     * @param subject 用户主体
     * @param ttlMillis 过期时间, 毫秒值
     * @return jwt字符串
     */
    public static String createJWT(String id, String issuer, String subject, long ttlMillis) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(JWT_KEY);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        JwtBuilder builder = Jwts
                .builder()
                .setId(id)
                .setIssuedAt(now)
                .setSubject(subject)
                .setIssuer(issuer)
                .signWith(signingKey, signatureAlgorithm);

        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * 创建jwt字符串
     * @param issuer 作者信息
     * @param subject 用户主体信息
     * @param ttlMillis 过期时间, 毫秒值
     * @return jwt字符串
     */
    public static String createJwt(String issuer, String subject, long ttlMillis){
        return createJWT(uuid(), issuer, subject, ttlMillis);
    }

    /**
     * 创建jwt字符串
     * @param id 作者信息
     * @param subject 用户主体信息
     * @return jwt字符串
     */
    public static String createJwt(String id, String subject){
        return createJWT(id, "yyds", subject, EXP_TTL);
    }

    /**
     * 创建jwt字符串
     * @param subject 用户主体
     * @return jwt字符串
     */
    public static String createJwt(String subject){
        return createJwt("rj", subject, EXP_TTL);
    }

    /**
     * uuid
     * @return String
     */
    private static String uuid(){
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    /**
     * 解析jwt
     * @param jwt  jwt字符串
     * @return  Claims
     */
    public static Claims parseJWT(String jwt) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(DatatypeConverter.parseBase64Binary(JWT_KEY))
                .build()
                .parseClaimsJws(jwt).getBody();

        return claims;
    }

    /**
     * 判断token是否过期
     * @param token
     * @return
     */
    public static boolean isTokenExpired(String token){
        Claims claims = parseJWT(token);
        return new Date(System.currentTimeMillis()).after(claims.getExpiration());
    }

    public static void main(String[] args) {
        String jwt = createJWT("1024", "rj", "rj", EXP_TTL);
        System.out.println(jwt);
        Claims claims = parseJWT(jwt);
        Object subject = claims.get("sub");
        System.out.println(subject);
        System.out.println(claims);
        System.out.println(claims.getSubject());
        System.out.println(claims.getIssuedAt());
        System.out.println(claims.getExpiration());
    }

执行一下main方法,验证一下工具类是否可以正常工作,另外这个工具类还可以扩展一下,将密钥、过期时间做成配置文件更好一些.
jwt2

2.2.2 跨域处理

解决跨域的问题的方式有很多种,这里咱们简单粗暴,在服务器端处理一下即可

@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 这里仅为了说明问题,配置为放行所有域名,生产环境请对此进行修改
        config.addAllowedOriginPattern("*");
        // 放行的请求头
        config.addAllowedHeader("*");
        // 放行的请求类型,有 GET, POST, PUT, DELETE, OPTIONS
        config.addAllowedMethod("*");
        // 暴露头部信息
        config.addExposedHeader("*");
        // 是否允许发送 Cookie
        config.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

注意导包别导错了:

  • import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
  • import org.springframework.web.filter.CorsFilter;

2.2.3 封装实体类

这里需要引入lombok

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SysUser {
    private String username;
    private String password;
}

2.2.4 统一结果返回工具类

后端返回给前端的数据,应该是统一格式,方便前端、后端处理,这里简单封装一下. 这里找你自己喜欢的就行,无所谓的.

@Data
public class Result implements Serializable {
	private static final long serialVersionUID = -3657911743622479730L;

	private int code;
	private String msg;
	private Object data;

	private Result(int code, String msg, Object data) {
		this.code = code;
		this.msg = msg;
		this.data = data;
	}

	public static Result success(int code, String msg, Object data){
		return new Result(code, msg, data);
	}

	public static Result success(String msg, Object data){
		return success(0, msg, data);
	}

	public static Result success(String msg){
		return success(0, msg, null);
	}

	public static Result error(int code, String msg, Object data){
		return new Result(code, msg, data);
	}

	public static Result error(int code, String msg){
		return error(code, msg, null);
	}
}

2.2.5 工程结构

66

2.3 测试前后端通信

前端点击登录按钮, 发送请求: localhost:9527/api/pub/v1/login, 在后端写一下登录接口,测试一下,看看能否正常通信.

67
返回数据
679
前端登录处理示例代码

const submitForm = (formEl: FormInstance | undefined) => {
    if (!formEl) return
    formEl.validate((valid) => {
        if (valid) {
        	// 发起请求
            login(ruleForm).then((res) => {
                if (res.code === 0) {
                    // 存储一下token
                    sessionStorage.setItem('token', res.data.token)
                    // 路由跳转
                    router.push({
                        name: 'home'
                    })
                }else {
                    ElMessage({
                        type: 'error',
                        message: res.msg,
                        showClose: true,
                    })
                }
            }).catch(error => {
                ElMessage.error(error)
            })
        } else {
            console.log('error submit!')
        }
    })
}

所以的工作都准备结束,下边开始实现一下自定义的认证逻辑

2.4 自定义认证整体流程分析

90
有了流程之后,我们就按照这个流程实现代码即可,接口我们处理完成,下边处理service层业务

2.4.1 认证核心业务逻辑

回顾一下默认的认证流程,看如下代码, 我们可以根据默认的流程,自己写认证流程.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
		throws AuthenticationException {
	if (this.postOnly && !request.getMethod().equals("POST")) {
		throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
	}
	String username = obtainUsername(request);
	username = (username != null) ? username.trim() : "";
	String password = obtainPassword(request);
	password = (password != null) ? password : "";
	UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
			password);
	// Allow subclasses to set the "details" property
	setDetails(request, authRequest);
	return this.getAuthenticationManager().authenticate(authRequest);
}
  • 封装用户名称和密码,
String username = obtainUsername(request);
	username = (username != null) ? username.trim() : "";
	String password = obtainPassword(request);
	password = (password != null) ? password : "";
	UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
			password);

其中username和password我们工程当中,由前端直接传递过来.然后封装成UsernamePasswordAuthenticationToken对象即可.

  • 使用认证管理器,调用认证方法
 this.getAuthenticationManager().authenticate(authRequest);

我们需要解决的是自己构建一个AuthenticationManager对象,我们可以在spring security配置文件当中进行配置

@Bean
 public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
     return authenticationConfiguration.getAuthenticationManager();
 }

综上所述我们就可以写自己的逻辑了.在service层,实现核心业务

@Service
public class SysUserServiceImpl implements ISysUserService {
    private final AuthenticationManager authenticationManager;

    public SysUserServiceImpl(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Result login(SysUser sysUser) {
        String username = sysUser.getUsername();
        String password = sysUser.getPassword();

        if(!StringUtils.hasText(username)){
            return Result.error(-1, "用户名称不能为空");
        }

        if(!StringUtils.hasText(password)){
            return Result.error(-2, "用户密码不能为空");
        }

        // 封装请求参数
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
                 = new UsernamePasswordAuthenticationToken(username, password);

        // 手动调用认证方法
        // 如果没有抛出异常,则表示认证成功,则返回一个完整对象,我们从中获取封装的UserDetails对象
        try {
            Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
            // 获取认证对象
            User user = (User) authenticate.getPrincipal();
            // 生成jwt
            String id = UUID.randomUUID().toString().replace("-", "");
            String token = JwtUtil.createJwt(id, user.getUsername());
            return Result.success(0, "登录成功", token);
        }catch (Exception e){
            e.printStackTrace();
        }

        return Result.error(-1, "用户名称或者密码错误");
    }
}

123
核心代码都添加注释了,自行查阅.

自己实现UserDetailsService接口, 这个先模拟操作一下,代码如下所示

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名称去数据当中查询出用户信息,这里还是先模拟一下
        String name = "admin";
        String password = "admin";
        
        // 如果根据用户名称没有查询到到用户信息,则抛出异常,这里模拟操作
        // 如果没有问题,则将用户信息封装成UserDetails对象
        return new User(name, password, Collections.emptyList());
    }
}

根据咱们的画的流程图,我们的核心业务已经完成,下边测试一下.

2.4.2 测试

890
返回服务器控制台,查看日志
log
PasswordEncoder是 Spring Security 中用于对用户密码进行加密和验证的接口。SpringSecurity默认对用户密码是加密的处理,我们必须得配置一个加密算法.其主要作用是

  • 密码加密:
    • 在用户注册或创建账户时,PasswordEncoder可以将用户输入的明文密码进行加密处理,然后将加密后的密码存储在数据库中。这样可以防止密码以明文形式存储,提高系统的安全性。
    • 常见的加密算法有 BCrypt、SHA-256 等。不同的加密算法具有不同的强度和特点。
  • 密码验证:
    • 在用户登录时,PasswordEncoder可以将用户输入的密码与存储在数据库中的加密密码进行比较和验证。它会使用相同的加密算法对用户输入的密码进行加密,然后与存储的加密密码进行匹配。
    • 如果匹配成功,则用户身份验证通过;否则,验证失败。

常用的加密器BCryptPasswordEncoder, 这是一种比较常用的密码编码器,它使用 BCrypt 哈希算法对密码进行加密。BCrypt 算法具有较高的安全性,因为它会自动生成随机的盐值(salt),并将盐值与密码一起进行加密。这样即使两个用户使用相同的密码,加密后的结果也会不同。

所以我们在配置文件当中配置PasswordEncoder即可.

@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}

其次,我们在数据库存储的密码应该使用BCryptPasswordEncoder进行加密处理,才能匹配上验证密码的算法.对默认的密码
admin处理一下

@Test
 public void passwordEncoder() {
     BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
     String pwd = bCryptPasswordEncoder.encode("admin");
     System.out.println(pwd);
 }

在UserDetailsServiceImpl当中修改一下

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 根据用户名称去数据当中查询出用户信息,这里还是先模拟一下
    String name = "admin";
    // String password = "admin";
    String password = "$2a$10$4/S6K/z/nF5eTk9KlF/PgOGtv2jlLGrzpO3oXINQAkNNlMqtVT6ru";

    // 如果根据用户名称没有查询到到用户信息,则抛出异常,这里模拟操作
    // 如果没有问题,则将用户信息封装成UserDetails对象
    return new User(name, password, Collections.emptyList());
}

再次测试
785
log1
token
至此, 整个认证流程完成一半了,剩下一半就是当认证成功之后,再次请求服务器接口,对token的校验了.我们放在下篇完成它.

2.5 完整配置文件

@Configuration
public class SpringSecurityConfig {
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeHttpRequests(authorize -> {
            try {
                authorize.requestMatchers("/api/pub/v1/login").permitAll()
                        .requestMatchers("/static/**", "/resources/**").permitAll()
                        .anyRequest().authenticated();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).csrf(AbstractHttpConfigurer::disable)
                .build();
    }
}

如果这里不使用PasswordEncoder,还可以在密码处理显示的标记一下: {noop}admin,表示使用明文密码,此种方式不是特别常用,可自行验证.

三、总结

3.1 重点内容

  • 前后端分离认证流程分析
  • 整个认证流程处理
  • PasswordEncoder

3.2 下篇内容

  • 验证token,完善自定义认证流程

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

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

相关文章

File systems

inode descriptor 文件系统中核心的数据结构就是inode和file descriptor。后者主要与用户进程进行交互。 inode&#xff0c;这是代表一个文件的对象&#xff0c;并且它不依赖于文件名。实际上&#xff0c;inode是通过自身的编号来进行区分的&#xff0c;这里的编号就是个整数…

修改 idea 的 Terminal 命令窗口使用 git-bash

修改配置方法 实际使用效果 &#xff08;END&#xff09;

Java Stream 神技!10招顶级技巧,让你的代码简洁又高效!

哈喽&#xff0c;欢迎来到【程序视点】&#xff0c;我是小二哥。 引言 你是否曾在编写Java代码时&#xff0c;为了处理集合而感到头痛不已&#xff1f;是否在寻找一种更优雅、更简洁的方式来简化你的代码&#xff1f; 如果你的答案是肯定的&#xff0c;那么Java Stream API无…

org.eclipse.paho.client.mqttv3.MqttException: 无效客户机标识

需求背景 最近有一个项目,需要用到阿里云物联网,不是MQ。发现使用原来EMQX的代码去连接阿里云MQTT直接报错,试了很多种方案都不行。最终还是把错误分析和教程都整理一下。 需要注意的是,阿里云物联网平台和MQ不一样。方向别走偏了。 概念描述 EMQX和阿里云MQTT有什么区别…

OpenCV视频I/O(6)检查视频捕获对象是否已成功打开的函数isOpened()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 如果视频捕获已经初始化&#xff0c;则返回 true。 如果之前调用 VideoCapture 构造函数或 VideoCapture::open() 成功&#xff0c;则该方法返回…

ireport 5.1 中文生辟字显示不出来,生成PDF报字体找不到

问题&#xff1a; 情况1&#xff1a;ireport中填入中文生辟字的时候不显示&#xff0c;或者无法输入和粘贴生辟字。 情况2&#xff1a;生成pdf的时候报字体找不到。 net.sf.jasperreports.engine.JRRuntimeException: Could not load the following font : pdfFontName : …

十分钟实现内网连接,配置frp

十分钟实现内网连接&#xff0c;配置frp 一.frp是什么&#xff1f;其实是一款实现外网连接内网的一个工具&#xff0c;个人理解&#xff0c;说白了就像是teamviwer一样&#xff0c;外网能访问内网。 利用处于内网或防火墙后的机器&#xff0c;对外网环境提供 http 或 https 服…

Python神经求解器去耦合算法和瓦瑟斯坦距离量化评估

&#x1f3af;要点 神经求解器求解对偶方程&#xff0c;并学习两个空间之间的单调变换&#xff0c;最小化它们之间的瓦瑟斯坦距离。使用概率密度函数解析计算&#xff0c;神经求解器去耦合条件正则化流使用变量变换公式的生成模型瓦瑟斯坦距离量化评估神经求解器 &#x1f36…

CSS06-元素显示模式、单行文字垂直居中

一、什么是元素显示模式 1-1、块级元素 1-2、行内元素 1-3、行内块元素 1-4、小结 二、元素显示模式转换 三、单行文字垂直居中 CSS 没有给我们提供文字垂直居中的代码&#xff0c;这里我们可以使用一个小技巧来实现。 解决方案: 让文字的行高等于盒子的高度&#xff0c;就可…

普通二叉搜索树的模拟实现【C++】

二叉搜素树简单介绍 二叉搜索树又称二叉排序树&#xff0c;是具有以下性质的二叉树: 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值 它的左右子树也分别为二叉搜索树 注意…

C++深入学习string类成员函数(4):字符串的操作

引言 在c中&#xff0c;std::string提供了许多字符串操作符函数&#xff0c;让我们能够秦松驾驭文本数据&#xff0c;而与此同时&#xff0c;非成员函数的重载更是为string类增添了别样的魅力&#xff0c;输入输出流的重载让我们像处理基本类型的数据一样方便地读取和输出字符…

51单片机系列-串口(UART)通信技术

&#x1f308;个人主页&#xff1a; 羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 并行通信和串行通信 并行方式 并行方式&#xff1a;数据的各位用多条数据线同时发送或者同时接收 并行通信特点&#xff1a;传送速度快&#xff0c;但因需要多根传输线&#xf…

20.指针相关知识点1

指针相关知识点1 1.定义一个指针变量指向数组2.指针偏移遍历数组3.指针偏移的补充4.指针和数组名的见怪不怪5.函数、指针、数组的结合 1.定义一个指针变量指向数组 指向数组首元素的地址 指向数组起始位置&#xff1a;等于数组名 #include <stdio.h>int main(){int ar…

LeetCode 2266. 统计打字方案数

Alice 在给 Bob 用手机打字。数字到字母的 对应 如下图所示。 为了 打出 一个字母&#xff0c;Alice 需要 按 对应字母 i 次&#xff0c;i 是该字母在这个按键上所处的位置。 比方说&#xff0c;为了按出字母 s &#xff0c;Alice 需要按 7 四次。类似的&#xff0c; Alice 需…

Qt --- Qt窗口

一、前言 前面学习的所有代码&#xff0c;都是基于QWidget控件。QWidget更多的是作为别的窗口的一个部分。 Qt中的QMainWindow就是窗口的完全体 Menu Bar菜单栏 Tool Bar Area 工具栏&#xff0c;类似于菜单栏&#xff0c;工具栏本质上就是把菜单中的一些比较常用的选项&…

活动展览棚:灵活多变的展览解决方案—轻空间

在快速变化的市场环境中&#xff0c;活动展览棚作为一种创新的展示空间&#xff0c;正受到越来越多企业和组织的青睐。无论是展览、活动、还是市场推广&#xff0c;活动展览棚都能提供高效、灵活的解决方案&#xff0c;为品牌传播和产品展示带来全新体验。 便捷的搭建与拆卸 活…

C. Cards Partition 【Codeforces Round 975 (Div. 2)】

C. Cards Partition 思路&#xff1a; 可以O(n)直接判断&#xff0c;牌组从大到小依次遍历即可。 不要用二分答案&#xff0c;因为答案不一定是单调的 代码: #include <bits/stdc.h> #define endl \n #define int long long #define pb push_back #define pii pair<…

Angular与Vue的全方位对比分析

一、框架概述 Angular Angular是由Google开发和维护的一款开源JavaScript框架。它采用TypeScript编写&#xff0c;具有一套完整的开发工具和规范。Angular遵循MVC&#xff08;Model - View - Controller&#xff09;或更确切地说是MVVM&#xff08;Model - View - ViewModel&a…

【Python】数据可视化之分布图

分布图主要用来展示某些现象或数据在地理空间、时间或其他维度上的分布情况。它可以清晰地反映出数据的空间位置、数量、密度等特征&#xff0c;帮助人们更好地理解数据的内在规律和相互关系。 目录 单变量分布 变量关系组图 双变量关系 核密度估计 山脊分布图 单变量分布…

超全面的线程编程实战指南

第一部分&#xff1a;线程基本概念 一、线程简介 线程是操作系统能够进行运算调度的最小单位&#xff0c;它是一个进程内的独立控制流。线程之间共享同一进程的资源&#xff0c;如内存空间和其他系统资源。 二、线程的优势 效率高&#xff1a;由于线程共享相同的地址空间&a…