两种鉴权方式
session 认证
流程
缺点
- 服务器保存每个用户信息,随着用户增多,服务器开销增大
- 在不使用缓存等中间件情况下,不适用于分布式系统
- 不适用于不保存cookie的客户端或浏览器禁用了cookie的情况
- cookie被截获,用户很容易收到跨站请求伪造攻击
- 无法跨域,不适用于单点登录的情况
token 认证
流程
优点
- 节约服务器资源
- 对移动端和分布式更加友好
- 支持跨域访问
- 无状态,减轻服务端压力
- 无需考虑CSRF(跨站请求伪造)
JWT 认证
JWT是token的一种具体实现方式,其全称是JSON Web Token,官网地址:https://jwt.io/ 。
通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。
JWT的认证流程如下:
JWT 的结构
JWT包含3部分:标头(Header)、有效载荷(Payload)和签名(Signature)。这三部分编码后通过.(英文的点)连接成为最后的JWT。
Header
Header由两部分组成:token的类型,也就是JWT,和使用的签名算法,如HMAC SHA256 或 RSA。Header通过Base64Url方式编码后成为JWT的第一部分。
{
"alg": "HS256",
"typ": "JWT"
}
Payload
Payload为声明字段等一些需要传递的信息。声明字段可以是关于一个实体(如用户)和附加信息。声明有三种类型:注册声明、公共声明和私有声明。Payload通过Base64Url方式编码后成为JWT的第二部分。
- 注册声明
注册声明不是强制的,但十分推荐在Payload里含有该声明。注册的声明包含iss (issuer), exp (expiration),sub (subject), aud (audience), nbf(not before),iat(issued at),jti(JWT id),全部是IANA 的 “JSON Web Token Claims” 中声明。
- 公共声明
公共声明是使用JWT的时候均可以使用的声明,但是为了避免声明题目的冲突,需要先在IANA 的 “JSON Web Token Claims” 中声明或在一个包含声明冲突的URI。
- 私有声明
在payload中使用的声明较多的是注册声明,同时也可以向payload中加入自己的私有声明以便传递更多的信息。
Payload示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意!
JWT中的Payload的部分使用Base64Url编码,任何人都可以读取其内容,因此不要包含隐私信息字段(如用户名、密码)。JWT只用于在网络中传输一些非敏感的信息。
Signature
签名部分通过header和payload编码后的内容和密钥经过header中指定的算法加密而成。例如在header中指定了HMAC SHA256算法,则签名通过下面的方式生成。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用来验证信息在传输的过程中没有发生改变,同时,在签名使用私钥的情况下,签名还可以用来验证信息的发送者。
在Java中使用JWT
在JWT的官网推荐java-jwt、jose4j、nimbus-jose-jwt、jjwt-root、fusionauth-jwt、vertx-auth-jwt和inverno-security-jose,其中比较常用的是jwt和jjwt。
java-jwt
- 引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.1</version>
</dependency>
- 生成token
Map<String, Object> map = new HashMap<>();
map.put("typ", "jwt");
map.put("alg", "HS256");
String token = JWT.create()
.withHeader(map) // Header
.withClaim("sub", "80310620") // Payload
.withClaim("userName", "cfn")
.sign(Algorithm.HMAC256("secret")); // 签名secret
System.out.println(token);
- 解析token
// 创建解析对象,使用的算法和secret要与创建token时保持一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("secret")).build();
// 解析指定的token
DecodedJWT decodedJWT = jwtVerifier.verify(token);
// 获取解析后的token中的payload信息
Claim userId = decodedJWT.getClaim("sub");
Claim userName = decodedJWT.getClaim("userName");
jjwt-root
- 引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.11.5</version>
</dependency>
- 生成token
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("baobao-user")
.claim("id", "80310620")
.claim("nickname", "cfn")
.signWith(SignatureAlgorithm.HS256, "xxx")
.compact();
- 解析token
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor("xxx".getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(jwtToken);
JWT 鉴权实例
下面以CMBT的鉴权为例演示JWT的用户认证。CMBT为前后端分离,所有前端的请求携带token通过gateway模块的认证后分发到不同的模块。用户第一次登录的时候通过一号通认证,认证完成后获得用户的信息,在gateway模块生成用户信息所对应的token,并将用户信息和token保存到redis。后续用户请求经过gateway时,gateway根据token解析出用户信息,如果redis中的键值仍然有效,则直接转发请求,如果无效则回到一号通重新进行认证。
该JWT使用的是jjwt-root,根据用户信息生成token的代码如下:
public static String generateToken(String rtcId, String name) {
Map<String, Object> claims = new HashMap();
claims.put("sub", rtcId);
claims.put("name", name);
claims.put("expiredDate", DateUtils.getOffsetDayMidNight(7));
return generateToken(claims);
}
private static String generateToken(Map<String, Object> claims) {
return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, "xxx").compact();
}
从token解析出用户信息的代码如下:
public static Claims getClaimsFromToken(String token) {
try {
Claims claims = (Claims)Jwts.parser().setSigningKey("xxx").parseClaimsJws(token).getBody();
if (claims != null && claims.containsKey("expiredDate") && claims.containsKey("sub")) {
return claims;
} else {
log.info("Token包含信息不完整");
return null;
}
} catch (Exception var2) {
log.info("Token解析异常");
return null;
}
}a