【笔记】ASP.NET Core Web API之Token验证

在实际开发中经常需要对外提供接口以便客户获取数据,由于数据属于私密信息,并不能随意供其他人访问,所以就需要验证客户身份。那么如何才能验证客户的身份呢?一个简单的小例子,简述ASP.NET Core Web API开发过程中,常用的一种JWT身份验证方式。

1.什么是JWT?

JSON WEB TokenJWT,读作 [/dʒɒt/]),是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。主要用于认证和保护API之间信息交换。

JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。

2.JWT 组成

 JWT 由三部分组成:Header,Payload,Signature 三个部分组成,并且最后由.拼接而成 xxxxx.yyyyy.zzzzz。

2.1 头部(Header)

Header 一般由alg 和 typ 两个部分组成:

{"alg": "HS256",  (使用的hash算法,如:HMAC SHA256或RSA)"typ": "JWT"      (Token的类型,在这里就是:JWT)
}

然后使用Base64Url编码成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2.2. 载荷(Payload)

这部分是JWT主要的信息存储部分,其中包含了许多种的声明(claims)。
Claims的实体一般包含用户和一些元数据,这些claims分成三种类型:

  • reserved claims:预定义的 一些声明,并不是强制的但是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(这里都使用三个字母的原因是保证 JWT 的紧凑)。
  • public claims: 公有声明,这个部分可以随便定义,但是要注意和 IANA JSON Web Token 冲突。
  • private claims: 私有声明,这个部分是共享被认定信息中自定义部分。

Pyload可以是这样子的:

{-- 官方的字段"iss" (issuer):签发人,"exp" (expiration time):过期时间,"sub" (subject):主题,"aud" (audience):订阅者,"nbf" (Not Before):生效时间,"iat" (Issued At):签发时间,"jti" (JWT ID):编号,-- 自定义的字段"user_id": 123456,"name": "John Doe","admin": true
}

这部分同样使用Base64Url编码成第二部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.

2.3 签名(Signature)

Signature是对前两部分的签名,是用来验证发送者的JWT的同时也能确保在期间不被篡改。

首先需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。

这个密钥(secret)是大于16位的随机字符串(数字),

生成jwt:

// 由 HMACSHA256 算法进行签名,secret 不能外泄
const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret)算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔.
// jwt 由三部分拼接而成
const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign

为啥要用base64编码,由于 ASCII 码称为了国际标准,所以我们要把其它字符转成 ASCII 就要用到 base64。

utf-8 -> base64(编码) -> ASCIIASCII -> base64(解码) -> utf-8

这样就可以让只支持 ASCII 的计算机支持 utf-8 了。

2.4 JWT的结构

eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LZA1L21kZW50aXR5L2NsYW1tcy9zaWQi0iIxIiwiaHRBcDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MVMjAWNS8wNS9pZGVudG1Bes9jbGFpbXMvbmFtZSI6IuWFrOWtkOWwjWFrSIsImhBdHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAw0C8wNi9pZGVudG1Bes9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNjg3NZEyMDE1LCJpc3Mi0iLlhazlrzDlsI_lhagiLcJhdWQi0iLlhazlrZDlsI_1hagifQ.QeZ1Cy5JPV0s8fPfFR59g-rVI3SNKPNP2ZcODzr308Y

打开:JSON Web Tokens - jwt.ioJSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).icon-default.png?t=N7T8https://jwt.io/

3.应用JWT步骤

 安装JWT授权库

采用JWT进行身份验证,需要安装【Microsoft.AspNetCore.Authentication.JwtBearer】,可通过Nuget包管理器进行安装,如下所示:

添加JWT身份验证服务

在启动类Program.cs中,添加JWT身份验证服务,如下所示:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{options.TokenValidationParameters = new TokenValidationParameters{ValidateIssuer = true,ValidateAudience = true,ValidateLifetime = true,ValidateIssuerSigningKey = true,ValidIssuer = TokenParameter.Issuer,ValidAudience = TokenParameter.Audience,IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenParameter.Secret))};});
应用鉴权授权中间件

在启动类Program.cs中,添加鉴权授权中间件,如下所示:

app.UseAuthentication();app.UseAuthorization();
配置Swagger身份验证输入(可选)

在启动类Program.cs中,添加Swagger服务时,配置Swagger可以输入身份验证方式,如下所示:

