在文章之前,我们先介绍几个概念
OAuth2、JWT,Spring Security、Spring Security OAuth2
OAuth2:Open Authorization,是一种授权协议,是规范,不是技术实现。
JWT:JSON Web Token,是一种具体的Token实现框架。
Spring Security:前身是 Acegi Security ,能够为 Spring企业应用系统提供声明式的安全访问控制。该框架老古董了。
Spring Security OAuth2:Spring 对 OAuth2 开源实现(与Spring Cloud技术栈无缝集成)。
目前用的最多是JWT,因此本文也是围绕JWT来实现。
JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户。当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。
JWT TOKEN分为三部分:header.payload.signature,因此JWT通常如下表示xxx.yyyyy.zz
头部包含了两部分,token 类型和采用的加密算法。alg字段指定了生成signature的算法,默认值为HS256,typ默认值为JWT。
如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程
{
"alg": "HS256",
"typ": "JWT"
}
这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分。
{
"iss": "admin", //该JWT的签发者
"iat": 1535967430, //签发时间
"exp": 1535974630, //过期时间
"nbf": 1535967430, //该时间之前不接收处理该Token
"sub": "www.admin.com", //面向的用户
"jti": "9f10e796726e332cec401c569969e13e" //该Token唯一标识
}
String secret = "123456";//秘钥
HMACSHA256( base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。
三个部分通过.
连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ
.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s
其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。
因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。
流程参考如下:资料引用于https://www.cnblogs.com/wenqiangit/p/9592132.html
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Description: Token工具类
* @author hutao
* @mail [email protected]
* @date 2020年1月11日
*/
public class TokenUtil {
/**
* 签名秘钥
*/
public static final String SECRET = "123456";
/**
* 签发地
*/
public static final String ISSUER = "hutao.com";
/**
* 过期时间
*/
public static final long TTLMILLIS = 3600*1000*60;
/**
* @Description: 生成Token令牌
* @author hutao
* @mail [email protected]
* @date 2020年1月11日
* @param claims 私有声明
* @param id 编号
* @param issuer 该JWT的签发者,是否使用是可选的
* @param subject 该JWT所面向的用户,是否使用是可选的;
* @param ttlMillis 有效时间
* @return token String
*/
public static String generateJwtToken(Map
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
/* Map
claims.put("aaaa", "aaaa");
claims.put("bbbb", "bbbb");
claims.put("cccc", "cccc");*/
// 通过秘钥签名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// 让我们设置JWT声明
JwtBuilder builder = Jwts.builder();
if(claims!=null) {
builder.setClaims(claims);//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
}
builder.setId(id);//jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
builder.setIssuedAt(now);iat: jwt的签发时间
builder.setSubject(subject);//sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
builder.setIssuer(issuer);//iss(issuer):签发地
builder.signWith(signatureAlgorithm, signingKey); //设置签名使用的签名算法和签名使用的秘钥
// 如果已指定,则添加到期时间
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
}
// 构建JWT并将其序列化为一个紧凑的url安全字符串
return builder.compact();
}
/**
* @Description: 解析Token
* @author hutao
* @mail [email protected]
* @date 2020年1月11日
*/
public static Claims parseJWT(String jwt) {
// 如果这行代码不是签名的JWS(如预期),那么它将抛出异常
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
.parseClaimsJws(jwt).getBody();
return claims;
}
public static void main(String[] args) {
String token = TokenUtil.generateJwtToken(null,"100",ISSUER,"hutao",TTLMILLIS);
System.out.println(token);
Claims claims = TokenUtil.parseJWT(token);
System.out.println(claims);
}
}