基于M实现的JWT解决方案

news/2023/5/28 6:43:07

文章目录

  • 基于`M`实现的`JWT`解决方案
    • 简介
    • 现状
    • 原理
    • JWT 组成结构
      • 头部`Header`
      • 有效载荷`Payload`
      • 哈希签名`Signature`
      • JWT完整结果
    • `JWT`基于`M`的使用流程
    • 总结
    • 完整代码

基于M实现的JWT解决方案

简介

JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。

JWTJSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。

现状

在讲解 JWT 之前我们先来看一个问题。我们都知道互联网服务的身份验正过程是这样的,客户端向服务器发送登录名和登录密码,服务器验证后将对应的相关信息保存到当前会话中,这些信息包括权限、角色等数据。

服务器向客户端返回 SessionSession 信息都会写入到客户端的 Cookie 中,后面的请求都会从 Cookie 中读取 Session 发送给服务器,服务器在收到 Session 后会对比保存的数据来确认客户端身份。

但是上述模式存在一个问题,无法横向扩展。在服务器集群或者面向服务且跨域的结构中,需要数据库来保存 Session 会话,实现服务器之间的会话数据共享。

在单点登录中我们会遇到上述问题,当有多个网站提供同一拨服务,那么我们该怎么实现在甲网站登陆后其他网站也同时登录呢?

其中一种方法是持久化 Session 数据,也就是上面所说的将 Session 会话存到数据库中。这个方法的优点是架构清晰明了。

但是缺点也非常明显,就是架构修改很困难,验证逻辑需要重写,并且整体依赖于数据库,如果存储 Session 会话的数据库挂掉那么整个身份认证就无法使用,进而导致系统无法登录。要解决这个问题我们就用到了 JWT

原理

客户端身份经过服务器验证通过后,会生成带有签名的 JSON 对象并将它返回给客户端。客户端在收到这个 JSON 对象后存储起来。

在以后的请求中客户端将 JSON 对象连同请求内容一起发送给服务器,服务器收到请求后通过 JSON 对象标识用户,如果验证不通过则不返回请求的数据。

验证不通过的情况有很多,比如签名不正确、无权限、过期等。在 JWT 中服务器不保存任何会话数据,使得服务器更加容易扩展。

Base64URL 算法

在讲解 JWT 的组成结构前我们先来讲解一下 Base64URL 算法。这个算法和 Base64 算法类似,但是有一点区别。

我们通过名字可以得知这个算法使用于 URL 的,因此它将 Base64 中的 +/= 三个字符替换成了 -_ 删除掉了 = 。因为这个三个字符在 URL 中有特殊含义。

基于M实现的方法:

ClassMethod Base64UrlEncryption(str As %String)
{s ret = $zcvt(str, "O", "UTF8")s ret = ##class(%SYSTEM.Encryption).Base64Encode(ret, 1)s ret = ..ConvertUrl(ret)q ret
}ClassMethod ConvertUrl(str)
{q $tr(str, "+/=", "-_")
}

JWT 组成结构

JWT 是由三段字符串和两个 . 组成,每个字符串和字符串之间没有换行(类似于这样:abc.def.xyz),每个字符串代表了不同的功能,我们将这三个字符串的功能按顺序列出来并讲解:

头部Header

JWT 头描述了 JWT 元数据,是一个 JSON 对象,它的格式如下:

{"alg": "HS256","typ": "JWT"
}

这里的 alg 属性表示签名所使用的算法,JWT 签名默认的算法为 HMAC SHA256alg 属性值 HS256 就是 HMAC SHA256 算法。typ 属性表示令牌类型,这里就是 JWT

基于M实现的方法:

ClassMethod GenerateHeaderPart()
{s header = {}s header.alg = "HS256"s header.typ = "JWT"s headerPart = ..Base64UrlEncryption(header.%ToJSON())q headerPart
}
USER>w ##class(M.Jwt).GenerateHeaderPart()
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

有效载荷Payload

