基于M实现的JWT解决方案
文章目录
- 基于`M`实现的`JWT`解决方案
- 简介
- 现状
- 原理
- JWT 组成结构
- 头部`Header`
- 有效载荷`Payload`
- 哈希签名`Signature`
- JWT完整结果
- `JWT`基于`M`的使用流程
- 总结
- 完整代码
基于M
实现的JWT
解决方案
简介
JWT
英文名是Json Web Token
,是一种用于通信双方之间传递安全信息的简洁的、URL
安全的表述性声明规范,经常用在跨域身份验证。
JWT
以JSON
对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。
现状
在讲解 JWT
之前我们先来看一个问题。我们都知道互联网服务的身份验正过程是这样的,客户端向服务器发送登录名和登录密码,服务器验证后将对应的相关信息保存到当前会话中,这些信息包括权限、角色等数据。
服务器向客户端返回 Session
,Session
信息都会写入到客户端的 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 SHA256
, alg
属性值 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
指定七个默认字段供选择,也可以省略,一般建议使用。包括以下内容:
iss
:jwt
的签发者/发行人sub
:主题aud
:接收方exp
:jwt
过期时间nbf
:jwt
生效时间iat
:签发时间jti
:jwt
唯一身份标识,可以避免重放攻击
也可以自定义字段:
{"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
头定义的算法生成哈希。哈希签名的过程如下:
指定密码,密码保存在服务器中,不能向客户端公开。
使用
JWT
头指定的算法进行签名,进行签名前需要对JWT
头Header
和有效载荷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
的使用流程
- 首先创建后端门面接口,用户处理通用权限校验和接收反射需要调用的方法:
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)
}
}
- 模拟客户端使用用户名和密码请求登录返回
Jwt
。服务端收到请求,验证用户名和密码。验证成功后,服务端会签发一个JWT token
,再把这个JWT token
返回给客户端。
后端登录方法:
Class M.Jwt Extends %RegisteredObject
{ClassMethod Login()
{#; todo 验证用户密码信息#; 返回JWTq ..GenerateJwt()
}}
使用PostMan模拟接口登录:
{"code": 200,"msg": "","data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ5eCIsImp0aSI6IjQ3MDY0MDQ3IiwiZXhwIjoiMjAyMy0wMS0yNSAxMDoyMzo1NCIsIm5iZiI6IjIwMjMtMDEtMTggMTA6MjM6NTQiLCJpYXQiOiIyMDIzLTAxLTE4IDEwOjIzOjU0In0.IcYRp5BqGtslB94lNYmgIrvIwuyne347HjmHkNmAceo"
}
- 客户端收到
token
后可以把它存储起来,比如放到sessionStorage
中。客户端每次向服务端请求资源时需要携带服务端签发的JWT token
,可以在header中携带。
使用PostMan
模拟请求数据:
- 把请求到的
JWT
信息放到PostMan
->Authorization
->TYPE = Bear Token
-> 设置Token
中。 - 前端在每次请求时将
JWT Token
放入HTTP
请求头中的Authorization
属性中(解决XSS
和XSRF
问题)。
- 服务端收到请求,然后去验证客户端请求里面带着的
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
时需要注意以下事项:
JWT
默认不加密,如果要写入敏感信息必须加密,可以用生成的原始令牌再次对内容进行加密。JWT
无法使服务器保存会话状态,当令牌生成后在有效期内无法取消也不能更改。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)
基于M实现的JWT解决方案相关推荐
- 基于Spring Security与JWT实现单点登录
基于RBAC的权限管理 RBAC(Role-Based Access Control):基于角色的访问控制 当前项目中,RBAC具体的表现为: 管理员表:ams_admin 角色表:ams_role ...
- 构建一个基本的前端自动化开发环境 —— 基于 Gulp 的前端集成解决方案(四)
构建一个基本的前端自动化开发环境 -- 基于 Gulp 的前端集成解决方案(四) 参考文章: (1)构建一个基本的前端自动化开发环境 -- 基于 Gulp 的前端集成解决方案(四) (2)https: ...
- REST API 基于ACCESS TOKEN 的权限解决方案
REST API 基于ACCESS TOKEN 的权限解决方案 参考文章: (1)REST API 基于ACCESS TOKEN 的权限解决方案 (2)https://www.cnblogs.com/ ...
- 基于FastJson的通用泛型解决方案
基于FastJson的通用泛型解决方案 参考文章: (1)基于FastJson的通用泛型解决方案 (2)https://www.cnblogs.com/scy251147/p/9451879.html ...
- #研发解决方案介绍#基于StatsD+Graphite的智能监控解决方案
2019独角兽企业重金招聘Python工程师标准>>> 关键词: 监控 .dashboard.PHP.graphite.statsd.whisper.carbon.grafana.i ...
- 单例设计模式-静态内部类-基于类初始化的延迟加载解决方案及原理解析
刚刚线程1看不到线程0的重排序,我们创建一个类,这个方案是使用静态内部类来解决,一会我们也会分析一下原理,我们创建一个静态内部类,静态内部类的代理模式,JVM在类的初始化阶段,也就是class被加载后 ...
- 氮化镓 服务器电源管理系统报价,基于氮化镓的电源解决方案总体拥有成本评估...
引言 近年来,电信市场正在朝云计算的方向转变,这导致超大规模数据中心空前快速的增长,而每个机架需要处理的功能也越来越多. 反过来,这种趋势也意味着对功率的需求快速增加,而重点则是采用消耗更少电力的更高 ...
- 云图说|图解DGC:基于华为智能数据湖解决方案的一体化数据治理平台
摘要:数据湖治理中心DGC,帮助企业快速构建从数据集成到数据服务的端到端智能数据系统,消除数据孤岛,统一数据标准,加快数据变现,实现数字化转型. 本文分享自华为云社区<[云图说]第232期 图解 ...
- 基于yaf+yar微服务解决方案教程
基于yaf+yar微服务解决方案教程 大纲 主要内容 [课程地址](https://edu.csdn.net/course/detail/9933) 大纲 主要内容 课程地址
最新文章
- 归并排序详解(python实现)
- java 7zip解压_Apache Commons Compress介绍-JAVA压缩解压7z文件
- [RDLC]报表根据字段列动态加载图片(二)
- python做炫酷的界面_用python打造可视化爬虫监控系统,酷炫的图形化界面
- 数字三角形的显示 java
- 拓端tecdat|stata马尔可夫Markov区制转移模型分析基金利率
- mysql 组复制详解_MySQL 5.7: 使用组复制(MySQL Group Replication)
- java密码验证代码_java用户名密码验证示例代码分享
- 数据可视化——ECharts基础
- db2 随机数函数_sql中的随机函数怎么用?
- 如何测一个纸杯_阿薇塔罗——4个吊坠,凭直觉选一个,测你这辈子婚姻状况如何?...
- 怎么接微信公众号人工服务器,怎么设置微信公众号接入人工客服?
- 杂记之关于视频、音频编/解码
- 各类邮箱谷歌邮箱、Outlook邮箱、雅虎邮箱的购买养号策略
- 学计算机i58300够用嘛,酷睿i58300h处理器怎么样
- 工业设计公司如何进行家电设计创新?
- 创业生活经历:我的非洲回忆录
- python输入日期选择日期_python输入日期输出星座?
- 【BZOJ】3007 拯救小云公主 最短路径
- nao机器人导入自己写的python程序_python程序控制NAO机器人行走
热门文章
- r语言归一化_R语言scale()标准化
- J storm战队成员_DOTA2J.Storm战队介绍-DOTA2梦幻联赛S11预选赛J.Storm战队介绍_牛游戏网攻略...
- 查5G覆盖:中国联通客户端提供查询服务,快看你家门口有5G吗
- Android基础再回首——四大组件之Activity、Service俩兄弟
- 如何用panda3d写一个游戏
- com.neenbedankt.android-apt
- laravel-admin扩展wang-Editor上传图片设置
- python 远程控制实例
- 一对老耗子,每个月都生一对小耗子。小耗子长3个月,第四个开始变成老耗子开始生! 假如都不死,那么请问24个月后有多少只耗子?...
- OpenGL学习脚印: 绘制一个三角形