【Java手搓RAGFlow】-3- 用户认证与权限管理

【Java手搓RAGFlow】-3- 用户认证与权限管理

  • [1 引言](#1 引言)
  • [2 JWT](#2 JWT)
    • [2.1 JWT组成](#2.1 JWT组成)
    • [2.2 JWT流程](#2.2 JWT流程)
    • [2.3 生成JWT Secret Key](#2.3 生成JWT Secret Key)
    • [2.4 创建JwtUtils](#2.4 创建JwtUtils)
  • [3 密码加密](#3 密码加密)
  • [4 实现User相关代码](#4 实现User相关代码)
    • [4.1 实现UserService](#4.1 实现UserService)
    • [4.2 实现CustomUserDetailsService](#4.2 实现CustomUserDetailsService)
  • [5 实现安全配置与认证流程](#5 实现安全配置与认证流程)
    • [5.1 实现JwtAuthenticationFilter](#5.1 实现JwtAuthenticationFilter)
    • [5.2 更新SecurityConfig](#5.2 更新SecurityConfig)
    • [5.3 实现认证控制器](#5.3 实现认证控制器)
  • [6 测试](#6 测试)
    • [6.1 测试注册接口](#6.1 测试注册接口)
    • [6.2 测试登录接口](#6.2 测试登录接口)

1 引言

在构建任何一个成熟的RAG系统之前,一个稳定可靠的用户认证与权限管理模块是必不可少的基础设施。它能确保系统的安全性,保护用户数据,并为后续的个性化服务和资源隔离打下基础。上一章我们已经搭建好了项目的基础骨架,现在,我们需要亲手为它填充上认证与权限管理的核心功能。

常见的认证方案有:

  • Session 认证:传统方案,需要服务器存储会话这是一种传统的认证方案。用户登录成功后,服务器会创建一个Session来存储用户信息,并将一个Session ID通过Cookie返回给浏览器。后续请求浏览器都会携带这个Session ID,服务器以此来识别用户。这种方案的优点是简单直观,但缺点是需要在服务器端存储会话信息,当系统需要水平扩展时,会面临Session共享的难题。
  • JWT 认证:它是一种无状态的认证方案。用户登录后,服务器会生成一个包含用户信息的加密Token并返回给客户端。客户端在后续请求中只需携带这个Token即可,服务器通过验证Token的签名来确认用户身份,无需在服务端存储任何会话信息。这使得它天然适合分布式系统和微服务架构。
  • OAuth2:它是一个关于授权的开放标准,更侧重于允许第三方应用访问用户在某个服务上的资源,而不是直接的用户认证。我们常见的"微信登录"、"GitHub登录"等第三方登录功能,就是基于OAuth2实现的。

我们最终选择 JWT 作为本项目的认证方案。主要原因有两点:一是我个人对它比较熟悉,开发效率更高;二是因为它的无状态特性,这对于未来系统可能的扩展和维护都极为友好。=v=

2 JWT

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。

2.1 JWT组成

JWT 由三部分组成:

java 复制代码
Header.Payload.Signature
  • Header:通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法(如HS256或RS256)。这部分内容会经过Base64Url

  • Payload:负载:也称为声明Claims,是存放有效信息的地方。这里可以包含用户ID、用户名、角色、过期时间等自定义数据。需要注意的是,Payload默认只经过Base64Url编码,并非加密,因此不应在其中存放任何敏感信息。

  • Signature:签名的作用是验证消息在传输过程中没有被篡改,并验证发送者的身份。它由三部分组合生成:编码后的Header、编码后的Payload、以及一个只有服务器知道的密钥(Secret),再通过Header中指定的签名算法进行加密生成。

2.2 JWT流程

JWT的认证流程清晰明了,具体步骤如下:

复制代码
1. 用户登录
   ↓
2. 服务器验证用户名密码
   ↓
3. 生成 JWT Token
   ↓
4. 返回 Token 给客户端
   ↓
5. 客户端存储 Token(localStorage/cookie)
   ↓
6. 后续请求携带 Token
   ↓
7. 服务器验证 Token
   ↓
8. 返回数据

2.3 生成JWT Secret Key

签名的密钥是JWT安全性的核心,它必须被妥善保管在服务端,绝不能泄露。我们可以通过下面的代码来生成一个足够安全的密钥。

创建com.alibaba.utils.GenerateJwtKey,我们通过代码生成一个高强度的随机密钥,并将其配置在application.yml中,而不是硬编码在代码里,这是一种更安全的实践。

java 复制代码
import io.jsonwebtoken.security.Keys;
import java.util.Base64;

public class GenerateJwtKey {
    public static void main(String[] args) {
        byte[] keyBytes = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256).getEncoded();
        String base64Key = Base64.getEncoder().encodeToString(keyBytes);
        System.out.println("Base64 Encoded Key: " + base64Key);
    }
}

运行这个类,将生成的密钥复制到 application.yml,这个用我们openssl也可以的。

2.4 创建JwtUtils

为了让JWT相关的操作(生成、解析、验证)逻辑集中管理,我们创建一个JwtUtils工具类。

当其他服务需要操作JWT时,只需注入并调用这个工具类即可,避免了在多处编写重复的代码。

创建com.alibaba.utils.JwtUtils

java 复制代码
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT 工具类
 * 负责生成、验证和解析 JWT Token
 */
@Component
public class JwtUtils {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

    @Value("${jwt.secret-key}")
    private String secretKeyBase64;

    @Value("${jwt.expiration}")
    private long expiration;

    /**
     * 解析 Base64 密钥,并返回 SecretKey
     */
    private SecretKey getSigningKey() {
        byte[] keyBytes = Base64.getDecoder().decode(secretKeyBase64);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    /**
     * 生成 JWT Token
     *
     * @param username 用户名
     * @param userId   用户ID
     * @param role     用户角色
     * @return JWT Token
     */
    public String generateToken(String username, Long userId, String role) {
        SecretKey key = getSigningKey();

        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId.toString());
        claims.put("role", role);

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    /**
     * 验证 JWT Token 是否有效
     *
     * @param token JWT Token
     * @return 是否有效
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(getSigningKey())
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            logger.warn("Token expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            logger.warn("Unsupported token: {}", e.getMessage());
        } catch (MalformedJwtException e) {
            logger.warn("Malformed token: {}", e.getMessage());
        } catch (Exception e) {
            logger.error("Error validating token", e);
        }
        return false;
    }

    /**
     * 从 JWT Token 中提取用户名
     *
     * @param token JWT Token
     * @return 用户名
     */
    public String extractUsernameFromToken(String token) {
        try {
            Claims claims = extractClaims(token);
            return claims != null ? claims.getSubject() : null;
        } catch (Exception e) {
            logger.error("Error extracting username from token", e);
            return null;
        }
    }

    /**
     * 从 JWT Token 中提取用户ID
     *
     * @param token JWT Token
     * @return 用户ID
     */
    public String extractUserIdFromToken(String token) {
        try {
            Claims claims = extractClaims(token);
            return claims != null ? claims.get("userId", String.class) : null;
        } catch (Exception e) {
            logger.error("Error extracting userId from token", e);
            return null;
        }
    }

    /**
     * 从 JWT Token 中提取用户角色
     *
     * @param token JWT Token
     * @return 用户角色
     */
    public String extractRoleFromToken(String token) {
        try {
            Claims claims = extractClaims(token);
            return claims != null ? claims.get("role", String.class) : null;
        } catch (Exception e) {
            logger.error("Error extracting role from token", e);
            return null;
        }
    }

    /**
     * 提取 Claims
     */
    private Claims extractClaims(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(getSigningKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            logger.debug("Cannot extract claims from token: {}", e.getMessage());
            return null;
        }
    }
}

3 密码加密

用户的密码绝不能以明文形式存储在数据库中,这会带来巨大的安全风险。我们必须对密码进行加密处理。这里我们选用BCrypt算法。

为什么使用BCrypt?

BCrypt是一种专为密码哈希设计、经过时间考验的强大算法。它有两个显著的优点:

  • 自动加盐:对于同一个原始密码,每次加密生成的结果都是不同的。这是因为它在加密时会自动生成一个随机的盐,并将盐和密文存储在一起。这可以有效抵御彩虹表攻击。
  • Spring Security 内置支持:Spring Security框架原生支持BCrypt算法,我们可以非常方便地集成,无需引入额外的依赖和复杂的配置。

创建com.alibaba.utils.PasswordUtil

java 复制代码
public class PasswordUtil {
    private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

    /**
     * 加密密码
     */
    public static String encode(String rawPassword) {
        return encoder.encode(rawPassword);
    }

    /**
     * 验证密码是否匹配
     */
    public static boolean matches(String rawPassword, String encodedPassword) {
        return encoder.matches(rawPassword, encodedPassword);
    }
}

4 实现User相关代码

有了JWT工具和密码加密工具,我们现在可以开始编写用户管理的核心业务逻辑了。

4.1 实现UserService

UserService是一个业务逻辑层的组件。它负责处理与用户相关的业务操作,如注册和登录验证。

创建com.alibaba.service.UserService

java 复制代码
/**
 * 用户服务类
 * 处理用户注册、登录等业务逻辑
 */
@Service
public class UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);

    @Autowired
    private UserRepository userRepository;

    /**
     * 注册新用户
     *
     * @param username 用户名
     * @param password 密码
     * @throws RuntimeException 如果用户名已存在
     */
    @Transactional
    public User registerUser(String username, String password) {
        // 检查用户名是否已存在
        if (userRepository.findByUsername(username).isPresent()) {
            throw new RuntimeException("Username already exists");
        }

        // 创建新用户
        User user = new User();
        user.setUsername(username);
        user.setPassword(PasswordUtil.encode(password)); // 加密密码
        user.setRole(User.Role.USER); // 默认角色为普通用户

        User savedUser = userRepository.save(user);
        logger.info("User registered successfully: {}", username);
        return savedUser;
    }

    /**
     * 验证用户登录
     *
     * @param username 用户名
     * @param password 密码
     * @return 用户对象,如果验证失败返回 null
     */
    public User validateUser(String username, String password) {
        Optional<User> userOpt = userRepository.findByUsername(username);
        
        if (userOpt.isEmpty()) {
            logger.warn("User not found: {}", username);
            return null;
        }

        User user = userOpt.get();
        
        // 验证密码
        if (!PasswordUtil.matches(password, user.getPassword())) {
            logger.warn("Invalid password for user: {}", username);
            return null;
        }

        logger.info("User authenticated successfully: {}", username);
        return user;
    }

    /**
     * 根据用户名查找用户
     *
     * @param username 用户名
     * @return 用户对象
     */
    public Optional<User> findByUsername(String username) {
        return userRepository.findByUsername(username);
    }
}

4.2 实现CustomUserDetailsService

Spring Security在进行认证时,需要一个标准的方式来根据用户名加载用户的详细信息(包括加密后的密码和权限列表)。

UserDetailsService就是这个标准接口。我们通过实现它的loadUserByUsername方法,告诉Spring Security:"当你需要用户信息时,请调用这个方法,我会从我的数据库中查询,并以你(Spring Security)能理解的UserDetails格式返回给你"。这样,我们的用户体系就成功地接入了Spring Security的认证流程。

为了让Spring Security 能够加载用户信息,我们需要实现 UserDetailsService

java 复制代码
import com.alibaba.model.User;
import com.alibaba.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.Collections;

/**
 * 自定义 UserDetailsService
 * 用于 Spring Security 加载用户信息
 */
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));

        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                getAuthorities(user.getRole())
        );
    }

    /**
     * 将用户角色转换为 Spring Security 的权限格式
     */
    private Collection<? extends GrantedAuthority> getAuthorities(User.Role role) {
        return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.name()));
    }
}

5 实现安全配置与认证流程

现在,我们将所有组件串联起来,配置Spring Security,让它使用我们定义的JWT认证逻辑。

5.1 实现JwtAuthenticationFilter

Filter(过滤器)是Java Web中处理请求和响应的链式结构。我们自定义的JwtAuthenticationFilter是整个JWT认证流程的核心执行者。

它的作用是拦截每一个进入系统的HTTP请求。在请求到达真正的业务控制器(Controller)之前,这个过滤器会检查请求头中是否携带了有效的JWT。如果Token有效,它会解析出用户信息,并将其设置到Spring Security的"安全上下文"(SecurityContextHolder)中。

这个动作相当于告诉Spring Security:"当前这个请求的用户已经通过认证了,他的身份是xxx,角色是yyy"。这样,后续的授权检查(比如检查用户是否有权访问某个API)才能正常工作。

在每个请求中检查JWT Token,并设置用户认证信息

java 复制代码
import com.alibaba.service.CustomUserDetailsService;
import com.alibaba.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

/**
 * JWT 认证过滤器
 * 在每个请求中检查 JWT Token,并设置用户认证信息
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain)
            throws ServletException, IOException {
        try {
            // 从请求头中提取 JWT Token
            String token = extractToken(request);
            
            if (token != null && jwtUtils.validateToken(token)) {
                // Token 有效,提取用户名
                String username = jwtUtils.extractUsernameFromToken(token);
                
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    // 加载用户信息
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    
                    // 创建认证对象
                    UsernamePasswordAuthenticationToken authentication = 
                            new UsernamePasswordAuthenticationToken(
                                    userDetails, 
                                    null, 
                                    userDetails.getAuthorities()
                            );
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    
                    // 设置到 Spring Security 上下文
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    String userId = jwtUtils.extractUserIdFromToken(token);
                    request.setAttribute("userId", userId);
                }
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}",e);
        }
        
        filterChain.doFilter(request, response);
    }

    /**
     * 从请求头中提取 JWT Token
     * 格式:Authorization: Bearer <token>
     */
    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7); // 去掉 "Bearer " 前缀
        }
        return null;
    }
}

5.2 更新SecurityConfig

更新com.alibaba.config.SecurityConfig

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Spring Security 配置类
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 禁用 CSRF
                .csrf(csrf -> csrf.disable())

                // 配置请求授权
                .authorizeHttpRequests(auth -> auth
                        // 允许公开访问的接口
                        .requestMatchers("/api/v1/users/register", "/api/v1/users/login").permitAll()
                        // 其他请求需要认证
                        .anyRequest().authenticated()
                )

                // 设置会话管理策略为无状态
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )

                // 添加 JWT 认证过滤器
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

5.3 实现认证控制器

它向外界暴露了两个核心的API接口:/register用于用户注册,/login用于用户登录。

它负责接收前端传递过来的HTTP请求和JSON数据,对参数进行基础校验,然后调用UserService来执行实际的业务逻辑。

当业务逻辑成功完成后(例如登录成功),它会调用JwtUtils生成Token,并将其封装在标准的HTTP响应中返回给客户端。

创建com.alibaba.controller.AuthController

java 复制代码
import com.alibaba.model.User;
import com.alibaba.service.UserService;
import com.alibaba.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * 认证控制器
 * 处理用户注册和登录
 */
@RestController
@RequestMapping("/api/v1/users")
public class AuthController {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtUtils jwtUtils;

    /**
     * 用户注册
     */
    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
        try {
            // 参数验证
            if (request.username() == null || request.username().isEmpty()) {
                return ResponseEntity.badRequest()
                        .body(Map.of("code", 400, "message", "Username cannot be empty"));
            }
            if (request.password() == null || request.password().isEmpty()) {
                return ResponseEntity.badRequest()
                        .body(Map.of("code", 400, "message", "Password cannot be empty"));
            }

            // 注册用户
            User user = userService.registerUser(request.username(), request.password());

            // 生成 Token
            String token = jwtUtils.generateToken(
                    user.getUsername(), 
                    user.getId(), 
                    user.getRole().name()
            );

            return ResponseEntity.ok(Map.of(
                    "code", 200,
                    "message", "Registration successful",
                    "data", Map.of(
                            "token", token,
                            "user", Map.of(
                                    "id", user.getId(),
                                    "username", user.getUsername(),
                                    "role", user.getRole().name()
                            )
                    )
            ));
        } catch (RuntimeException e) {
            return ResponseEntity.badRequest()
                    .body(Map.of("code", 400, "message", e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(500)
                    .body(Map.of("code", 500, "message", "Internal server error"));
        }
    }

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            // 参数验证
            if (request.username() == null || request.username().isEmpty()) {
                return ResponseEntity.badRequest()
                        .body(Map.of("code", 400, "message", "Username cannot be empty"));
            }
            if (request.password() == null || request.password().isEmpty()) {
                return ResponseEntity.badRequest()
                        .body(Map.of("code", 400, "message", "Password cannot be empty"));
            }

            // 验证用户
            User user = userService.validateUser(request.username(), request.password());
            if (user == null) {
                return ResponseEntity.status(401)
                        .body(Map.of("code", 401, "message", "Invalid username or password"));
            }

            // 生成 Token
            String token = jwtUtils.generateToken(
                    user.getUsername(), 
                    user.getId(), 
                    user.getRole().name()
            );

            return ResponseEntity.ok(Map.of(
                    "code", 200,
                    "message", "Login successful",
                    "data", Map.of(
                            "token", token,
                            "user", Map.of(
                                    "id", user.getId(),
                                    "username", user.getUsername(),
                                    "role", user.getRole().name()
                            )
                    )
            ));
        } catch (Exception e) {
            return ResponseEntity.status(500)
                    .body(Map.of("code", 500, "message", "Internal server error"));
        }
    }

    // 请求 DTO
    record RegisterRequest(String username, String password) {}
    record LoginRequest(String username, String password) {}
}

6 测试

启动成功后我们一一进行测试

6.1 测试注册接口

POST http://localhost:8081/api/v1/users/register

返回:

json 复制代码
{
    "message": "Registration successful",
    "data": {
        "user": {
            "username": "testuser",
            "id": 1,
            "role": "USER"
        },
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiVVNFUiIsInVzZXJJZCI6IjEiLCJzdWIiOiJ0ZXN0dXNlciIsImlhdCI6MTc2MzI5NzUxMCwiZXhwIjoxNzYzMzgzOTEwfQ.5Uthh_z_iXD0vE__Fdv53FN-MNJ7185NntF6Dnoj7x0"
    },
    "code": 200
}

查看数据库,落表成功

6.2 测试登录接口

POST http://localhost:8081/api/v1/users/login

返回:

java 复制代码
{
    "message": "Login successful",
    "data": {
        "user": {
            "username": "testuser",
            "id": 1,
            "role": "USER"
        },
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiVVNFUiIsInVzZXJJZCI6IjEiLCJzdWIiOiJ0ZXN0dXNlciIsImlhdCI6MTc2MzI5NzY5MSwiZXhwIjoxNzYzMzg0MDkxfQ.l6iBcLc9VoXjgPauMqh1juCdXr8SAmPW5G3NNEsQoRY"
    },
    "code": 200
}

至此,我们的用户认证和权限管理已经编写完毕,下一章开始我们就会开始真正的编写我们的核心流程。

相关推荐
youngfengying3 小时前
《轻量化 Transformers:开启计算机视觉新篇》
人工智能·计算机视觉
csdn_wuwt3 小时前
前后端中Dto是什么意思?
开发语言·网络·后端·安全·前端框架·开发
print(未来)4 小时前
C++ 与 C# 的性能比较:选择合适的语言进行高效开发
java·开发语言
四问四不知4 小时前
Rust语言入门
开发语言·rust
JosieBook4 小时前
【Rust】 基于Rust 从零构建一个本地 RSS 阅读器
开发语言·后端·rust
云边有个稻草人4 小时前
部分移动(Partial Move)的使用场景:Rust 所有权拆分的精细化实践
开发语言·算法·rust
一晌小贪欢4 小时前
Pandas操作Excel使用手册大全:从基础到精通
开发语言·python·自动化·excel·pandas·办公自动化·python办公
搞科研的小刘选手5 小时前
【同济大学主办】第十一届能源资源与环境工程研究进展国际学术会议(ICAESEE 2025)
大数据·人工智能·能源·材质·材料工程·地理信息
MARS_AI_5 小时前
云蝠智能 VoiceAgent 2.0:全栈语音交互能力升级
人工智能·自然语言处理·交互·信息与通信·agi