有效载荷是 JWT 的主体,同样也是个 JSON 对象。

JWT指定七个默认字段供选择,也可以省略,一般建议使用。包括以下内容:

  • issjwt的签发者/发行人
  • sub:主题
  • aud:接收方
  • expjwt过期时间
  • nbfjwt生效时间
  • iat:签发时间
  • jtijwt唯一身份标识,可以避免重放攻击

也可以自定义字段:

{"name": "yx","code": "yaoxin","desc": "engineer",
}

下面这个代码段就是定义了一个有效载荷:

{"iss": "yx","jti": "78006423","exp": "2023-01-15 09:30:04","nbf": "2023-01-08 09:30:04","iat": "2023-01-08 09:30:04"
}

基于M实现的方法:

Parameter Sign = "yx";ClassMethod GeneratePayloadPart()
{s payload = {}s payload.iss = ..#Signs payload.jti = ##class(%SYSTEM.Encryption).GenCryptToken()s payload.exp = $zd(($h + 7), 3) _ " " _$zt($p($h, ",", 2), 1)s payload.nbf = $zdt($h, 3)s payload.iat = $zdt($h, 3)s payloadPart = ..Base64UrlEncryption(payload.%ToJSON())q payloadPart
}
USER>w ##class(M.Jwt).GeneratePayloadPart()
eyJpc3MiOiJ5eCIsImp0aSI6IjYzMjg1OTgyIiwiZXhwIjoiMjAyMy0wMS0yNSAwOTozNTozMCIsIm5iZiI6IjIwMjMtMDEtMTggMDk6MzU6MzAiLCJpYXQiOiIyMDIzLTAxLTE4IDA5OjM1OjMwIn0

哈希签名Signature

哈希签名的算法主要是确保数据不会被篡改。它主要是对前面所讲的两个部分进行签名,通过 JWT 头定义的算法生成哈希。哈希签名的过程如下:

  1. 指定密码,密码保存在服务器中,不能向客户端公开。

  2. 使用JWT 头指定的算法进行签名,进行签名前需要对 JWTHeader和有效载荷payload进行 Base64URL 编码,结果之间需要用 . 来连接。

示例如下:

HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

基于M实现的方法:

Parameter DOT = ".";Parameter Key = 00000000111111112222222233333333;ClassMethod GenerateSignaturePart(headerPart, payloadPart)
{s content = headerPart _ ..#DOT _ payloadParts signaturePart = ..HMACSHABase64Encode(content, ..#Key)q signaturePart
}

JWT完整结果

JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。

Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

基于M实现的方法:

ClassMethod GenerateJwt()
{s headerPart = ..GenerateHeaderPart()s payloadPart = ..GeneratePayloadPart()s signaturePart = ..GenerateSignaturePart(headerPart, payloadPart)s jwt = headerPart _ ..#DOT _ payloadPart _ ..#DOT _ signaturePartq jwt
}
USER>w ##class(M.Jwt).GenerateJwt()
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ5eCIsImp0aSI6IjA0MzYzNzA1IiwiZXhwIjoiMjAyMy0wMS0yNSAwOTo0ODowMSIsIm5iZiI6IjIwMjMtMDEtMTggMDk6NDg6MDEiLCJpYXQiOiIyMDIzLTAxLTE4IDA5OjQ4OjAxIn0.XxgI9PG8AznJIyS6neze2UC4XNG-V0dP46pkp_JNhBY

