1.token的原理和作用
token是用于验证身份的,在web系统中验证前端是否能够访问后端系统
token在服务端产生,如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 token 证明自己的合法地位。token是服务端生成的一串字符串,以作前端进行请求的一个令牌,当第一次登录后,服务器生成一个token并返回给前端,之后前端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
token相比其他验证方式,主要是无需每次都去验证用户名密码,同时由于token保存在前端,服务端无需保存每个用户的登录状态,只需要验证访问时带着的token是不是自己签发的,可以减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
2.token可以解决的问题
token的作用是验证身份,那token可以解决那些问题呢?
1)避开同源策略
web页面中,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源,这样也给不同系统的交互验证带来了较大的问题,但是token完全由应用管理,不涉及服务端,只要使用同样的密钥与算法,token就是有效的。
2)避免CSRF攻击
使用cookie保存用户信息,可能会造成黑客恶意伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。但是token放入header之后,是无法被直接伪造的。
3)token可以在多个应用直接共享
在多个系统协同工作的时候,token可以被共享到多个服务中,避免用户多次登录
3.token的应用
我们应用token在我们的系统中,一般会使用jwt来进行
jwt(Json Web Token)是实现token技术的一种解决方案,可以使用各种语言实现,java自然也在其中。
jwt的字符串有三部分组成,头部(header),载荷(playload),签证(signature)。头部用于声明类型和加密算法,载荷用于保存各种信息,token的有效信息就保存在此,但是因为base64是对称加密的,故载荷不能保存机密信息。签证部分是非常重要的,因为其中需要使用前两部分base64之后的字符串,进行加盐(secret)后再加密,生成一个字符串。这三部分字符串连接起来,就是完整的jwt字符串了。需要注意的是,载荷的信息越多,jwt字符串就会越长。
jwt应用中需要注意三点,载荷部分存放的信息是客户端可解密的部分,所以不能存放敏感信息,第二就是由于使用了对称算法,双方之间仅共享一个,密钥签证中的盐(secret)是属于私钥的,一旦泄露,非常麻烦,第三尽量使用https。
当然,jwt不止可以使用对称算法,也可以使用不对称算法,但是这样会造成不同系统之间配合更为复杂。
1)jwt的具体实现
我们一般在springboot中使用jwt,故先导入maven的依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
然后编写jwt的工具类
public class JwtUtils {
/**
* 签发JWT
*/
public static String generateToken(String id, String subject, long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
SecretKey TokenEncryptKey = encrypt();
JwtBuilder builder = Jwts.builder()
.setId(id)
.setSubject(subject) // 主题
.setIssuer("user") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, TokenEncryptKey); // 签名算法以及密匙
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate); // 过期时间
}
return builder.compact();
}
/**
* 验证JWT
* @param jwtStr
* @return
*/
public static CheckResult validateToken(String jwtStr) {
CheckResult checkResult = new CheckResult();
Claims claims = null;
try {
claims = decrypt(jwtStr);
checkResult.setSuccess(true);
checkResult.setClaims(claims);
} catch (ExpiredJwtException e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_EXPIRE);
checkResult.setSuccess(false);
} catch (SignatureException e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
}
public static SecretKey encrypt() {
byte[] encodedKey = Base64.decode(SystemConstant.JWT_SECERT);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析JWT字符串
*/
public static Claims decrypt(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
工具类完成后就可以使用了,但是要注意,由于jwt应用与web页面中,可能经常会被URL编码解码,因为其中含有特殊字符,要依据流程进行url编解码,以防止错误的jwt进入工具类,造成异常。
2)工具类的应用实例
工具类编写好之后,要考虑如何在系统中使用。
一般来说,用户登录。输入用户名和密码后,验证通过了,就可以token发送到前端了,但是要考虑前端应该如何使用这个token,一般来说,有两种,一种是在http请求参数中放token,并在服务器端建立一个拦截器来验证这个 token,但这种请求仍然有可能被CSRF攻击,而且如果系统已经完成了大量的功能,还需要去修改请求方法以及后台接口。另一种就是给页面各个请求都统一配置header,在http请求头中放置token,这样对于请求本身没有影响,后台也只需要拦截器来验证这个 token就行了。
3)不同系统之间的配合
使用不同系统的配合时,考虑到用户统一登录,可以考虑如下方案
设置一个主要的登录系统,用户登录之后,通过该系统,把token分享给各个应用,分享的方法可以有很多种,比如通过请求参数直接发送给另一个应用。考虑到安全性问题,该token需要设置较短的失效时间,防止滥用。然后各个系统因为都具有登录系统一样的密钥,就可以进行验证,这个通过参数传来的token是不是可以使用,如果可以,就通过。由于登录系统的token设置的过期时间很短,各个应用可以自行设置token的策略,只是把登录系统的token作为第一次的登录token。
这个方法仅限于互相信任的系统,如果系统互相不信任,最好不要这样使用。
4.token的安全问题
1)过期时间问题
jwt在载荷中,提供了一些方法(就是标准中注册的声明),其中就有过期时间的设置。考虑安全性,token肯定不能一直有效,设置过期时间就成了必然。但是token有了过期时间,如果比较长,用户可以使用比较长的时间而不会掉出有效期重新登录,但是这样较长的时间,安全性又会打折扣,毕竟在token过期之前,服务都是可以一直访问的。但是设置过短的话,安全性得到了保证,用户却会在使用中频繁掉线。
为了解决这个问题,保存token状态到服务器肯定是不行的,比较token的初衷就是为了减轻服务器压力。使用就需要使用Refresh Token,就是服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。Refresh Token就相当于一个长效的token,用于请求真正的访问token。Refresh Token失效的时候,就可以让用户重新登录了。
但是Refresh Token这种方法并非完美,比较用户还是有了一个长效的token的,所以使用这种方法要注意,Refresh Token需要在服务端监控状态,用户手动登出的时候,需要注销掉用户使用的Refresh Token。同时,在进行敏感操作时,注意进行二次验证或者其他方式确保操作安全。
2)信任和不信任
我们前面说过,使用同一套密钥和算法搭建多系统协同时,需要互相信任的系统,就是比如说,同一家公司为某一个项目开发的多个系统,可以使用同一套密钥和算法。但是如果无法绝对信任的系统之间相互配合,就不能使用了,这时可能就要涉及多种业务直接的互相信任,授信,还可能需要用户主动授信,还有需要非对称算法,保证安全。这时可能简单使用token不是很合适,最好使用类似于OAuth认证的开放式 API 认证标准。
3)token本身被盗用
token本身并非不会被获取或滥用,所以,如果风险比较高,,还需要再token中加入一些不易伪造的信息,比如,ip,时间戳,客户端信息等。
5.总结
token本身来说是比较容易理解和使用的,实现方式简单,操作方便,能够快速实现。由于服务端不存储用户状态信息,因此大用户量,对后台服务也不会造成压力。但是并非所有系统都适合使用,其本身也存在一些漏洞和问题,还有其本身无状态的特征,也可能导致被恶意使用。
参考链接:
https://www.jianshu.com/p/fe67b4bb6f2c
https://www.jianshu.com/p/cba0dfe4ad4a
https://www.jianshu.com/p/576dbf44b2ae
https://www.jianshu.com/p/24825a2683e6
https://www.cnblogs.com/xuxinstyle/p/9675541.html
https://blog.csdn.net/weixin_44172434/article/details/99594870
https://www.cnblogs.com/lingyejun/p/9282169.html
https://blog.csdn.net/downloads_zip/article/details/79171368
https://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html
https://blog.csdn.net/xunfeng13/article/details/52371562
https://www.cnblogs.com/shanyou/p/5038794.html