builder.Services.AddSwaggerGen(options =>
{options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme{Description = "请输入token,格式为 Bearer xxxxxxxx(注意中间必须有空格)",Name = "Authorization",In = ParameterLocation.Header,Type = SecuritySchemeType.ApiKey,BearerFormat = "JWT",Scheme = "Bearer"});//添加安全要求options.AddSecurityRequirement(new OpenApiSecurityRequirement {{new OpenApiSecurityScheme{Reference =new OpenApiReference{Type = ReferenceType.SecurityScheme,Id ="Bearer"}},new string[]{ }}});
});

注意:此处配置主要是方便测试,如果采用Postman或者其他测试工具,此步骤可以省略。

创建JWT帮助类

创建JWT帮助类,主要用于生成Token,如下所示:

using DemoJWT.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;namespace DemoJWT.Authorization
{public class JwtHelper{public static string GenerateJsonWebToken(User userInfo){var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenParameter.Secret));var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);var claimsIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);claimsIdentity.AddClaim(new Claim(ClaimTypes.Sid, userInfo.Id));claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, userInfo.Name));claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, userInfo.Role));var token = new JwtSecurityToken(TokenParameter.Issuer,TokenParameter.Audience,claimsIdentity.Claims,expires: DateTime.Now.AddMinutes(120),signingCredentials: credentials);return new JwtSecurityTokenHandler().WriteToken(token);}}
}

其中用到的TokenParameter主要用于配置Token验证的颁发者,接收者,签名秘钥等信息,如下所示:

namespace DemoJWT.Authorization
{public class TokenParameter{public const string Issuer = "公子小六";//颁发者        public const string Audience = "公子小六";//接收者        public const string Secret = "1234567812345678";//签名秘钥        public const int AccessExpiration = 30;//AccessToken过期时间(分钟)}
}
创建Token获取接口

创建对应的AuthController/GetToken方法,用于获取Token信息,如下所示:

using DemoJWT.Authorization;
using DemoJWT.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.IdentityModel.Tokens.Jwt;namespace DemoJWT.Controllers
{[Route("api/[controller]/[Action]")][ApiController]public class AuthController : ControllerBase{[HttpPost]public ActionResult GetToken(User user){string token = JwtHelper.GenerateJsonWebToken(user);return Ok(token);}}
}
创建测试接口

创建测试接口,用于测试Token身份验证。如下所示:

using DemoJWT.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;namespace DemoJWT.Controllers
{[Authorize][Route("api/[controller]/[Action]")][ApiController]public class TestController : ControllerBase{[HttpPost]public ActionResult GetTestInfo(){var claimsPrincipal = this.HttpContext.User;var name = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == ClaimTypes.Name)?.Value;var role = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == ClaimTypes.Role)?.Value;var test = new Test(){Id = 1,Name = name,Role = role,Author = "公子小六",Description = "this is a test.",};return Ok(test);}}
}
接口测试

运行程序,看到公开了两个接口,如下所示:

 校验逻辑
  • 1)客户端向授权服务系统发起请求,申请获取“令牌”。
  • 2)授权服务根据用户身份,生成一张专属“令牌”,并将该“令牌”以JWT规范返回给客户端
  • 3)客户端将获取到的“令牌”放到http请求的headers中后,向主服务系统发起请求。主服务系统收到请求后会从headers中获取“令牌”,并从“令牌”中解析出该用户的身份权限,然后做出相应的处理(同意或拒绝返回资源)

1. 获取Token
运行api/Auth/GetToken接口,输入用户信息,点击Execute,在返回的ResponseBody中,就可以获取接口返回的Token

2. 设置Token
在Swagger上方,点击Authorize,弹出身份验证配置窗口,如下所示:

3. 接口测试
配置好身份认证信息后,调用/api/Test/GetTestInfo接口,获取信息如下:

如果清除掉Token配置,再进行访问/api/Test/GetTestInfo接口,则会返回401未授权信息,如下所示:

代码例子

JWT 与 Session

有无状态对比
  • Session 是一种记录服务器和客户端会话状态的机制,需要在数据库或者 Redis 中保存用户信息和token信息,所以它是有状态的。
  • JWT 看完了前面的 JWT 结构和 JWT 校验原理,在后端并不需要存储数据,直接通过私有密钥验证就可以了。

当有这样的一个需求,一家公司下同时关联了多个业务,A业务网站,B业务网站,但是现在要求用户在A网站登陆过,再访问B网站的时候能够自动登陆,JWT 就可以很快的实现这个需求,把 JWT 直接存储在前端,后端只要校验 JWT 就可以了。