可以在jwt.io(地址:https://jwt.io/)上验证一下:

在这里插入图片描述

JWT基于M的使用流程

在这里插入图片描述

  1. 首先创建后端门面接口,用户处理通用权限校验和接收反射需要调用的方法:
Class M.Broker Extends (%CSP.Page, %CSP.REST)
{
/// desc:返回页面数据方法
ClassMethod OnPage() As %Status
{s pClassName = $g(%request.Data("ClassName",1))s pMethodName = $g(%request.Data("MethodName",1)) /* 输出返回值到页面上 */w ..RunMethod(pClassName, pMethodName)q $$$OK
}/// desc:运行通用方法
ClassMethod RunMethod(pClassName, pMethodName) [ ProcedureBlock = 0 ]
{s $zt = "Error"d ..GetInfo()q:(pMethodName'="Login")&&('$d(%request.CgiEnvs("HTTP_AUTHORIZATION"))) ..Failure2Json("请求头中AUTHORIZATION不存在!")q:(pClassName = "") ..Failure2Json("类不能为空!")q:(pMethodName = "") ..Failure2Json("方法不能为空!")d:(pMethodName'="Login") ##class(M.Jwt).VerifyJwt()s requestMethod = $lb("POST", "GET")q:('$lf(requestMethod, %request.CgiEnvs("REQUEST_METHOD")))try {s result = $classmethod(pClassName, pMethodName)} catch e {s msg = e.Name _ e.Location _ " *" _ data _ $zeret ..Failure2Json(msg)}q ..Success2Json(result)
Error s $zt = ""ret ..Failure2Json("意外没有捕捉错误的错误:" _ $ze)
}
}
  1. 模拟客户端使用用户名和密码请求登录返回Jwt。服务端收到请求,验证用户名和密码。验证成功后,服务端会签发一个 JWT token,再把这个JWT token返回给客户端。

后端登录方法:

Class M.Jwt Extends %RegisteredObject
{ClassMethod Login()
{#; todo 验证用户密码信息#; 返回JWTq ..GenerateJwt()
}}

使用PostMan模拟接口登录:

在这里插入图片描述

{"code": 200,"msg": "","data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ5eCIsImp0aSI6IjQ3MDY0MDQ3IiwiZXhwIjoiMjAyMy0wMS0yNSAxMDoyMzo1NCIsIm5iZiI6IjIwMjMtMDEtMTggMTA6MjM6NTQiLCJpYXQiOiIyMDIzLTAxLTE4IDEwOjIzOjU0In0.IcYRp5BqGtslB94lNYmgIrvIwuyne347HjmHkNmAceo"
}
  1. 客户端收到token后可以把它存储起来,比如放到sessionStorage中。客户端每次向服务端请求资源时需要携带服务端签发的JWT token,可以在header中携带。

使用PostMan模拟请求数据:

  • 把请求到的JWT信息放到 PostMan -> Authorization -> TYPE = Bear Token -> 设置Token 中。
  • 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSSXSRF问题)。

在这里插入图片描述

  1. 服务端收到请求,然后去验证客户端请求里面带着的JWT token,如果验证成功,就向客户端返回请求数据。

解析验证JWT后端方法:

ClassMethod VerifyJwt()
{#; 验证是否携带JWTq:'$d(%request) ""q:'$d(%request.CgiEnvs("HTTP_AUTHORIZATION")) $$$JwtException("请求头中AUTHORIZATION不存在!")#; 获取JWTs token = %request.CgiEnvs("HTTP_AUTHORIZATION")s jwt = $p(token," " ,2)s headerPart = $p(jwt, ".", 1)s payloadPart = $p(jwt, ".", 2)s signaturePart = $p(jwt, ".", 3)#; 解析header部分s header = ..Base64Decryption(headerPart)s headerObj = {}.%FromJSON(header)#; 解析payload部分s payload = ..Base64Decryption(payloadPart)s payloadObj = {}.%FromJSON(payload)#; 解析重新验签判断数据是否被修改过s content = headerPart _ ..#DOT _ payloadParts signatureConetent = ..HMACSHABase64Encode(content, ..#Key)q:(signatureConetent '= signaturePart) $$$JwtException("签名校验异常")#; 其他验证信息,根据需求自行判断q:(payloadObj.iss '= ..#Sign) $$$JwtException("IIS发行人校验异常")s exp = payloadObj.exp#; 这里只计算了日期,需要对比时间根据需要自行判断q:(+$zdth(exp, 3) < +$h) $$$JwtException("JWT已经过期")q $$$OK
}

