【.NET 8 实战--孢子记账--从单体到微服务】--用户(登录/注册/Token)

news2024/9/20 18:54:06

从这篇文章开始,我们就进入到了项目开发阶段。我们的项目是面向用户的,因此我们首先要做的是和用户相关的逻辑代码。

一、需求

首先,我们来看一下服务端的需求:

编号需求标题需求内容
1登录传入参数用户名、密码和验证码,三个参数都验证通过后返回token和刷新token,反之返回登录失败相关信息
2注册传入参数用户名、密码和验证码,三个参数都验证通过后返回注册成功相关信息,返回返回注册失败相关信息
3找回密码传入参数用户名和验证码,两个参数都验证通过后返回验证成功相关信息,然后传入新密码重置密码,返回找回密码成功相关信息,反之返回找回失败相关信息
4刷新token传入刷新token,返回新token

二、User类和User表

在这一小结,我们一起来创建项目的第一个类和第一个表:SysUser
SysUser类是数据库中SysUser表的映射,因此它的结构和数据库中表的结构是一样的(列名称、列类型等都一样)。下面的代码就是新建的SysUser类。

using SporeAccounting.BaseModels;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace SporeAccounting.Models;

/// <summary>
/// 用户表自定义属性类
/// </summary>
[Table(name:"SysUser")]
public class SysUser:BaseModel
{
    /// <summary>
    /// 用户名
    /// </summary>
    [Column(TypeName = "nvarchar(20)")]
    [Required]
    public string UserName { get; set; }
    /// <summary>
    /// 加密后的密码
    /// </summary>
    [Column(TypeName = "nvarchar(50)")]
    [Required]
    public string Password { get; set; }
    /// <summary>
    /// 加密用的盐
    /// </summary>
    [Column(TypeName = "nvarchar(36)")]
    [Required]
    public string Salt { get; set; }
    /// <summary>
    /// 邮箱
    /// </summary>
    [Column(TypeName = "nvarchar(50)")]
    public string Email { get; set; }
    /// <summary>
    /// 手机号
    /// </summary>
    [Column(TypeName = "nvarchar(11)")]
    public string PhoneNumber { get; set; }

}

这个类继承自 BaseModel,并且使用了 System.ComponentModel.DataAnnotationsSystem.ComponentModel.DataAnnotations.Schema 命名空间中的特性来配置数据库表结构和字段的约束。代码中一共出现了三个引用了:包含基础模型类的命名空间 SporeAccounting.BaseModels 、包含数据注释属性的 System.ComponentModel.DataAnnotations 命名空间,它主要用于验证模型数据,以及包含数据库表和列注释属性的 System.ComponentModel.DataAnnotations.Schem 的命名空间,它用于配置数据库结构。
我们使用 [Table(name: "SysUser")]SysUser 类映射到数据库中的 SysUser 表,并在类中指定了数据库表的列(类的属性)以及列的属性。其中特性 Column 用于指定数据库表的列的数据类型,Required 特性指定了列不能为空。

SysUser 表的映射类定义完了,接下来我们要做的是进行数据库迁移。执行迁移前,我们需要将 SysUser 类加入到数据库连接上下文 SporeAccountingDBContext 中,代码如下:

public DbSet<SysUser> SysUsers { get; set; }

然后,我们在程序包管理器控制台中执行添加迁移命令 Add-Migration InitSysUser 来创建一个迁移文件,接着在执行更新数据库命令 Update-Database 即可完成数据库的迁移。迁移完成后我们在数据库中就能看到创建的 SysUser 表。
在这里插入图片描述

Tip:数据库迁移是指在数据库管理系统中对数据库结构进行变更的一系列步骤和操作。这些变更通常涉及添加、修改或删除数据库表、列、索引和其他数据库对象,以便数据库能够满足应用程序的新需求或优化其性能。数据库迁移的目的是确保数据的完整性和一致性,同时使得数据库结构适应业务需求的变化。
数据库迁移的详细讲解,请关注我的EF Core 专栏。

三、创建用户接口

3.1. 注册

注册用户是用户使用我们应用的第一步,这一小节我们来一起编写注册的逻辑。

  1. 服务
    我们在项目中创建一个 User 服务接口 ISysUserServer 和一个 User 服务接口实现类 SysUserImp,并在其中新增 Add
    接口,代码如下:

