Java Web Token 之 JJWT 使用
文章目录
- Java Web Token 之 JJWT 使用
- 1. JJWT 简介
- 2. JJWT 引入
- 2.1 Maven依赖
- 3. 快速入门
- 3.1 构建 JWT
- 3.2 解析 JWT
- 4. JWT 加密签名实现
- 4.1 JWT 签名算法介绍
- 4.1.1 支持算法类型
- 4.1.2 算法使用要求
- 4.2 创建 JWS
- 4.2.1 设置 Header Parameters
- 4.2.2 设置 Claims
- 标准的 Claims
- 4.2.3 签名 Signing Key
- 4.3 解析 JWS
- 4.3.1 校验 Key
- 4.3.2 签名密钥解析器 Signing Key Resolver
- 4.3.3 Claims 断言
- 5. 压缩
- 5.1 默认压缩
- 5.2 自定义压缩
- Learn More
- 参考文档
Java Web Token 之 JJWT 使用
1. JJWT 简介
JJWT Github Repository
JJWT
旨在成为最容易使用和理解的库,用于在JVM
和Android
上创建和验证JSON Web令牌(JWT)
。
JJWT
是一个纯Java实现,完全基于JWT
,JWS
,JWE
,JWK
和JWA RFC
规范以及Apache 2.0
许可条款下的开源。
2. JJWT 引入
2.1 Maven依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.5</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.5</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.5</version><scope>runtime</scope>
</dependency>
<!-- Uncomment this next dependency if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.60</version><scope>runtime</scope>
</dependency>
-->
上面的依赖声明只有一个编译时依赖项,其余的声明为运行时依赖项。这是因为JJWT在应用程序中明确设计的API,使其所有其他内部实现细节降级为仅运行时依赖项。
永远不要使用编译范围将jjwt-impl .jar
添加到项目中,始终使用运行时范围声明它。精心策划jjwt-api .jar
并确保它包含的内容并尽可能保持向后兼容。
运行时jjwt-impl .jar
策略为JJWT
开发人员提供了随时随地更改内部包和实现的灵活性。
3. 快速入门
3.1 构建 JWT
@Test
public void getJWTTest() {Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);String jws = getJwtStr(key);if (log.isDebugEnabled()) {log.debug(jws);}
}private String getJwtStr(Key key) {return Jwts.builder().setSubject("JDKONG").signWith(key).compact();
}
是不是很简单!
在以上代码中,构建的过程如下:
- 构建一个主题为
JDKONG
的JWT
; - 使用适用于
HMAC-SHA-256
算法的密钥对JWT
进行签名; - 最后,将它压缩成最终的String形式。 签名的
JWT
称为JWS
。
最终生成的JWT
如下所示:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKREtPTkcifQ.C-eSTnoK-lryYVerB6SCbgbTRMKpXyWvDJNNPH07g3Q
3.2 解析 JWT
现在,通过类似的方式验证JWT:
@Test
public void parseJwtStr() {// 得到密钥Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);// 得到 JWTString jwtStr = getJwtStr(key);// 验证 JWTassert Jwts.parser().setSigningKey(key).parseClaimsJws(jwtStr).getBody().getSubject().equals("JDKONG");
}
这里需要注意两件事:
之前的密钥用于验证
JWT
的签名。 如果它无法验证JWT
,则抛出SignatureException
(从JwtException扩展)。如果
JWT
已经过验,会接着断言该claim
设置为JDKONG
。如果都没问题,则验证通过。
如果,在验证的过程中失败了会怎样呢?其实,在做JWT
解析时,我们可以捕捉异常JwtException
,比如:
try {Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws);//OK, we can trust this JWT
} catch (JwtException e) {//don't trust the JWT!
}
4. JWT 加密签名实现
JWT
本身是支持加密签名的,在使用签名的JWT
时,需要注意一下两点:
- 保证JWT是由我们认识的人(它是真实的)创建的
- 保证在创建JWT之后没有人操纵或改变JWT(保持其完整性)。
真实性
和完整性
保证JWT
包含可以信任的信息。 如果JWT
未通过真实性或完整性检查,应该始终拒绝JWT
,因为我们无法信任它。
那么JWT
如何签约? 让我们通过一些易于阅读的伪代码来完成它:
假设我们有一个带有JSON头和主体的JWT:
header
{"alg": "HS256" }
body
{"sub": "JDKOGN" }
删除JSON中所有不必要的空格:
String header = '{"alg":"HS256"}'
String claims = '{"sub":"JDKONG"}'
- 对他们分别进行
UTF_8
编码:
String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )
- 将编码后的
Header
和Body
使用.
进行分隔,并连接成一个字符串:
String concatenated = encodedHeader + '.' + encodedClaims
- 使用加密秘密或私钥,选择的签名算法(此处使用HMAC-SHA-256),并对连接的字符串进行签名:
Key key = getMySecretKey()
byte[] signature = hmacSha256( concatenated, key )
- 由于签名始终结果是字节数组,因此
Base64URL
对签名进行编码并使用.
将 它连接到字符串concatenated
后面:
String jws = concatenated + '.' + base64URLEncode( signature )
- 最后生成的结果:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
这被称为
JWS
- 签名JWT
的简称。
下面讨论一下签名算法和密钥,特别是它们与JWT规范相关的内容。了解这些对于能够正确创建JWS至关重要。
4.1 JWT 签名算法介绍
4.1.1 支持算法类型
JWT规范确定了12种标准签名算法–3种密钥算法和9种非对称密钥算法 - 由以下名称标识:
HS256
: HMAC using SHA-256HS384
: HMAC using SHA-384HS512
: HMAC using SHA-512ES256
: ECDSA using P-256 and SHA-256ES384
: ECDSA using P-384 and SHA-384ES512
: ECDSA using P-521 and SHA-512RS256
: RSASSA-PKCS-v1_5 using SHA-256RS384
: RSASSA-PKCS-v1_5 using SHA-384RS512
: RSASSA-PKCS-v1_5 using SHA-512PS256
: RSASSA-PSS using SHA-256 and MGF1 with SHA-256PS384
: RSASSA-PSS using SHA-384 and MGF1 with SHA-384PS512
: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
这些都在io.jsonwebtoken.SignatureAlgorithm
枚举类中表示。
除了它们的安全属性之外,这些算法真正重要的是JWT
规范 RFC 7518第3.2到3.5节 强制要求必须使用对所选算法足够强大的密钥。
这意味着JJWT
也会强制使用足够强的密钥。 如果为给定算法提 供弱键,JJWT
将拒绝它并抛出异常。
JWT
规范以及JJWT
规定密钥长度的原因在于,如果不遵守算法的强制密钥属性,特定算法的安全模型可能完全崩溃,实际上根本没有安全性,这将会导致完全不安全的JWT
。
4.1.2 算法使用要求
HMAC-SHA
JWT HMAC-SHA签名算法HS256,HS384和HS512需要一个密钥,该密钥至少与 RFC 7512第3.2节 中算法的签名(摘要)长度一样多。 这意味着:
HS256
是HMAC-SHA-256
,它产生256位(32字节)长的摘要,因此HS256要求您使用至少32字节长的密钥。HS384
是HMAC-SHA-384
,它产生384位(48字节)长的摘要,因此HS384要求您使用至少48字节长的密钥。HS512
是HMAC-SHA-512
,它产生512位(64字节)长的摘要,因此HS512要求您使用至少64字节长的密钥。
RSA
JWT RSA
签名算法RS256
,RS384
,RS512
,PS256
,PS384
和PS512
都要求每个RFC 7512
第3.3和3.5节的最小密钥长度(也称为RSA模数位长度)为2048位。 任何小于此值的内容(例如1024位)都将被拒绝,并抛出异常InvalidKeyException
。也就是说,为了与最佳实践保持一致并增加键长度,
JJWT
建议考虑使用的:RS256
和PS256
至少有2048位密钥RS384
和PS384
至少3072位密钥RS512
和PS512
至少4096位密钥
这些只是JJWT的建议而非要求。 JJWT仅强制执行JWT规范要求,对于任何RSA密钥,要求是RSA密钥(模数)长度,必须> = 2048位。
Elliptic Curve
JWT
椭圆曲线签名算法ES256
,ES384
和ES512
都需要最小密钥长度(也称为椭圆曲线顺序位长度),其至少与RFC 7512第3.4节中算法签名的各个R和S分量一样多。 这意味着:ES256
要求您使用至少256位(32字节)长的私钥。ES384
要求您使用长度至少为384位(48字节)的私钥。ES512
要求您使用长度至少为512位(64字节)的私钥。
4.2 创建 JWS
首先,可以按如下方式创建JWS
:
- 使用
Jwts.builder()
方法创建JwtBuilder
实例。 - 调用
JwtBuilder
方法根据需要添加标头参数和声明。 - 指定要用于对
JWT
进行签名的SecretKey
或非对称PrivateKey
。 - 最后,调用
compact()
方法进行压缩和签名,生成最终的jws
。
例如:
String jws = Jwts.builder() // (1).setSubject("JDKONG") // (2) .signWith(key) // (3).compact(); // (4)
4.2.1 设置 Header Parameters
JWT Header
提供关于JWT Claims
相关的内容,格式和加密操作的元数据。
如果需要设置一个或多个JWT
头参数,则可以根据需要简单地多次调用JwtBuilder#setHeaderParam
,如下所示:
String jws = Jwts.builder().setHeaderParam("kid", "myKeyId")// ... etc ...
每次调用setHeaderParam
时,它只是将键值对附加到内部Header
实例,如果键值已经存在,则会覆盖任何现有的同名键/值对。
注意:不需要设置
alg
或zip
标头参数,因为JJWT
会根据使用的签名算法或压缩算法自动设置它们。
除此之外,你还可以使用另外两种方式,设置JWT Header
,如下所示:
- 方式 2:
Header header = Jwts.header();
populate(header); //implement me
String jws = Jwts.builder().setHeader(header)// ... etc ...
- 方式 3:
Map<String,Object> header = getMyHeaderMap(); //implement me
String jws = Jwts.builder().setHeader(header)// ... etc ...
方式2 与 方式3 需要注意的是:调用setHeader
将覆盖任何现有的同名的key/value
对。 在所有情况下,JJWT
仍将设置(并覆盖)任何alg
和zip
标头,无论它们是否在指定的标头对象中。
4.2.2 设置 Claims
Claims
是JWT
的正文部分,包含JWT
创建者希望向JWT
收件人提供的信息。
标准的 Claims
setIssuer
: sets theiss
(Issuer) ClaimsetSubject
: sets thesub
(Subject) ClaimsetAudience
: sets theaud
(Audience) ClaimsetExpiration
: sets theexp
(Expiration Time) ClaimsetNotBefore
: sets thenbf
(Not Before) ClaimsetIssuedAt
: sets theiat
(Issued At) ClaimsetId
: sets thejti
(JWT ID) Claim
例如:
String jws = Jwts.builder().setIssuer("me").setSubject("Bob").setAudience("you").setExpiration(expiration) //a java.util.Date.setNotBefore(notBefore) //a java.util.Date .setIssuedAt(new Date()) // for example, now.setId(UUID.randomUUID()) //just an example id/// ... etc ...
自定义 Claims
如果需要设置一个或多个与上面显示的标准setter方法声明不匹配的自定义声明,可以根据需要多次调用JwtBuilder#claim
声明:
String jws = Jwts.builder().claim("hello", "world")// ... etc ...
每次调用claim
时,它只是将键值对附加到内部claims
实例,如果键值已经存在,则会覆盖任何现有的同名key/value
对。
同上,你还可以使用另外两种方式,设置JWT Claims
,如下所示:
- 方式 2:
Claims claims = Jwts.claims();
populate(claims); //implement me
String jws = Jwts.builder().setClaims(claims)// ... etc ...
- 方式 3:
Map<String,Object> claims = getMyClaimsMap(); //implement me
String jws = Jwts.builder().setClaims(claims)// ... etc ...
同样,方式2 与 方式3 需要注意的是:调用setClaims
将覆盖任何现有的同名的key/value
对。
4.2.3 签名 Signing Key
建议通过调用JwtBuilder
的signWith
方法来指定签名密钥,并让JJWT
确定指定密钥允许的最安全算法:
String jws = Jwts.builder()// ... etc ....signWith(key) // <---.compact();
例如,如果使用长度为256位(32字节)的SecretKey
调用signWith
,则对于HS384
或HS512
,它不够强大,因此JJWT
将使用HS256
自动对JWT
进行签名。
使用signWith
时,JJWT
还会自动使用相关的算法标识符设置所需的alg
头。
类似地,如果使用长度为4096位的RSA PrivateKey
调用signWith
,JJWT
将使用RS512
算法并自动将alg
头设置为RS512
。
相同的选择逻辑适用于Elliptic Curve PrivateKeys
。
注意:你不能用
PublicKeys
签署JWT
,因为这总是不安全的。JJWT
将拒绝任何指定的PublicKey
的方式签名,并抛出异常:InvalidKeyException
。
- 自定义 签名算法
在某些特定情况下,您可能希望覆盖给定键的JJWT
默认选定算法。
例如,如果有一个2048位
的RSA PrivateKey
,JJWT
会自动选择RS256
算法。 如果使用RS384
或RS512
,可以使用重载的signWith
方法手动指定它,该方法接受SignatureAlgorithm
作为附加参数:
.signWith(privateKey, SignatureAlgorithm.RS512) // <---.compact();
这是允许的,因为JWT规范允许任何RSA密钥> = 2048位
的任何RSA
算法强度。JJWT
只需要RS512的键> = 4096位
,然后是RS384
,键> = 3072位
,最后是RS256
,键> = 2048位
。
但是,无论您选择哪种算法,JJWT
都会断言,根据JWT
规范要求,允许将指定的密钥用于该算法。
4.3 解析 JWS
按如下方式解析JWS
:
- 使用
Jwts.parser()
方法创建JwtParser
实例。 - 指定要用于验证
JWS
签名的SecretKey
或非对称PublicKey
- 最后,使用
jws String
调用parseClaimsJws(String)
方法,生成原始JWS
。 - 整个调用将包装在
try/catch
块中,以防解析或签名验证失败。
Jws<Claims> jws;
try {jws = Jwts.parser() // (1).setSigningKey(key) // (2).parseClaimsJws(jwsString); // (3)// we can safely trust the JWT
catch (JwtException ex) { // (4)// we cannot use the JWT as intended by its creator
}
4.3.1 校验 Key
阅读JWS
时,最重要的事情是指定用于验证JWS加密签名的密钥。 如果签名验证失败,则无法安全地信任此JWT
,应将其丢弃。
那么我们使用哪个密钥进行验证?
如果jws
是使用SecretKey
签名的,则应在JwtParser
上指定相同的SecretKey
。 例如:
Jwts.parser().setSigningKey(secretKey) // <----.parseClaimsJws(jwsString);
如果jws
是使用PrivateKey
签名的,那么应该在JwtParser
上指定该密钥相应的PublicKey
(不是PrivateKey
)。 例如:
Jwts.parser().setSigningKey(publicKey) // <---- publicKey, not privateKey.parseClaimsJws(jwsString);
如果你的应用程序不只使用一个SecretKey或KeyPair会怎么样? 如果可以使用不同的SecretKeys或公钥/私钥或两者的组合创建JWS,该怎么办?
在这些情况下,无法使用单个键调用JwtParser
的setSigningKey
方法。相反,需要使用SigningKeyResolver
,接下来介绍。
4.3.2 签名密钥解析器 Signing Key Resolver
如果程序需要使用不同密钥签名的JWS
,则不会调用setSigningKey
方法。 相反,需要实现SigningKeyResolver
接口并通过setSigningKeyResolver
方法在JwtParser
上指定实例。例如:
SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();
Jwts.parser().setSigningKeyResolver(signingKeyResolver) // <----.parseClaimsJws(jwsString);
事实上,可以通过从SigningKeyResolverAdapter
扩展并实现resolveSigningKey(JwsHeader,Claims)
方法来简化一些事情。 例如:
public class MySigningKeyResolver extends SigningKeyResolverAdapter {@Overridepublic Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {// implement me}
}
在解析JWS JSON
之后,JwtParser
将在验证jws
签名之前调用resolveSigningKey()
方法。 这也就允许检查Jws Header
和Claims
参数,以帮助查找用于验证特定jws
的密钥的信息。 这对于复杂安全模型的应用程序非常强大,这些安全模型可能在不同时间使用不同的密钥或针对不同的用户或客。
JWT
规范支持的方法是在创建JWS
时在JWS
头中设置kid(Key ID)
字段,例如:
Key signingKey = getSigningKey();
String keyId = getKeyId(signingKey); //any mechanism you have to associate a key with an ID is fine
String jws = Jwts.builder().setHeaderParam(JwsHeader.KEY_ID, keyId) // 1.signWith(signingKey) // 2.compact();
然后在解析期间,SigningKeyResolver
可以检查JwsHeader
以获取该kid
,然后使用该值从某个位置查找密钥,如数据库。 例如:
public class MySigningKeyResolver extends SigningKeyResolverAdapter {@Overridepublic Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {//inspect the header or claims, lookup and return the signing keyString keyId = jwsHeader.getKeyId(); //or any other field that you need to inspectKey key = lookupVerificationKey(keyId); //implement mereturn key;}
}
注意,检查
jwsHeader.getKeyId()
只是查找密钥的最常用方法,也可以检查任意数量的标头字段或声明,以确定如何查找验证密钥。
最后要记住,对于
HMAC
算法,返回的验证密钥应该是SecretKey
,对于非对称算法,返回的密钥应该是PublicKey
(不是PrivateKey
)。
4.3.3 Claims 断言
假设你要求正在解析的JWS
具有特定的子sub
值,否则可能不信任该令牌。 那么可以使用JwtParser
上的各种require *
方法之一来实现:
try {Jwts.parser().requireSubject("JDKONG").setSigningKey(key).parseClaimsJws(s);
} catch(InvalidClaimException ice) {// the sub field was missing or did not have a 'JDKONG' value
}
如果缺少某个值而不是不正确的值,那么就不会捕获InvalidClaimException
,而是捕获MissingClaimException
或IncorrectClaimException
:
try {Jwts.parser().requireSubject("JDKONG").setSigningKey(key).parseClaimsJws(s);
} catch(MissingClaimException mce) {// the parsed JWT did not have the sub field
} catch(IncorrectClaimException ice) {// the parsed JWT had a sub field, but its value was not equal to 'JDKONG'
}
当然,也可以使用require(fieldName,requiredFieldValue)
方法来要求自定义字段。例如:
try {Jwts.parser().require("field","requiredValue").setSigningKey(key).parseClaimsJws(s);
} catch(InvalidClaimException ice) {// the 'myfield' field was missing or did not have a 'myRequiredValue' value
}
请参阅JwtParser类
或JavaDoc
以获取可用于声明断言的各种require *
方法的完整列表。
5. 压缩
JWT
规范仅为标准化JWE
(加密JWT)而非JWS
(签名JWT),但JJWT
支持两者。 如果您肯定使用JJWT创建的JWS也将使用JJWT进行解析,则可以将此功能与JWS一起使用,否则最好只将其用于JWE。
如果JWT
的Claim
集足够大,也就是说,它包含许多key/value
对,或者单个值非常大或冗长,那么可以通过压缩声明主体来减小创建的JWS
的大小。
例如,如果在URL
中使用生成的JWS
,压缩可能会很重要,因为由于浏览器,用户邮件代理或HTTP网关兼容性问题,URL
最好保持在4096
个字符以下。 较小的JWT
还有助于降低带宽利用率。
5.1 默认压缩
如果要压缩JWT
,可以使用JwtBuilde
r的compressWith(CompressionAlgorithm)
方法。 例如:
Jwts.builder().compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP// .. etc ...
使用DEFLATE
或GZIP
压缩编解码器,但是在解压缩时,不必执行任何操作,不需要配置JwtParser
,JJWT
将按预期自动解压缩主体。
5.2 自定义压缩
如果在创建JWT
时使用自己的自定义压缩编解码器(通过JwtBuilder compressWith
),则需要使用setCompressionCodecResolver
方法将编解码器提供给JwtParser
。 例如:
CompressionCodecResolver ccr = new MyCompressionCodecResolver();
Jwts.parser().setCompressionCodecResolver(ccr) // <----// .. etc ...
通常,CompressionCodecResolver
实现将检查zip
标头以找出使用的算法,然后返回支持该算法的编解码器实例。 例如:
public class MyCompressionCodecResolver implements CompressionCodecResolver {@Overridepublic CompressionCodec resolveCompressionCodec(Header header) throws CompressionException {String alg = header.getCompressionAlgorithm();CompressionCodec codec = getCompressionCodec(alg); //implement mereturn codec;}
}
Learn More
- JSON Web Token for Java and Android
- How to Create and Verify JWTs in Java
- Where to Store Your JWTs - Cookies vs HTML5 Web Storage
- Use JWT the Right Way!
- Token Authentication for Java Applications
- JJWT Changelog
参考文档
- jjwt
Java Web Token 之 JJWT 使用相关推荐
- Spring框架整合Java Web Token
Java Web Token JSON Web Token(JWT)是一个非常轻巧的规范.这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息. JWT组成 一个JWT实际上就是一个字符串, ...
- Java Web Token - JWT
JWT认证过程:https://www.codetd.com/article/3602378 JWT官方文档:https://jwt.io/introduction/ 转载于:https://www. ...
- JWT/JJWT JSON WEB TOKEN介绍和使用
JSON web Token,简称JWT,本质是一个token,是一种紧凑的URL安全方法(注意是方法,博主刚开始接触一直以为是一种像shiro一样的),用于在网络通信的双方之间传递.一般放在HTTP ...
- Java Web 之Token+Cookie+Session
发展史: 1. 早期的WEB基本上就是文档的浏览而已,服务器不需要记录谁在某个时间都浏览了什么文档,每次请求都是全新的HTTP协议. 2. 随着交互式WEB应用的兴起,例如在线购物网站,需要登录的网站 ...
- java框架 token_Java安全框架(三):JWT(Json Web Token)
一.概述 1.什么是无状态? 简单讲,服务里面一定要存一个session那就是有状态,不需要存就是无状态 2.服务架构演进 单体架构(有状态) 缺陷:Session在服务器中,如果服务器有多个,由于服 ...
- JSON Web Token(缩写 JWT) 目前最流行、最常见的跨域认证解决方案,前端后端都需要会使用的东西
JSON Web Token(缩写 JWT)是目前最流行,也是最常见的跨域认证解决方案.无论是咱们后端小伙伴,还是前端小伙伴对都是需要了解. 本文介绍它的原理.使用场景.用法. 关于封面:这个冬天你过 ...
- JWT (Json Web Token)教程
JWT(Json Web Token)是实现token技术的一种解决方案,JWT由三部分组成: header(头).payload(载体).signature(签名). 头 JWT第一部分是heade ...
- JSON Web Token (JWT)生成Token及解密实战
转载自 JSON Web Token (JWT)生成Token及解密实战 昨天讲解了JWT的介绍.应用场景.优点及注意事项等,今天来个JWT具体的使用实践吧. 从JWT官网支持的类库来看,jjwt是J ...
- 单点登录SSO----JSON Web Token(JWT)机制
JSON Web Token(JWT)机制 pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns: ...
最新文章
- 解决Android Studio中DDMS缺少File Explore视窗的方法
- 部署 SCCM 2012R2之三:系统准备篇
- MySQL:缓存算什么东西?!
- 一次性口令设计代码_品牌上新||预算百元,就能买到百搭又有设计感的首饰...
- CodeForces - 1450E Capitalism(差分约束)
- Lync Server 2013无法共享PPT故障排错处理
- Hough transform(霍夫变换)
- oracle各种查询语句
- 反射工厂在数据访问层的应用
- c语言如何扩大字体,C语言图形汉字及放大显示程序
- Python和Ruby语言对比
- mi5splus android9,小米MIUI 9.5国际版稳定版开始推送:超30款机型将获更新
- 关于工程总承包(EPC)项目最高限价的说明
- html页面大于号,css中大于号()是什么意思?
- 解决Loadrunner打开网页证书错误问题
- 渗透测试详解及爱加密加固使用及优势
- 多重背包二进制优化(wzk吃小鸡腿)
- Elasticsearch与最新的log4j2零日漏洞
- Thinkpad X230 升级 AX210 wifi6 无线网卡
- Android背景设置透明和半透明效果