在这里插入图片描述

后端数据方法:

ClassMethod Data()
{q $$$OK
}

总结

使用场景:

  • 支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题。
  • 无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力。
  • 更适用于移动端:当客户端是非浏览器平台时,cookie是不被支持的,此时采用token认证方式会简单很多。
  • 无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御。
  • 单点登录。

在使用 JWT 时需要注意以下事项:

  1. JWT 默认不加密,如果要写入敏感信息必须加密,可以用生成的原始令牌再次对内容进行加密。

  2. JWT无法使服务器保存会话状态,当令牌生成后在有效期内无法取消也不能更改。

  3. JWT 包含认证信息,如果泄露了,任何人都可以获得令牌所有的权限;因此 JWT 有效期不能太长,对于重要操作每次请求都必须进行身份验证。

完整代码

  • M.Broker.cls
Class M.Broker Extends (%CSP.Page, %CSP.REST)
{/// 设置为"utf-8",否则中文乱码
Parameter CHARSET = "utf-8";ClassMethod OnPreHTTP() As %Boolean
{#dim %response as %CSP.Response#dim %session as %CSP.Session#; js是否可读cookie#; s %response.UseHttpOnly = 1/* 星号表示所有的域都可以接受 */d %response.SetHeader("Access-Control-Allow-Origin", ..GetOrigin())#; 允许请求方式,例如get。postd %response.SetHeader("Access-Control-Allow-Methods", "*")#; 允许头信息d %response.SetHeader("Access-Control-Allow-Headers", "x-requested-with,content-type")#; 允许验证d %response.SetHeader("Access-Control-Allow-Credentials", "true")#; 额外暴露头信息#;d %response.SetHeader("Access-Control-Expose-Headers", "cookie")#; 超时时间d %response.SetHeader("Access-Control-Max-Age", 3600)/* 设置返回结构为Json */d %response.SetHeader("Content-Type", "application/json")#; 每次请求将Session超时时间重新设置s %session.AppTimeout = 900if ($zv [ "IRIS") {#; 确定是否应该使用 sessionId cookie 发送 "Secure" 标志的内部属性s %session.SecureSessionCookie = 1#; 确定如何严格限制 sessionId cookie 域的属性。选项有 None (0)Lax (1)Strict (2),其中 Strict 表示 cookie 只能在当前应用程序中使用。默认为 CSP 应用程序的相应设置。除非另有配置,否则应用程序默认为 Strict。请注意,None 对于不安全 (HTTP) 连接无效s %session.SessionScope = 0#; 用于确定要与用户创建的 cookie 一起发送的 SameSite 属性的属性。选项有 None (0)Lax (1)Strict (2)。默认为 CSP 应用程序的相应设置。除非另有配置,否则应用程序默认为 Strict。请注意,None 对于不安全 (HTTP) 连接无效。s %session.UserCookieScope = 0}q 1
}/// desc:返回页面数据方法
ClassMethod OnPage() As %Status
{s pClassName = $g(%request.Data("ClassName",1))s pMethodName = $g(%request.Data("MethodName",1)) /* 输出返回值到页面上 */w ..RunMethod(pClassName, pMethodName)q $$$OK
}/// desc:运行通用方法
ClassMethod RunMethod(pClassName, pMethodName) [ ProcedureBlock = 0 ]
{s $zt = "Error"d ..GetInfo()q:(pMethodName'="Login")&&('$d(%request.CgiEnvs("HTTP_AUTHORIZATION"))) ..Failure2Json("请求头中AUTHORIZATION不存在!")q:(pClassName = "") ..Failure2Json("类不能为空!")q:(pMethodName = "") ..Failure2Json("方法不能为空!")d:(pMethodName'="Login") ##class(M.Jwt).VerifyJwt()s requestMethod = $lb("POST", "GET")q:('$lf(requestMethod, %request.CgiEnvs("REQUEST_METHOD")))try {s result = $classmethod(pClassName, pMethodName)} catch e {s msg = e.Name _ e.Location _ " *" _ data _ $zeret ..Failure2Json(msg)}q ..Success2Json(result)
Error s $zt = ""ret ..Failure2Json("意外没有捕捉错误的错误:" _ $ze)
}/// 返回消息Json
ClassMethod Msg(code, msg = "", data = "") As %DynamicObject
{s ret = {}s ret.code = codes ret.msg = msgs ret.data = dataq ret
}/// 无Session判断
ClassMethod Session() As %DynamicObject
{q ..Msg(-2, "", "")
}/// 返回前台的成功消息转Json
ClassMethod Session2Json() As %String
{q ..Session().%ToJSON()
}/// 返回前台的成功消息
ClassMethod Success(data) As %DynamicObject
{q ..Msg(200, "", data)
}/// 返回前台的成功消息转Json
ClassMethod Success2Json(data) As %String
{q ..Success(data).%ToJSON()
}/// 返回前台分页数据的成功消息转Json
ClassMethod Success2Json4Page(data, total) As %String
{s ret = ..Success(data)d ret.%Set("size" ,$g(%request.Data("size", 1)) ,"number")d ret.%Set("current" ,$g(%request.Data("current", 1)) ,"number")d ret.%Set("total" ,total ,"number")q ret.%ToJSON()
}/// 返回前台的失败消息
ClassMethod Failure(msg, data = "") As %DynamicObject
{q ..Msg(-1, msg, data)
}/// 返回前台的失败消息转Json
ClassMethod Failure2Json(msg, data = "") As %String
{q ..Failure(msg, data).%ToJSON()
}/// 返回前台的警告消息
ClassMethod Warning(msg, data = "") As %DynamicObject
{q ..Msg(0, msg, data)
}/// 返回前台的警告消息转Json
ClassMethod Warning2Json(msg, data = "") As %String
{q ..Warning(msg, data).%ToJSON()
}ClassMethod GetOrigin()
{q:'$d(%request) ""q:'$d(%request.CgiEnvs("HTTP_ORIGIN")) ""s origin = %request.CgiEnvs("HTTP_ORIGIN")q origin
}ClassMethod GetAuthorization()
{q:'$d(%request) ""q:'$d(%request.CgiEnvs("HTTP_AUTHORIZATION")) ""s token = %request.CgiEnvs("HTTP_AUTHORIZATION")q token
}}
  • M.Jwt.cls