注:这个需求用 session 也是可以实现的,只是会存储状态,查询存储,没有 JWT 方便而已。

适用场景对比
邮箱验证

                很多网站在注册成功后添加了邮箱验证功能,功能实现:用户注册成功后,完善邮箱,服务端会给用户邮箱发一个链接,用户点开链接校验成功,这个功能使用 JWT 是个不错的选择。

// 把邮箱以及用户id绑定在一起,设置生效时间
const code = jwt.sign({ email, userId }, secret, { expiresIn: 60 * 30 })// 在此链接校验验证码
const link = `https://www.inode.club/code=${code}`
做那些短期的验证需求

        比如在 BFF 层,用 JWT 去验证传递一些数据还是不错的选择,可以把有效时间设置的短一些,过期了就需要重新去请求,我这么直接表述你可能还不太懂,举个现实生活中的例子。

我们上学的时候,有班主任学科老师这两个概念,有一天你想请假,你需要先去找班主任开一个请假条,然后请教条你的班主任签完字之后,你会将请假条交给你的学科课教师,学科教师确认签字无误后,把请假条收了,并在请假记录表中作出了相应记录。

        上诉的例子中,“请假申请单”就是JWT中的payload,领导签字就是base64后的数字签名,领导是issuer,“学科教师的老王”即为JWT的audience,audience需要验证班主任签名是否合法,验证合法后根据payload中请求的资源给予相应的权限,同时将JWT收回。

放到一些系统集成的应用场景中,例如我前面说的 BFF 中其实 JWT 更适合一次性操作的认证:

服务 B 你好, 服务 A 告诉我,我可以操作 <JWT内容>, 这是我的凭证(即 JWT )

        在这里,服务 A 负责认证用户身份(类似于上例班主任批准请假),并颁布一个很短过期时间的JWT给浏览器(相当于上例的请假单),浏览器(相当于上例请假的我们)在向服务 B 的请求中带上该 JWT,则服务 B(相当于上例的任课教师)可以通过验证该 JWT 来判断用户是否有权限执行该操作。通过这样,服务 B 就成为一个安全的无状态的服务。

个人还是认为 JWT 更适合做一些一次性的安全认证,好多其他场景考虑多了之后又做回了 session,传统的 cookie-session 机制工作得更好,但是对于一次性的安全认证,颁发一个有效期极短的JWT,即使暴露了危险也很小。上面的邮箱验证其实也是一次性的安全认证。

跨域认证

因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)。JWT 确实是跨域认证的一个解决方案,但是对于跨域场景时要注意一点。 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

Authorization: Bearer另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

跨域知识扩展

跨域这两个字就像一块狗皮膏药一样黏在每一个开发者身上,无论你在工作上或者面试中无可避免会遇到这个问题。为了应付面试,我们每次都随便背几个方案。但是如果突然问你为什么会有跨域这个问题出现? ...停顿几秒,这里只是普及一下,知道的可以忽略掉。

登陆验证

登陆验证:不需要控制登录设备数量以及注销登陆情况,无状态的 jwt 是一个不错的选择。具体实现流程,可以看上文中的校验原理,校验原理使用的登陆验证例子。

当需求中出现控制登陆设备数量,或者可以注销掉用户时,可以考虑使用原有的 session 模式,因为针对这种登陆需求,需要进行的状态存储对 jwt 添加额外的状态支持,增加了认证的复杂度,此时选用 session 是一个不错的选择。 针对上面的特殊需求,可能也有小伙伴仍喜欢使用 jwt ,补充一下特殊案例

注销登陆

用户注销时候要考虑 token 的过期时间。

  • session: 只需要把 user_id 对应的 token 清掉即可 ;

  • jwt: 使用 redis,需要维护一张黑名单,用户注销时把该 token 加入黑名单,过期时间与 jwt 的过期时间保持一致。

用户登陆设备控制
  • session: 使用 sql 类数据库,维护一个用户验证token表,每次登陆重置表中 token 字段,每次请求需要权限接口时,根据 token 查找 user_id(也可以使用 redis 维护 token 数据的存储)

  • jwt: 假使使用 sql 类数据库,维护一个用户验证token表,表中添加 token 字段,每次登陆重置 token 字段,每次请求需要权限接口时,根据 jwt 获取 user_id,根据 user_id 查用户表获取 token 判断 token 是否一致。(也可以使用 redis 维护 token 数据的存储)