ISysUserServer

using SporeAccounting.Models;

namespace SporeAccounting.Server.Interface;

/// <summary>
/// 用户接口
/// </summary>
public interface ISysUserServer
{
    /// <summary>
    /// 新增用户
    /// </summary>
    void Add(SysUser sysUser);
}

SysUserImp

using SporeAccounting.Models;
using SporeAccounting.Server.Interface;

namespace SporeAccounting.Server;

/// <summary>
/// 用户实现类
/// </summary>
public class SysUserImp : ISysUserServer
{
    private SporeAccountingDBContext _dbContext;
    public SysUserImp(SporeAccountingDBContext dbContext)
    {
        _dbContext = dbContext;
    }
    /// <summary>
    /// 新增用户
    /// </summary>
    /// <param name="sysUser">用户实体</param>
    public void Add(SysUser sysUser)
    {
        try
        {
            _dbContext.SysUsers.Add(sysUser);
            _dbContext.SaveChanges();
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

我们看到,在 SysUserImp 代码的构造函数中包含 SporeAccountingDBContext 类型的参数 dbContext,这个参数是上一篇文章中我们编写的数据库上下文,通过依赖注入的方式把它注入到了 SysUserImp 类中。

  1. 注入User服务
    User 服务创建完成,我们需要将这个服务注入到项目中。和注入数据库上下的方式类似,我们只需要在 Program 类中加入如下代码即可:
 builder.Services.AddScoped(typeof(ISysUserServer), typeof(SysUserImp));

在这个代码段中,我们将User服务注册为了 Scoped 类型,Scoped 类型表示每次请求都会创建一个User服务的实例。这么做的原因是因为我们要保证请求之间不会相互影响。

Tip:相关的作用域范围请关注我的后续文章,这里不详细讲解。

  1. 控制器
    注册功能的最后一步,就是创建 Web Api 接口。新建 SysUserController 控制器类,并编写注册 Action :
using System.Net;
using System.Security.Cryptography;
using System.Text;
using AutoMapper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SporeAccounting.BaseModels;
using SporeAccounting.Models;
using SporeAccounting.Models.ViewModels;
using SporeAccounting.Server.Interface;

namespace SporeAccounting.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SysUserController : ControllerBase
    {
        private readonly ISysUserServer _sysUserServer;
        private readonly IMapper _mapper;

        public SysUserController(ISysUserServer sysUserServer, IMapper mapper)
        {
            _sysUserServer = sysUserServer;
            _mapper = mapper;
        }

        [HttpPost]
        [Route("Register")]
        public ActionResult<ResponseData<bool>> Register(SysUserViewModel sysUserViewModel)
        {
            try
            {
                SysUser sysUser = _mapper.Map<SysUser>(sysUserViewModel);
                sysUser.Salt = Guid.NewGuid().ToString("N");
                sysUser.Password = HashPasswordWithSalt(sysUser.Password, sysUser.Salt);
                sysUser.CreateUserId = sysUser.Id;
                _sysUserServer.Add(sysUser);
                return new ResponseData<bool>(HttpStatusCode.OK, "", false);
            }
            catch (Exception ex)
            {
                return new ResponseData<bool>(HttpStatusCode.InternalServerError, "服务端异常", false);
            }
        }

        private string HashPasswordWithSalt(string password, string salt)
        {
            using (var sha256 = SHA256.Create())
            {
                string saltedPassword = password + salt;
                byte[] saltedPasswordBytes = Encoding.UTF8.GetBytes(saltedPassword);
                byte[] hashBytes = sha256.ComputeHash(saltedPasswordBytes);
                return Convert.ToBase64String(hashBytes);
            }
        }
    }
}

上面的代码中我们定义了 SysUserController 作为控制器,它继承自 ControllerBase,用来处理与用户相关的 API 请求。并且加上了两个属性:

  • [Route("api/[controller]")] 定义了路由前缀(根路径),也就是说这个控制器的根路径是 api/SysUser
  • [ApiController] 是一个属性修饰符,用来注明这个类是一个 API 控制器,它会自动处理请求验证以及响应格式化。

然后我们通过注入的方式在构造函数注入了 ISysUserServer(用于与数据库交互)和 IMapper(用于对象映射)。接着我们定义了 Register 方法作为Action,并通过 [HttpPost] 属性将这个Action 标注为只接受 HTTP POST 请求。又通过 [Route("Register")] 属性指定了这个Action的路由路径为 api/SysUser/Register。在这个 Register 方法中它接收了一个 SysUserViewModel 类型的参数,用于从客户端接收用户注册信息。并使用了 AutoMapperSysUserViewModel 映射为 SysUser 实体类。接着我们为 Salt属性生成一个随机生成的一个随机的字符串用作密加密的盐。最后调用 _sysUserServer.Add(sysUser) 将用户数据保存到数据库。

在控制器中我们还定义了一个私有方法 HashPasswordWithSalt ,它主要用来对密码进行加盐哈希处理。使用的是 SHA256 哈希算法,它将加盐后的密码进行哈希处理,然后将结果转换为 Base64 字符串进行存储。通过加盐处理,即使两个用户使用相同的密码,也会生成不同的哈希值,提升了安全性。

Tip:为什么要对密码加密?这个问题属于老生常谈了,之所以对密码加密是因为保护用户隐私,防止密码被直接泄露或被恶意攻击者获取。通过使用哈希算法和加盐技术,即使数据库被攻破,密码仍难以被还原,提升了系统安全性,并降低了潜在的连锁风险。

3.2. 登录

用户注册完就要开始登录了,同样,我们也要开始编写登录代码了。

  1. 服务
    我们首先在 ISysUserServer 接口中增加 Get 方法:
/// <summary>
/// 根据用户名获取用户
/// </summary>
/// <param name="sysUser"></param>
SysUser Get(string userName);

接着,我们在 SysUserImp 类中实现新增的 Get 方法:

    /// <summary>
    /// 根据用户名获取用户
    /// </summary>
    /// <param name="userName"></param>
    /// <returns></returns>
    public SysUser Get(string userName)
    {
        try
        {
           SysUser sysUser= _dbContext.SysUsers.FirstOrDefault(p=>p.UserName==userName);
           return sysUser;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

这段代码很简单,我们不做讲解,接下来我们再来看看控制器的编写。

  1. 控制器
    我们在 SysUserController 控制器中新增 Login Action ,这个Action 是用来登录的,需要验证用户名和密码,验证通过后在调用生成token和生成刷新token的方法,生成我们所需的token。代码如下:
/// <summary>
/// 登录
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <returns></returns>
[HttpGet]
[Route("Login/{userName}/{password}")]
public ActionResult<ResponseData<TokenViewModel>> Login([FromRoute] string userName, [FromRoute] string password)
{
    try
    {
        //验证用户
        SysUser sysUser = _sysUserServer.GetByUserName(userName);
        if (sysUser == null)
        {
            return Ok(new ResponseData<bool>(HttpStatusCode.BadRequest, "用户或密码错误!", false));
        }
        string passwordHash = HashPasswordWithSalt(password, sysUser.Salt);
        //验证密码
        if (sysUser.Password != passwordHash)
        {
            return Ok(new ResponseData<bool>(HttpStatusCode.OK, "用户或密码错误!", false));
        }
        //生成Token和刷新Token
        TokenViewModel sysToken = new TokenViewModel();
        sysToken.RefreshToken = GenerateRefreshToken();
        sysToken.Token = GenerateToken(sysUser.Id, sysToken.RefreshToken);
        return Ok(new ResponseData<TokenViewModel>(HttpStatusCode.OK, data: sysToken));
    }
    catch (Exception ex)
    {
        return Ok(new ResponseData<bool>(HttpStatusCode.InternalServerError, "服务端异常", false));
    }
}

/// <summary>
/// 生成Token
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
private string GenerateToken(string userId, string refreshToken)
{
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, userId),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim("refreshToken",refreshToken)
    };
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:IssuerSigningKey"]));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    int tokenExpirces = int.Parse(_config["JWT:Expirces"]);
    var token = new JwtSecurityToken(
        issuer: _config["JWT:ValidIssuer"],
        audience: _config["JWT:ValidAudience"],
        claims: claims,
        expires: DateTime.Now.AddMinutes(tokenExpirces), // Token 有效期
        signingCredentials: creds);
    return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// 生成刷新Token
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private string GenerateRefreshToken()
{
    var randomNumber = new byte[32];
    using (var rng = RandomNumberGenerator.Create())
    {
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
    }
}

Login Action 的内容很简单,我们不做讲解了,这里主要讲解一下 GenerateTokenGenerateRefreshToken 这两个方法。GenerateToken 方法的生成一个Token 用来标识用户身份的访问令牌,其中包含用户的身份信息、声明(claims)、有效期,刷新token等。这个令牌通常会附带在用户后续请求的Authorization头中,以便服务器验证用户的身份。RefreshAccessToken 方法的主要作用是使用一个有效的刷新令牌(Refresh Token)生成一个新的访问令牌(Access Token)。刷新令牌通常是一个长期有效的凭证,用于在访问令牌过期后,客户端请求新的访问令牌,以保持用户的持续认证状态,而无需重新登录。

3.3. 找回密码

作为用户,在某些情况下可能出现忘记密码的情况,那么这时我们就需要给用户提供找回密码的功能。但是由于存储的密码都是加盐之后在存储的,无法从将原始密码返给用户,因此需要随机生成一个密码返回给用户。那么,下面我们就来看看这个功能如何实现。

  1. 服务
    我们首先在 ISysUserServer 接口中增加 Update 方法:
/// <summary>
/// 修改用户
/// </summary>
/// <param name="sysUser"></param>
void Update(SysUser sysUser);

接着,我们在 SysUserImp 类中实现这个 Update 方法:

/// <summary>
/// 修改用户
/// </summary>
/// <param name="sysUser"></param>
public void Update(SysUser sysUser)
{
    try
    {
        _dbContext.SysUsers.Update(sysUser);
        _dbContext.SaveChanges();
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

我们通过上面的代码更新数据库中 SysUser 对象的记录,调用 EF Core DbSet 提供的 Update 方法将对象 sysUser 标记为 “已修改” 状态,告知 EF Core sysUser 对象的属性将在保存时要更新到数据库中。最后调用 SaveChanges 方法将修改的数据保存到数据库中。

  1. 控制器
    服务编写完成后,我们开始编写接口,代码如下:
/// <summary>
/// 找回密码
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <returns></returns>
[HttpGet]
[Route("RetrievePassword/{userName}/{email}")]
public ActionResult<ResponseData<string>> RetrievePassword([FromRoute] string userName, [FromRoute] string email)
{
    try
    {
        //验证用户是否存在
        SysUser sysUser = _sysUserServer.Get(userName);
        if (sysUser == null)
        {
            return Ok(new ResponseData<bool>(HttpStatusCode.BadRequest, "用户不存在!", false));
        }
        if (sysUser.Email != email)
        {
            return Ok(new ResponseData<bool>(HttpStatusCode.BadRequest, "邮箱不正确!", false));
        }
        //生成12位随机密码
        string newPassword = GenerateRandomPassword(12);
        sysUser.Password= HashPasswordWithSalt(newPassword,sysUser.Salt);
        _sysUserServer.Update(sysUser);

        return Ok(new ResponseData<string>(HttpStatusCode.OK, data: newPassword));
    }
    catch (Exception ex)
    {
        return Ok(new ResponseData<bool>(HttpStatusCode.InternalServerError, "服务端异常", false));
    }
}

前面的代码用于根据用户名和邮箱重置用户密码,它是一个 HTTP GET 请求。首先,通过 _sysUserServer.Get(userName) 检查用户名是否存在。如果用户不存在或邮箱不匹配,返回 400 Bad Request 状态和错误信息。如果验证通过,则通过 GenerateRandomPassword 方法生成一个 12 位随机密码,并通过 HashPasswordWithSalt 方法加密密码后更新用户记录。成功后,返回状态码 200 OK 和新密码。若出现异常,则捕获并返回 500 Internal Server Error 和相应的错误信息。
接下来,我们编写随机生成密码的方法 GenerateRandomPassword,代码如下:

/// <summary>
/// 随机密码生成
/// </summary>
/// <param name="length"></param>
/// <returns></returns>
private string GenerateRandomPassword(int length)
{
    const string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()";
    StringBuilder password = new StringBuilder();
    using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
    {
        byte[] buffer = new byte[sizeof(uint)];
        while (password.Length < length)
        {
            rng.GetBytes(buffer);
            uint num = BitConverter.ToUInt32(buffer, 0);
            password.Append(validChars[(int)(num % (uint)validChars.Length)]);
        }
    }
    return password.ToString();
}

在这里,我们使用了 System.Security.Cryptography 命名空间中的类来生成随机密码,其中局部变量 validChars 包含了密码中可以使用的所有字符,RNGCryptoServiceProvider 用于生成加密安全的随机数。

3.4. 刷新Token

所有网站的token都会有有效期,那么token如果过期了怎么办呢?难道要用户再次输入用户名和密码登录吗?当然不是,我们可以使用前面生成的 refreshToken 来生成一个新的Token发给用户。下面我们一起来看看如何实现它。

  1. 服务
    我们首先在 ISysUserServer 接口中增加 GetById 方法,这个方法实现了根据传入的用户Id来查询用户:
/// <summary>
/// 根据用户id获取用户
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
SysUser GetById(string userId);

接着我们在 SysUserImp 类中具体实现它,代码如下:

/// <summary>
/// 根据用户Id获取用户
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public SysUser GetById(string userId)
{
    try
    {
        SysUser sysUser = _dbContext.SysUsers.FirstOrDefault(p => p.Id == userId);
        return sysUser;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

这段实现代码用于从数据库中根据 userId 获取一个 SysUser 对象。它使用 Entity Framework Core 从 _dbContext.SysUsers 集合中查找第一个匹配的用户。若查询过程中发生异常,捕获异常并重新抛出。

  1. 控制器
    刷新Token的Action稍显复杂,我们需要首先获取到Token,然后解析出Token中的UserId和refreshToken,然后验证 refreshToken 和 UserId 验证通过后生成新token。这里比较复杂的是解析Token信息,下面我们一步一步的来看看。
/// <summary>
/// 刷新token
/// </summary>
/// <param name="refreshToken"></param>
/// <returns></returns>
[HttpGet]
[Route("RefreshToken/{refreshToken}")]
public ActionResult<ResponseData<string>> RefreshToken([FromRoute] string refreshToken)
{
    try
    {
        //获取token中的user id 和 refreshToken
        string token = HttpContext.Request.Headers["Authorization"].ToString()
            .Substring("Bearer ".Length).Trim();
        (string userId, _, string reToken) = GetTokenInfo(token);
        if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(reToken) || refreshToken != reToken)
        {
            return Ok(new ResponseData<bool>(HttpStatusCode.BadRequest, "数据违规!", false));
        }
        //根据userid查询用户
        SysUser sysUser = _sysUserServer.GetById(userId);
        if (sysUser == null)
        {
            return Ok(new ResponseData<bool>(HttpStatusCode.NotFound, "用户不存在!", false));
        }
        //使用刷新token刷新token
        string newToken = GenerateToken(userId, refreshToken);
        return Ok(new ResponseData<string>(HttpStatusCode.OK, data: newToken));
    }
    catch (Exception ex)
    {
        return Ok(new ResponseData<bool>(HttpStatusCode.InternalServerError, "服务端异常", false));
    }
}

/// <summary>
/// 获取token中的userid
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
private (string, string, string) GetTokenInfo(string token)
{
    var handler = new JwtSecurityTokenHandler();
    // 验证令牌格式
    if (!handler.CanReadToken(token))
    {
        throw new ArgumentException("无效的令牌");
    }
    // 读取令牌
    var jwtToken = handler.ReadJwtToken(token);
    // 从声明中提取用户ID(通常是“sub”声明)
    var userIdClaim = jwtToken.Claims.FirstOrDefault(claim => claim.Type == "sub");
    var jtiClaim = jwtToken.Claims.FirstOrDefault(claim => claim.Type == "jti");
    var refreshTokenClaim = jwtToken.Claims.FirstOrDefault(claim => claim.Type == "refreshToken");
    return (userIdClaim?.Value, jtiClaim.Value, refreshTokenClaim.Value);
}

在上面的代码中 RefreshToken Action 方法接收一个 refreshToken 作为路径参数,并从请求头中提取当前的 Authorization 令牌,然后 GetTokenInfo 方法从令牌中解析出用户 ID (userId)、令牌 ID (jti)、和刷新令牌 (reToken),如果 userIdreToken 为空,或提供的 refreshToken 与解析出的 reToken 不匹配,返回 BadRequest 响应。反之使用 userId 查询用户,若用户不存在,则返回 NotFound 响应。一切验证都通过后就生成新的 JWT 令牌并返回成功响应。
GetTokenInfo 方法主要解析 JWT 令牌,提取令牌的声明(subjtirefreshToken)。它使用 JwtSecurityTokenHandler 验证令牌的格式和合法性,并从 JWT 声明中提取用户 ID (sub)、令牌 ID (jti)、刷新令牌 (refreshToken) 的值。

四、总结

该文章介绍了一个面向用户的项目开发,首先梳理了服务端的需求,如登录、注册、找回密码、用户查询和管理等功能。接着,创建了用户类 SysUser 并通过 EF Core 映射到数据库,完成数据迁移。然后详细讲解了如何创建用户服务和 Web API 接口,处理注册、登录和找回密码等操作,包括密码的加盐哈希处理和生成 JWT Token。最终,通过依赖注入,将这些服务集成到项目中,确保了用户数据的安全性和系统的扩展性。

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

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

相关文章

国内PMP可以在线考试?

1. PMP考试的两种类型 PMP考试分为两种类型&#xff1a;一种是在线机考&#xff0c;另一种是线下笔试。国外采用机考形式&#xff0c;可以随时参加考试&#xff0c;除了节假日&#xff1b;而国内由中国国际人才交流基金会和PMI共同组织&#xff0c;因此是线下笔试。 虽然线上…

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合&#xff0c;内部除了存储元素外&#xff0c;还会存储一个score&#xff0c;存储在zset中的元素会按照score的大小升序排列&#xff0c;不同元素的score可以重复&#xff0c;score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd zadd key [NX |…

【计算机方向】中科院二区宝刊!国人发文友好,晋升从此不是梦!

期刊解析 &#x1f6a9;本 期 期 刊 看 点 &#x1f6a9; 国人发文占比第一&#xff0c;审稿友好 审稿速度快 自引率5.7% 今天小编带来计算机领域SCI快刊的解读&#xff01; 如有相关领域作者有意投稿&#xff0c;可作为重点关注&#xff01; 01 期刊信息✦ 期刊名称&…

Centos7安装JDK1.8保姆版

工欲善其事&#xff0c;必先利其器。这句话同样适用于学习Java编程。在开始Java的学习旅程之前&#xff0c;我们必须首先配置好适合的开发环境。 通过事先准备好这些工具和配置&#xff0c;我们可以避免在学习过程中遇到因环境问题导致的代码异常或错误。一个稳定、高效的开发环…

python binning data openAI gym

题意&#xff1a;Python 数据分箱 OpenAI Gym 问题背景&#xff1a; I am attempting to create a custom environment for reinforcement learning with openAI gym. I need to represent all possible values that the environment will see in a variable called observati…

11.Java基础概念-ArrayList

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 Facts speak louder than words&#xff01; ArrayList是Java中的一…

kubectl的安装使用

1. Windows下载kubectl 2.将kucectl的所在目录添加到PATH环境变量下 3.运行 kubectl version --client 命令来测试kubectl是否正确安装并显示其版本信息。这个命令会显示kubectl客户端的版本信息&#xff0c;如果一切正常&#xff0c;这将确认kubectl已经成功安装在你的Windo…

DC-DC升降压芯片(MC34063A/33063)典型电路与元件参数在线计算

MC34063包含DC/DC变换器所需的主要功能的单片控制电路&#xff0c;多用于升压变换器、降压变换器、反向器的控制核心部分。 MC34063的基本结构及引脚图功能&#xff1a; 1脚&#xff1a;开关管T1集电极引出端&#xff1b; 2脚&#xff1a;开关管T1发射极引出端&#xff1b; …

一次耗时的安全测试

简介 接到一个安全测试任务&#xff0c;数据包使用安全控件进行了加密。通过开发插件&#xff0c;实现明文测试&#xff0c;最终发现了2个越权。 加解密过程分析 访问网站首页&#xff0c;需要先安装一个控件。安装完成后&#xff0c;访问网站发现数据包加密处理。按照以前的…

基于springboot+vue实现的在线商城系统

系统主要功能&#xff1a; &#xff08;1&#xff09;商品管理模块&#xff1a;实现了商品的基本信息录入、图片上传、状态管理等相关功能。 &#xff08;2&#xff09;商品分类模块&#xff1a;实现了分类的增删改查、分类层级管理、商品分类的关联等功能。 &#xff08;3&…

不懂编程的都觉得AI要取代程序员了,程序员自己却在偷着乐?真相是…

最近&#xff0c;AI写代码的新闻满天飞&#xff0c;不懂编程的人都觉得AI要逆天了&#xff0c;程序员马上就要失业了&#xff01;但奇怪的是&#xff0c;程序员群体——这帮最懂代码、最常使用AI编程工具的人&#xff0c;怎么反而觉得AI取代不了他们呢&#xff1f;&#x1f914…

TD综合教程——噪波球和正交线性脉冲(附思路和工程文件)

一、噪波球 整体思路&#xff1a; 渲染三件套&#xff1a;Geometry COMP、Camera COMP、Render TOP 在此基础上进行pbr MAT材质和environment COMP环境光渲染 使sphere TOP&#xff08;球体&#xff09;更加柔和&#xff0c;将类型改为NURBS Twist TOP&#xff08;扭曲&#x…

C++---内存管理

1 C/C内存分布 栈区&#xff1a;由编译器自动分配和释放&#xff0c;存放运行时候的局部变量&#xff0c;函数参数&#xff0c;返回数据&#xff0c;返回地址。 堆区&#xff1a;一般由程序员自己分配&#xff0c;然后自己释放&#xff0c;例如栈的实现malloc开辟的数组空间。…

尝试开发油猴(Tampermonkey)脚本

篡改猴 (Tampermonkey) 是拥有 超过 1000 万用户 的最流行的浏览器扩展之一。 它适用于 Chrome、Microsoft Edge、Safari、Opera Next 和 Firefox。 有些人也会把篡改猴(Tampermonkey)称作油猴(Greasemonkey)&#xff0c;尽管后者只是一款仅适用于 Firefox 浏览器的浏览器扩展…

UE5 Linux编译流程(实战)

文章目录 概述setup.sh2.GenerateProjectFiles.sh3.make其他的 小结 概述 之前写过一篇linux上代码的流程&#xff0c;这一篇&#xff0c;补下编译流程。4.26还是4.27的时候&#xff0c;做过编译&#xff0c;那会刚出来&#xff0c;当时编译用的是QT&#xff0c;跟着文档&…

传输大咖40 | 医药行业跨国文件传输的挑战与解决方案

在当今全球化浪潮的推动下&#xff0c;医药健康领域的国际合作变得越发密切。无论是共享临床试验的数据&#xff0c;还是协作推进新药的研发&#xff0c;一个高效的文件传输系统都显得尤为关键&#xff0c;它直接关系到整个行业的快速进步。但是&#xff0c;当文件跨越国界进行…

Redis缓存击穿、缓存穿透、缓存雪崩场景描述及解决方案

Redis缓存击穿、缓存穿透、缓存雪崩场景描述及解决方案 缓存穿透 用户请求了很多既不存在于redis也不存在于数据库的无效数据请求&#xff0c;导致redis无法拦截&#xff0c;最终所有请求都落在数据库中 解决方案&#xff1a;缓存空对象&#xff08;会导致redis中存了很多垃…

The Magic Loop-快速职业发展的框架

前言 在 B2B 系列中短暂休息一下&#xff0c;我很高兴为你带来 Ethan Evans 的重要客座文章。在 Amazon 的 15 年里&#xff0c;Ethan 帮助发明了 Prime Video、Amazon Video、Amazon Appstore、Prime Gaming&#xff08;以前称为 Twitch Prime&#xff09;和 Twitch Commerce…

电阻器件选型

电阻参数 一般都是&#xff08;前面数字x10^末尾数字&#xff09;&#xff0c;如下面的例子 大概清楚电阻的结构即可&#xff0c;无需掌握 电阻器安装在陶瓷基板的上面&#xff1b;在每一端都有内部金属电极&#xff0c;使其与厚膜电阻体接触&#xff1b;电阻原件的成分是金…

第144天:内网安全-Linux权限维持OpenSSHPAM后门SSH软链接公私钥登录

目录 案例一&#xff1a; 权限维持-Linux-替换版本-OpenSSH 后门 案例二&#xff1a; 权限维持-Linux-更改验证-SSH-PAM 后门 案例三&#xff1a; 权限维持-Linux-登录方式-软链接&公私钥&新帐号 ssh软链接 公私钥 新帐号 案例一&#xff1a; 权限维持-Linux-替换…