Include M.JwtClass M.Jwt Extends %RegisteredObject
{ClassMethod Login()
{#; todo 验证用户密码信息#; 返回JWTq ..GenerateJwt()
}ClassMethod Data()
{q $$$OK
}Parameter DOT = ".";Parameter Sign = "yx";Parameter Key = 00000000111111112222222233333333;/// w ##class(M.Jwt).GetJwt()
ClassMethod VerifyJwt()
{#; 验证是否携带JWTq:'$d(%request) ""q:'$d(%request.CgiEnvs("HTTP_AUTHORIZATION")) $$$JwtException("请求头中AUTHORIZATION不存在!")#; 获取JWTs token = %request.CgiEnvs("HTTP_AUTHORIZATION")s jwt = $p(token," " ,2)s headerPart = $p(jwt, ".", 1)s payloadPart = $p(jwt, ".", 2)s signaturePart = $p(jwt, ".", 3)#; 解析header部分s header = ..Base64Decryption(headerPart)s headerObj = {}.%FromJSON(header)#; 解析payload部分s payload = ..Base64Decryption(payloadPart)s payloadObj = {}.%FromJSON(payload)#; 解析重新验签判断数据是否被修改过s content = headerPart _ ..#DOT _ payloadParts signatureConetent = ..HMACSHABase64Encode(content, ..#Key)q:(signatureConetent '= signaturePart) $$$JwtException("签名校验异常")#; 其他验证信息,根据需求自行判断q:(payloadObj.iss '= ..#Sign) $$$JwtException("IIS发行人校验异常")s exp = payloadObj.exp#; 这里只计算了日期,需要对比时间根据需要自行判断q:(+$zdth(exp, 3) < +$h) $$$JwtException("JWT已经过期")q $$$OK
}/// w ##class(M.Jwt).SetJwt()
ClassMethod GenerateJwt()
{s headerPart = ..GenerateHeaderPart()s payloadPart = ..GeneratePayloadPart()s signaturePart = ..GenerateSignaturePart(headerPart, payloadPart)s jwt = headerPart _ ..#DOT _ payloadPart _ ..#DOT _ signaturePartq jwt
}/// w ##class(M.Jwt).GenerateHeaderPart()
/// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
/// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.RkVBMjAwOEYwNzIwRDdENzI0MURCMzUxRjhGMzcyOUI5Mjc3MEYzOENFMjgxQjcyNTdDRTU3QTBEQTJDNTlCMw==
ClassMethod GenerateHeaderPart()
{s header = {}s header.alg = "HS256"s header.typ = "JWT"s headerPart = ..Base64UrlEncryption(header.%ToJSON())q headerPart
}/// w ##class(M.Jwt).GeneratePayloadPart()
ClassMethod GeneratePayloadPart()
{s payload = {}s payload.iss = ..#Signs payload.jti = ##class(%SYSTEM.Encryption).GenCryptToken()s payload.exp = $zd(($h + 7), 3) _ " " _$zt($p($h, ",", 2), 1)s payload.nbf = $zdt($h, 3)s payload.iat = $zdt($h, 3)s payloadPart = ..Base64UrlEncryption(payload.%ToJSON())q payloadPart
}/// w ##class(M.Jwt).GenerateSignaturePart()
/// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9._qIAjwcg19ckHbNR-PNym5J3DzjOKBtyV85XoNosWbM
ClassMethod GenerateSignaturePart(headerPart, payloadPart)
{s content = headerPart _ ..#DOT _ payloadParts signaturePart = ..HMACSHABase64Encode(content, ..#Key)q signaturePart
}ClassMethod Base64UrlEncryption(str As %String)
{s ret = $zcvt(str, "O", "UTF8")s ret = ##class(%SYSTEM.Encryption).Base64Encode(ret, 1)s ret = ..ConvertUrl(ret)q ret
}ClassMethod ConvertUrl(str)
{q $tr(str, "+/=", "-_")
}ClassMethod Base64Decryption(str As %String)
{s ret = ##class(%SYSTEM.Encryption).Base64Decode(str)q ret
}/// w ##class(M.Jwt).HMACSHABase64Encode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ5eCIsImp0aSI6IjY2MzkyMTU5IiwiZXhwIjoiMjAyMy0wMS0xOSAxMjoxNDozNCIsIm5iZiI6IjIwMjMtMDEtMTIgMTI6MTQ6MzQiLCJpYXQiOiIyMDIzLTAxLTEyIDEyOjE0OjM0In0","00000000111111112222222233333333")
ClassMethod HMACSHABase64Encode(str As %Text, key As %String) As %String
{s str = $zcvt(str, "O", "UTF8")s key = $zcvt(key, "O", "UTF8")s ret = ##class(%SYSTEM.Encryption).HMACSHA("256", str, key)s ret = ##class(%SYSTEM.Encryption).Base64Encode(ret, 1)s ret = ..ConvertUrl(ret)q ret
}}
  • M.Jwt.inc
