1) Token
定义
token 即使是在计算机领域中也有不同的定义,这里我们说的token,是指访问资源的凭据。
-
注册你的应用程序,注册完毕 会拿到认证信息(credentials)包括 ID 和 secret。 -
请求 access token, 通过代码取得 access token 参数直接放到header里
-
token 获取到之后,就能够带上 token 访问 API 对应资源
-
服务端需要对token,签名,时间戳ts 进行验证(防止恶意无限制访问),只有token有效,时间戳未超时,签名有效才能被放行
-
注意: access token 过期, 可以通过 refresh token 再次请求 access token。
token示例
{
"success": true,
"data": {
"access_token": "93cdfdc3260996cf01508fb571e826",
"expires_in": 1200000,
"refresh_token": "93cdfdc3260996cf01508b571e826",
"security_key": "K7ODHcfFAfzKhihGNLNljOq9aunGoE\u003d",
"ts": 1650786844444
},
"code": "200"
}
应用场景
Open API即开放API,也称[开放平台]
JWT 与 Token 的区别
相同: 都是访问资源的令牌 都可以记录用户的信息 都是使服务端无状态变化 都是验证成功后,客户端才能访问服务端上受保护的资源 区别
Token: 服务端验证客户端发送过来的Token 时,还需要查询数据库获取用户信息,然后验证Token 是否有效 JWT: 将token 和 Payload 加密后存储于客户端,服务端只需要使用秘钥进行校验即可,不需要查询或者减少查询数据库,因为JWT自包含了用户信息和加密的数据
2)Ticket
定义
Ticket 类似前面说的token,也是是指访问资源的凭据。 类似入 门票, 这种认证机制不关系持票人,重点关系的是门票是否合法!
用户登录成功后,会获取一个ticket值,接下去任何接口的访问都需要这个参数。 把它放置在ticket内,有效期为10分钟,在ticket即将超时,无感知续命。延长使用时间,如果用户在一段时间内没进行任何操作,就需要重新登录系统。
应用场景
登录认证
Sign签名 (防止参数恶意篡改)
把所有的参数拼接一起,再加入系统密钥,进行MD5计算生成一个sign签名,防止参数被人恶意篡改,后台按同样的方法生成签名,进行签名对比
/**
* @param request
* @return
*/
public static Boolean checkSign(HttpServletRequest request,String sign){
Boolean flag= false;
//检查sigin是否过期
Enumeration<?> pNames = request.getParameterNames();
Map<String, String> params = new HashMap<String, String>();
while (pNames.hasMoreElements()) {
String pName = (String) pNames.nextElement();
if("sign".equals(pName)) continue;
String pValue = (String)request.getParameter(pName);
params.put(pName, pValue);
}
System.out.println("现在的sign-->>" + sign);
System.out.println("验证的sign-->>" + getSign(params,secretKeyOfWxh));
if(sign.equals(getSign(params, secretKeyOfWxh))){
flag = true;
}
return flag;
}
重复访问 (防止恶意请求)
引入一个时间戳参数Ts,保证Ticket仅在2分钟内有效,需要和客户端时间保持一致。
需要跟当前服务器时间 new Date 进行对比,如果超过2分钟,就拒绝本次请求,节省服务器查询数据的消耗
拦截器 (过滤非法请求)
每次请求都带有这三个参数,我们都需要进行验证,只有在三个参数都满足我们的要求,才允许数据返回或被操作。
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws IOException {
JSONObject jsonObject = new JSONObject();
String ticket = request.getParameter("ticket");
String sign = request.getParameter("sign");
String ts = request.getParameter("ts");
if (StringUtils.isEmpty(ticket) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts)){
jsonObject.put("success",false);
jsonObject.put("message","args is isEmpty");
jsonObject.put("code","1001");
PrintWriter printWriter = response.getWriter();
printWriter.write(jsonObject.toJSONString());
return false;
}
//如果redis存在ticket就认为是合法的请求
if (redisTemplate.hasKey(ticket)){
String values = (String) redisTemplate.opsForValue().get(ticket);
//判断ticket是否即将过期,进行续命操作
if (redisTemplate.opsForValue().getOperations().getExpire(ticket) != -2 && redisTemplate.opsForValue().getOperations().getExpire(ticket) < 20){
redisTemplate.opsForValue().set(ticket,values,10L, TimeUnit.MINUTES);
}
//判断是否重复访问,存在重放攻击的时间窗口期
if (SignUtils.getTimestamp() - Long.valueOf(ts) > 1200){
jsonObject.put("success",false);
jsonObject.put("message","Overtime to connect to server");
jsonObject.put("code","1002");
PrintWriter printWriter = response.getWriter();
printWriter.write(jsonObject.toJSONString());
return false;
}
//验证签名
if (!SignUtils.checkSign(request,sign)){
jsonObject.put("success",false);
jsonObject.put("message","sign is invalid");
jsonObject.put("code","1003");
PrintWriter printWriter = response.getWriter();
printWriter.write(jsonObject.toJSONString());
return false;
}
return true;
}else {
jsonObject.put("success",false);
jsonObject.put("message","ticket is invalid,Relogin.");
jsonObject.put("code","1004");
PrintWriter printWriter = response.getWriter();
printWriter.write(jsonObject.toJSONString());
}
return false;
}
}
示例
/**
*
* Description:验证登录,ticket成功后放置缓存中,
* @param
* @author huangweicheng
* @date 2020/12/31
*/
public JSONObject login(String username,String password){
JSONObject result = new JSONObject();
PersonEntity personEntity = personDao.findByLoginName(username);
if (personEntity == null || (personEntity != null && !personEntity.getPassword().equals(password))){
result.put("success",false);
result.put("ticket","");
result.put("code","999");
result.put("message","用户名和密码不匹配");
return result;
}
if (personEntity.getLoginName().equals(username) && personEntity.getPassword().equals(password)){
String ticket = UUID.randomUUID().toString();
ticket = ticket.replace("-","");
redisTemplate.opsForValue().set(ticket,personEntity.getLoginName(),10L, TimeUnit.MINUTES);
result.put("success",true);
result.put("ticket",ticket);
result.put("code",200);
result.put("message","登录成功");
return result;
}
result.put("success",false);
result.put("ticket","");
result.put("code","1000");
result.put("message","未知异常,请重试");
return result;
}
3) JWT
定义
JWT Java library
维基百科的定义,JSON WEB Token(JWT,读作 [/dʒɒt/]),是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。
头信息指定了该JWT使用的签名算法:
header = '{"alg":"HS256","typ":"JWT"}'
HS256 表示使用了 HMAC-SHA256 来生成签名。
消息体包含了JWT的意图:
payload = '{"loggedInAs":"admin","iat":1422779638}'//iat表示令牌生成的时间
未签名的令牌由base64url编码的头信息和消息体拼接而成(使用"."分隔),签名则通过私有的key计算而成:
key = 'secretkey'
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)
signature = HMAC-SHA256(key, unsignedToken)
最后在未签名的令牌尾部拼接上base64url编码的签名(同样使用"."分隔)就是JWT了:
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
# token看起来像这样: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI
JWT常常被用作保护服务端的资源(resource),客户端通常将JWT通过HTTP的Authorization header发送给服务端,服务端使用自己保存的key计算、验证签名以判断该JWT是否可信:
应用场景
防止:CSRF 攻击 跨站请求伪造Cross-site request forgery(简称CSRF, 读作 [sea-surf])是一种典型的利用cookie-session漏洞的攻击
“开票系统”
“签名系统”
JWT 规范确定了 12 种标准签名算法——3 种密钥算法和 9 种非对称密钥算法——由以下名称标识:
HS256: 使用 SHA-256 的 HMACHS384: 使用 SHA-384 的 HMACHS512: 使用 SHA-512 的 HMACES256: ECDSA 使用 P-256 和 SHA-256ES384: ECDSA 使用 P-384 和 SHA-384ES512: ECDSA 使用 P-521 和 SHA-512RS256:使用 SHA-256 的 RSASSA-PKCS-v1_5RS384: RSASSA-PKCS-v1_5 使用 SHA-384RS512: RSASSA-PKCS-v1_5 使用 SHA-512PS256: RSASSA-PSS 使用 SHA-256 和 MGF1 和 SHA-256PS384: 使用 SHA-384 的 RSASSA-PSS 和使用 SHA-384 的 MGF1PS512: RSASSA-PSS 使用 SHA-512 和 MGF1 和 SHA-512
JWT 标准注册声明 方法
为 JWT 规范中定义的JwtBuilder标准注册声明名称提供了方便的 setter 方法。他们是:
setIssuer:设置iss(发行人)声明setSubject:设置sub(主题)声明setAudience:设置aud(观众)声明setExpiration:设置exp(过期时间)声明setNotBefore: 设置nbf(Not Before) 声明setIssuedAt:设置iat(颁发于)声明setId:设置jti(JWT ID)声明
JWT 自定义声明 方法
claim根据需要简单地调用一次或多次:
String jws = Jwts.builder()
.claim("hello", "world")
创建 JWS 签名
您可以按如下方式创建 JWS:
- 使用该
Jwts.builder()方法创建一个JwtBuilder实例。 - 调用
JwtBuilder方法以根据需要添加标头参数和声明。 - 指定要用于签署 JWT的
SecretKey或非对称。PrivateKey - 最后,调用该
compact()方法进行压缩和签名,生成最终的 jws。
例如:
String jws = Jwts.builder() // (1)
.setSubject("Bob") // (2)
.signWith(key) // (3)
.compact(); // (4)
// Create a legitimate RSA public and private key pair:
KeyPair kp = RsaProvider.generateKeyPair();
PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate();
String jwt = Jwts.builder()
.setSubject("Joe")
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
注意:不能使用PublicKeys 签署 JWT,因为这总是不安全的。 JJWT 将拒绝任何指定 PublicKey用于使用InvalidKeyException.
验证签名: 解析签名
- 使用该
Jwts.parserBuilder()方法创建一个JwtParserBuilder实例。 - 指定要用于验证 JWS 签名的
SecretKey或非对称的。PublicKey - 调用 上的
build()方法JwtParserBuilder以返回线程安全的JwtParser。 - 最后,
parseClaimsJws(String)使用您的 jws 调用该方法String,生成原始 JWS。 - 整个调用被包装在一个 try/catch 块中,以防解析或签名验证失败。稍后我们将介绍异常和失败的原因。
- 允许服务器时差 setAllowedClockSkewSeconds
Jws<Claims> jws;
try {
jws = Jwts.parserBuilder() // (1)
.setSigningKey(key) // (2)
.build() // (3)
.parseClaimsJws(jwsString); // (4)
// we can safely trust the JWT
catch (JwtException ex) { // (5)
// we *cannot* use the JWT as intended by its creator
}
SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();
Jwts.parserBuilder()
.setSigningKeyResolver(signingKeyResolver) // <----
.setAllowedClockSkewSeconds(seconds) // <----
.build()
.parseClaimsJws(jwsString);
使用RS256和Jwts.builder()生成令牌.signWith()生成签名 Jwts.parser()解析签名
使用并使用RS256算法产生一个令牌
@Component
@ConfigurationProperties("jwt.config")
@Data
public class JWTUtils {
private String key;//签名私钥
private Long ttl;//签名失效时间
/* private String key="zcc-ynhrm";
private int ttl=3600000;*/
/**
* 设置认证token
* 参数:
* id:登陆用户id
* subject(主题):登陆用户名
*/
public String createJWT(String id, String subject, Map<String,Object> map){
//1.设置失效时间
long now=System.currentTimeMillis();//当前毫秒数
long exp=now+ttl;
//2.创建jwtBuilder
JwtBuilder jwtBuilder = Jwts.builder().setId(id).setSubject(subject)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, key);
//3.通过map设置claims,指定失效时间
for (Map.Entry<String,Object> entry:map.entrySet()){
jwtBuilder.claim(entry.getKey(),entry.getValue());
}
//jwtBuilder.setClaims(map);
jwtBuilder.setExpiration(new Date(exp));
//4.创建token
String token = jwtBuilder.compact();
return token;
}
/**
* 解析token字符串获取clamis
*/
public Claims parseJWT(String token){
Claims claims =
Jwts.parser().setSigningKey(key)
.parseClaimsJws(token)
.getBody();
return claims;
}
}