适合做那些事来讲的,其实也就是针对JWT的优势来说的,还有一些辩证性的理解。接下来说说 JWT 的缺点。

JWT 缺点

  • 更多的空间占用。如果将原存在服务端session中的信息都放在JWT中保存,会造成JWT占用的空间变大,需要考虑客户端cookie的空间限制等因素,如果放在Local Storage,则可能会受到 XSS 攻击。

  • 无法作废已颁布的令牌。JWT 使用时由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。

  • 用户信息安全。通过J WT 的组成结构可以看出,Payload 存储的一些用户信息,它是通过Base64加密的,可以直接解密,不能将秘密数据写入 JWT,如果使用需要对 JWT 进行二次加密。

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

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

相关文章

Git学习笔记(三)Git分支

Git分支是Git中非常重要的一个概念&#xff0c;无论是个人开发还是多人协作中&#xff0c;分支都起着至关重要的作用。几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离 开来进行重大的Bug修改、开发新的功能&#xff0c;以免影响…

Linux驱动开发笔记(零)驱动基础知识及准备

文章目录 前言一、Liunx、MCU和FPGA编程的区别二、Linux内核模块1. 什么是内核模块2. 内核模块的代码架构3. 头文件4. 模块参数5. makefile说明 三、 驱动程序设计思路1. 基本步骤2. 设备号3. 数据结构3.1 file_operations3.2 file3.3 inode3.4 哈希表3.5 cdev结构体3.6 kobj_m…

[Linux][进程信号][一][信号基础][如何产生信号]详细解读

目录 0.前言预备1.系统定义的信号列表2.核心转储 -- Core Dump 1.信号基础1.信号概念2.信号处理方式概览3.理解信号如何被保存4.信号发送的本质 2.如何产生信号&#xff1f;1.终端按键产生信号2.系统调用接口1.kill()2.raise()3.abort()4.如何理解&#xff1f; 3.由软件条件产生…

C# 图像旋转一定角度后,对应坐标怎么计算?

原理分析 要计算图像内坐标在旋转一定角度后的新坐标&#xff0c;可以使用二维空间中的点旋转公式。假设图像的中心点&#xff08;即旋转中心&#xff09;为 (Cx, Cy)&#xff0c;通常对于正方形图像而言&#xff0c;中心点坐标为 (Width / 2, Height / 2)。给定原坐标点 (X, …

开发与产品的战争之自动播放视频

开发与产品的战争之自动播放视频 起因 产品提了个需求&#xff0c;对于网站上的宣传视频&#xff0c;进入页面就自动播放。但是基于我对chromium内核的一些浅薄了解&#xff0c;我当时就给拒绝了: “浏览器不允许”。&#xff08;后续我们浏览器默认都是chromium内核的&#…

【深度学习】Vision Transformer

一、Vision Transformer Vision Transformer (ViT)将Transformer应用在了CV领域。在学习它之前&#xff0c;需要了解ResNet、LayerNorm、Multi-Head Self-Attention。 ViT的结构图如下&#xff1a; 如图所示&#xff0c;ViT主要包括Embedding、Encoder、Head三大部分。Class …

OpenHarmony鸿蒙南向开发案例:【智能燃气检测设备】

样例简介 本文档介绍了安全厨房案例中的相关智能燃气检测设备&#xff0c;本安全厨房案例利用轻量级软总线能力&#xff0c;将两块欧智通V200Z-R/BES2600开发板模拟的智能燃气检测设备和燃气告警设备组合成。当燃气数值告警时&#xff0c;无需其它操作&#xff0c;直接通知软总…

VOS3000加装登陆服务器安全防护系统有用吗

VOS3000是一款专业的软交换系统&#xff0c;它主要用于中小规模的VoIP运营业务&#xff0c;包括运营费率设定、套餐管理&#xff0c;账户管理、业终端管理、网关管理、数据查询、卡类管理、号码管理、系统管理等功能1。而关于加装登陆服务器安全防护系统是否有用&#xff0c;这…

2.4 Web容器配置:Tomcat

2.4 Web容器配置 2.4.1Tomcat配置1.常规配置2. HTTPS配置 *********** 2.4.1Tomcat配置 1.常规配置 在SpringBoot项目中&#xff0c;可以内置Tomcat、Jetly、Undertow、Netty等容器。 当开发者添加了spring-boot-starter-web依赖之后&#xff0c;默认会使用Tomcat作为Web容器…