#define JwtException(%msg) ##class(M.JwtException).%New("",-100, %msg)

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

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

分析一次double强转float的翻车原因

背景 人逢喜事精神爽,总算熬到下班撩~~ 正准备和同事打个招呼回家,被同事拖住问了. ?‍♂️: 你们组做的那块代码,把double类型数据成float有问题啊?. ?‍♀️: 嗯?不对是正常啊,float精度是没有double高,但float能保存到小数点后好多位,对我们来说完全够用了! ?‍♂️: 不…

马的种类(十三)

渤海马渤海马主要产于中国山东省东北部的惠民、昌潍、烟台三地区和潍坊市沿渤海各县&#xff0c;以广饶、寿光和垦利三县为中心产区。渤海马属挽乘兼用型。体质结实&#xff0c;结构匀称&#xff0c;性情温驯&#xff0c;公马有悍威。头清秀&#xff0c;呈直头&#xff0c;眼大…

oppofindx3pro分辨率设置方法

Oppofindx3pro手机的分辨率仍然很好。但是&#xff0c;这不是默认设置&#xff0c;需要用户手动调整。您还不知道Oppofindx3pro分辨率在哪里吗&#xff1f;快来看看下面的小编带来的这个方法。相信一定能帮助你&#xff01; oppofindx3pro分辨率设置2021教程分享 1、打开手机…

