shiro安全控制目录
shiro有状态认证是利用session保存登录状态的授权认证方式。但是当前后端分离,web服务无状态化之后,那么JWT身份认证便是趋势。
1. JWT简介
1.1 什么叫做JWT
JWT(json web token)是为了在网络应用环境之间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便从资源服务器获取资源。比如用于登录。
shiro(9)-有状态身份认证和无状态身份认证的区别
1.2 JWT的构成
JWT由三部分组成:头部(header)、载荷(payload)、签名(signature)。头部定义类型和加密方式;载荷部分放的不是很重要的数据;签名使用定义的加密方式加密base64后的header和payload和一段自己加密key。最后的token由base64(header).base64(payload).base64(signature)组成。
JWT生成Token后是这个样子的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmciOiLku4rml6XlpLTmnaEiLCJuYW1lIjoiRnJlZeeggeWGnCIsImV4cCI6MTUxNDM1NjEwMywiaWF0IjoxNTE0MzU2MDQzLCJhZ2UiOiIyOCJ9.49UF72vSkj-sA4aHHiYN5eoZ9Nb4w5Vb45PsLF7x_NY
1.2.1. header
JWT头部分是一个描述JWT元数据的JSON对象。
- 声明类型,这里是jwt。
- 声明加密的算法,通常直接使用HMAC SHA256。
完整的头部就像下面这样的json。
{"typ": "JWT","alg": "HS256"}
然后将头部进行base64加密,构成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
1.2.2. payload
载荷是存放有效信息的地方,这些有效部分包含三个部分。
- 标准中注册的声明;
- 公共的声明;
- 私有的声明;
标准中注册的声明:
类型 | 作用 |
---|---|
iss | 发行人 |
sub | 主题 |
aud | 接收Token的用户 |
exp | Token过期时间 |
iat | Token签发时间 |
nbf | 在该时间之前Token不可用 |
jti | Token的唯一标识 |
公共的声明:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感的信息,因为这部分在客户端可解密。
私有的声明:
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{"name":"Free码农","age":"28","org":"今日头条"}
然后将其进行base64加密,得到第二部分
eyJvcmciOiLku4rml6XlpLTmnaEiLCJuYW1lIjoiRnJlZeeggeWGnCIsImV4cCI6MTUxNDM1NjEwMywiaWF0IjoxNTE0MzU2MDQzLCJhZ2UiOiIyOCJ9
1.2.3. signuature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header(base64后的)
- payload(base64后的)
- secret(密钥)
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,就构成了jwt的第三部分:
49UF72vSkj-sA4aHHiYN5eoZ9Nb4w5Vb45PsLF7x_NY
注:密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,所以要保护好。
1.3 JWT优点
- 因为json的通用性,所以JWT是可以跨语言支持的。
- payload部分,JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
- 便于传输,JWT构成简单,字节占用很小,所以它是非常便于传输的,它不需要在服务端保存会话信息,所以利于应用的扩展。
2. JAVA代码生成JWT
1.maven依赖
io.jsonwebtoken
jjwt
0.9.0
2. 生成jwt
public class JwtUtils {
private static final String secret = "66diangezanbageixioapang12oc";
/**
* (默认的超时时间)token存活时间,2小时
*/
private static final long liveMills = 3600 * 2 * 1000;
/**
* 获取secret
*/
public static SecretKey obtainKey() {
//对key进行解码
byte[] secretBytes = secret.getBytes();
return new SecretKeySpec(secretBytes, 0, secretBytes.length, "AES");
}
/**
* 获取token,使用默认的超时时间,即2个小时。
*
* @param sub 主题(需要加密的字符串)
* @return token字符串
*/
public static String createJWT(String sub) {
return createJWT(sub, liveMills);
}
/**
* 会自动对主题对象序列化(使用fastJson进行序列化)得到字符串
*
* @param subObj 主题对象
* @return token字符串
*/
public static String createJWT(T subObj) {
return createJWT(JSONObject.toJSONString(subObj), liveMills);
}
/**
* 会自动对主题对象序列化(使用fastJson进行序列化)得到字符串
*
* @param subObj 主题对象
* @param liveMills 失效时间,单位毫秒
* @return token字符串
*/
public static String createJWT(T subObj, Long liveMills) {
return createJWT(JSONObject.toJSONString(subObj), liveMills);
}
/**
* @param sub 需要加密的主题
* @param liveMills 失效时间,单位毫秒
* @return 经过jwt加密的token字符串,失效时间即当前时间+liveMills毫秒数
*/
public static String createJWT(String sub, Long liveMills) {
//加密模式
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long currentTimeMillis = System.currentTimeMillis();
Date now = new Date(currentTimeMillis); //iat token签发时间
SecretKey secretKey = obtainKey();
//jti表示该token的唯一id,不推荐使用相同值|isa 下发时间
JwtBuilder jwtBuilder = Jwts.builder().setId("jti-xp").
setIssuedAt(now).
setSubject(sub).
signWith(signatureAlgorithm, secretKey);
if (liveMills > 0) {
long expMills = currentTimeMillis + liveMills;
Date expDate = new Date(expMills); //失效时间
jwtBuilder.setExpiration(expDate);
}
return jwtBuilder.compact();
}
/**
* 解密token,返回Claims对象
*
* 注,若token失效,会抛出{@link ExpiredJwtException}的异常
**/
public static Claims parseJWT(String token) {
SecretKey key = obtainKey();
return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody(); // Claims [kleɪmz] 声明
}
/**
* 解析token获取到sub(主题)。
*
* 注,若token失效,会抛出{@link ExpiredJwtException}的异常
*/
public static String parseJWT2Sub(String token) {
SecretKey key = obtainKey();
Claims body = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
return body.getSubject();
}
/**
* 解密token获取sub,并反序列化为对象。
*
* @param token 需要解密的token字符串
* @param clazz sub反序列化的对象类型
*/
public static T parseJWT2Sub(String token, Class clazz) {
SecretKey key = obtainKey();
Claims body = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
return JSON.parseObject(body.getSubject(), clazz);
}
public static void main(String[] args) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", "xiaopang");
jsonObject.put("host", "127.0.0.1");
String token = JwtUtils.createJWT(jsonObject.toJSONString());
System.out.println(token);
//解析token
Claims claims = parseJWT(token);
String subject = claims.getSubject();
System.out.println(subject);
//base64 payload解析
String payload="eyJqdGkiOiJqdGkteHAiLCJpYXQiOjE1NjM5NDcxMTAsInN1YiI6IntcImhvc3RcIjpcIjEyNy4wLjAuMVwiLFwidXNlcm5hbWVcIjpcInhpYW9wYW5nXCJ9IiwiZXhwIjoxNTYzOTU0MzEwfQ";
//org.apache.shiro.codec.Base64
System.out.println("payload的base64解密:"+new String(Base64.decode(payload)));
//base64 header解析
String header="eyJhbGciOiJIUzI1NiJ9";
System.out.println("header的base64解密:"+new String(Base64.decode(header)));
}
}
解析结果
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJqdGkteHAiLCJpYXQiOjE1NjM5NDc2OTQsInN1YiI6IntcImhvc3RcIjpcIjEyNy4wLjAuMVwiLFwidXNlcm5hbWVcIjpcInhpYW9wYW5nXCJ9IiwiZXhwIjoxNTYzOTU0ODk0fQ.QibTsAVmwHnnl5D-o6HuvPRhJPgTmgxivSZj36lyEnU
{"host":"127.0.0.1","username":"xiaopang"}
payload的base64解密:{"jti":"jti-xp","iat":1563947110,"sub":"{\"host\":\"127.0.0.1\",\"username\":\"xiaopang\"}","exp":1563954310
header的base64解密:{"alg":"HS256"}
附录:
1. 重放攻击
重放攻击是攻击者获取客户端发送给服务器端的包,不做修改,原封不动的发送给服务器用来实现某些功能。比如说客户端发送给服务器端一个包的功能是查询某个信息,攻击者拦截到这个包,然后想要查询这个信息的时候,把这个包发送给服务器,服务器就会做相应的操作,返回查询的信息。
文章参考:
国服最强JWT生成Token做登录校验讲解,看完保证你学会!
JWT使用一些注意事项
token 的刷新问题?
jti(JWT ID)的作用
Java 对称加密几种算法分别实现