基于Springboot+Vue的Java项目-网上点餐系统开发实战(附演示视频+源码+LW)

大家好&#xff01;我是程序员一帆&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &am…

【EdgeBox-8120AI-TX2】Ubuntu18.04 + ROS_ Melodic + 星秒PAVO2单线激光 雷达评测

大家好&#xff0c;我是虎哥&#xff0c;好久不见&#xff0c;最近这断时间出现了一点变故&#xff0c;开始自己创业&#xff0c;很多事需要忙&#xff0c;所以停更了大约大半年&#xff0c;最近一切已经理顺&#xff0c;所以我还是抽空继续我之前的FLAG&#xff0c;CSDN突破十…

牛客NC314 体育课测验(一)【中等 图,BFS,拓扑排序 Java,Go、PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/1a16c1b2d2674e1fb62ce8439e867f33 核心 图&#xff0c;BFS,拓扑排序&#xff0c;队列参考答案Java import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修…

数据库——实验9 存储过程的使用

1. 存储过程的定义 存储过程是一系列预先编辑好的、能实现特定数据操作功能的SQL 代码集&#xff0c;它与特定的数据库相关联&#xff0c;存储在SQL Server服务器上。用户可以像使用自定义的函数一样重复调用这些存储过程&#xff0c;实现它所定义的操作。 2. 存储过程的类型…

后台管理系统加水印(react)

效果 代码图片 代码 window.waterMark function (config) {var defaultConfig {content: 我是水印,fontSize: 16px,opacity: 0.3,rotate: -15,color: #ADADAD,modalId: J_waterMarkModalByXHMAndDHL,};config Object.assign({}, defaultConfig, config);var existMarkModal…

镜舟科技荣获金科创新社 2024 年度金融数据智能解决方案奖

近日&#xff0c; 镜舟科技凭借领先的金融实时数仓构建智能经营解决方案&#xff0c;在“金科创新社第六届金融数据智能优秀解决方案评选”活动中&#xff0c;成功入选“数据治理与数据平台创新优秀解决方案”榜单。 金科创新社主办的“鑫智奖”评选活动&#xff0c;旨在展示…

【解决】Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed

问题原因&#xff1a; 在Java8及高版本以上的版本在源应用程序不信任目标应用程序的证书&#xff0c;因为在源应用程序的JVM信任库中找不到该证书或证书链。也就是目标站点启用了HTTPS 而缺少安全证书时出现的异常 解决方案&#xff1a; 我使用的是忽略证书验证 public clas…

【配电网故障定位】基于二进制矮猫鼬优化算法的配电网故障定位 33节点配电系统故障定位【Matlab代码#82】

文章目录 【获取资源请见文章第6节&#xff1a;资源获取】1. 配电网故障定位2. 二进制矮猫鼬优化算法3. 算例展示4. 部分代码展示5. 仿真结果展示6. 资源获取 【获取资源请见文章第6节&#xff1a;资源获取】 1. 配电网故障定位 配电系统故障定位&#xff0c;即在配电网络发生…

vscode微博发布案例

样例: CSS代码: * {margin: 0;padding: 0; }ul{list-style: none; }.w {width: 900px;margin: 0 auto; }.controls textarea {width: 878px;height: 100px;resize: none;border-radius: 10px;outline: none;padding-left: 20px;padding-top: 10px;font-size: 18px; }.controls…

UE4 相机围绕某点旋转

关卡&#xff08;一个相机CameraActor&#xff0c;一个Cube(名叫Target)&#xff09;&#xff1a; 关卡蓝图里的逻辑(为了大家看得清楚&#xff0c;特意连得很紧凑&#xff0c;也比较乱&#xff0c;不然一张截图放不下)&#xff1a; 只对Yaw 只Pitch: 同样对Roll: 围绕任…

汇编语言——将BX中的无符号数和有符号数以二进制、八进制、十六进制、十进制形式输出

文章目录 将BX中的无符号数以二进制形式输出将BX中的无符号数以八进制形式输出将BX中的无符号数以十六进制形式输出将BX中的无符号数以十进制形式输出将BX中的有符号数以十进制形式输出 将BX中的无符号数以二进制形式输出 利用移位指令会影响CF&#xff0c;默认dl30h(数字0)&a…