手机html像素,手机分辨率和网页中的PX是一回事吗?

大家好&#xff0c;我是IT修真院郑州分院第一期的学员胡嘉杰&#xff0c;一枚正直纯洁善良的WEB前端程序员。今天给大家分享一下&#xff0c;修真院官网CSS任务3&#xff0c;深度思考中的知识点——手机分辨率和网页中的PX是一回事吗&#xff1f;1.背景介绍在我们从设计人员那里…

Lecture_2_4 线性回归中的系数,衡量了什么?

1、beta系数的含义 比如做Y与X1、X-1的回归&#xff0c;X1为解释变量中的一个变量&#xff0c;X-1是除X的其他解释变量组。 &#xff08;1&#xff09;当我们回归Y与X-1&#xff0c;能得出Y中能由X-1部分变动解释的部分&#xff08;黑线&#xff09;&#xff0c;剔除了黑线&am…

统计学——线性回归决定系数R2

决定系数&#xff08;coefficient ofdetermination&#xff09;&#xff0c;有的教材上翻译为判定系数&#xff0c;也称为拟合优度。 决定系数反应了y的波动有多少百分比能被x的波动所描述&#xff0c;即表征依变数Y的变异中有多少百分比,可由控制的自变数X来解释. 表达式&…

机器学习小窥系列...Logistic回归算法中回归系数详解

Logistic回归总结 作者&#xff1a;洞庭之子 微博&#xff1a;洞庭之子-Bing &#xff08;2013年11月&#xff09; PDF下载地址&#xff1a;http://download.csdn.net/detail/lewsn2008/6547463 1.引言 看了Stanford的Andrew Ng老师的机器学习公开课中关于Logistic Regression的…

[GXYCTF2019]禁止套娃(无参数RCE)

目录 信息收集 知识讲解 涉及函数 PHP的正则表达式 无参rce 用到的函数 思路分析 方法一 方法二 信息收集 拿到这道题&#xff0c;抓包看了看&#xff0c;啥也没有&#xff0c;用dirsearch爆破目录发现.git目录&#xff0c;猜测存在.git源码泄露&#xff0c;用githac…

【插画与美文】 平静而幸福的画面——福井良佑水彩

福井良佑淡彩风景 让人感觉亲切&#xff0c;温和。 能带给人某种心境&#xff0c; 让人回想起好的时光&#xff0c; 使人愉快。 这种平凡简单的景象&#xff0c; 在疲惫时能够从中获取一丝温暖&#xff0c; 只是这样就足够了&#xff01;

绘画系统的简单实现(p5.js)

绘画定义 搜狗词条给了“绘画”一个很有意思的词----“猴子的艺术”&#xff0c;这是在中世纪的欧洲这样形容的&#xff0c;我觉得十分地贴切&#xff0c;因为如同猴子喜欢模仿人类活动一样&#xff0c;绘画也是模仿场景。 对绘画的基本定义------绘画是造型艺术中最主要的一…

HTML画廊效果,HTML5实践-使用css装饰图片画廊的代码分享(一)

本节课我们将介绍&#xff0c;如何使用css在不修改图片源的前提下装饰你的图片画廊。这里用到的技巧也很简单&#xff0c;就是在图片之前创建一个&#xff0c;并在span上使用background-image生成一个遮罩的效果。这种方式既简单又灵活&#xff0c;demo中介绍了20多种样式&…

怎样用PS把照片处理成水彩画效果

首先我们需要下载“做出静物风景照片水彩画效果PS动作”陌鱼社区可找到动作&#xff0c;然后用这个动作就可以做出下图的效果&#xff0c;动作支持CS6以上版本软件&#xff0c;下面请看演示教程。 01、载入画笔、图案、动作&#xff0c;关闭软件。 02、接下来我们需要看一下之前…

风景水彩画教程中文版 跟傅兰克学画画全16集高清

跟富兰克学画画 风景水彩画教程中文版 跟傅兰克学画画全16集高清配音&#xff1a;国语说明&#xff1a;高清晰画质&#xff0c;国语版&#xff01;平均一部约1000M 作者简介 傅兰克&#xff0c;生于爱尔兰都柏林&#xff0c;是爱尔兰最受欢迎的画家之一。由BBC出版的《傅兰克克…

TreeMap和TreeSet的介绍

目录 1、认识 TreeMap 和 TreeSet 2、TreeMap 的主要成员变量 3、TreeMap 的主要构造方法 4、TreeMap 和 TreeSet 的元素必须可比较 5、TreeMap 和 TreeSet 关于 key 有序 6、TreeMap 和 TreeSet 的关系 7、总结 1、认识 TreeMap 和 TreeSet TreeMap 和 TreeSet 是Ja…

MSDN Library - October 2001 精简方法

VC中使用MSDN Library - October 2001这个版本时&#xff0c; 安装后&#xff0c;包含一些自己用不到的资料&#xff0c;真的好烦。 比如查找CreateWindow 弹出的内容要选择&#xff0c;一不小心就要转到winCE的说明。 真的麻烦。 只要定义一下MSDN的MSDN130.COL文件&#…

MSDN的下载和使用

很多同学在初学C语言时&#xff0c;面对C语言中如此多的关键字、操作符、库函数感到非常头大。 利用MSDN这个工具可以帮助我们快速了解它们。 目录 MSDN的下载 MSDN的使用 索引 函数介绍 函数结构 头文件 返回值介绍 示例 MSDN的下载 完整版的MSDN非常大&#xff0c…

运算放大器:运放的平衡电阻

1&#xff0e;主要运放参数 集成运放的基本参数很多&#xff0c;包括静态技术指标&#xff08;直流参数&#xff09;和动态技术指标&#xff08;交流参数&#xff09;。。例如&#xff0c;输入失调电压、输入失调电流、输入偏置电流、输入失调电压温漂、最大差模输入电压、最大…

链路聚合(动态捆绑链路)、负载均衡详解、全双工与半双工区别、LACP优先级详解(附图)

目录 一、链路捆绑优点&#xff1a; 二、链路聚合方式&#xff1a; &#xff08;1&#xff09;手工静态绑定&#xff1a; &#xff08;2&#xff09;动态协商&#xff1a; 全双工模式&#xff1a; 半双工模式&#xff1a; 三、链路聚合——负载均衡&#xff1a; 负载均衡…

平衡二叉树的插入与删除

定义 AVL树是带有平衡条件的二叉查找树。它要求在AVL树中任何节点的两个子树的高度(高度是指节点到一片树叶的最长路径的长) 最大差别为1,如下图所示: 为什么有AVL树 大多数BST操作&#xff0c;例如查找&#xff0c;找最大&#xff0c;最小值&#xff0c;插入&#xff0c;删除…

再论颜色校正-白平衡之动态阈值 Python和c#实现

理论我就不写了&#xff0c;可以参考<一种动态阈值白平衡算法实现>&#xff0c;公式部分写的还比较详细&#xff0c;我就不赘述了。只是网上我搜到的这个算法的实现只有java和c版本的&#xff0c;所以就自己写了python版本的&#xff0c;只是对于理论上说的把图像分块计算…