引言
在前面的文章Web应用的认证机制中,已经对token认证的流程有了初步认识。由于token认证天生的无状态性,使得它非常适合应用在前后端分离的应用中。它采用加密算法与秘钥对身份信息进行签名,生成一个字符串,解析的时候也是拿到字符串再用解密算法与秘钥逆向还原身份信息,若能正确还原则通过认证。因此,它相对于传统的session认证而言,是采用了时间换空间的策略弥补了跨域情景下的无状态性。
之前都只停留在理论上,本章将介绍如何利用JWT(JSON Web Token)标准去真正实现token认证。
JWT标准
JWT是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准RFC 7519。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT组成
JWT标准的token由三个部分组成:
- Header
- Payload
- Signature
对这三部分分别进行Base64编码即可得到最终的token字符串。详情如下所述。
1.Header
JWT的头部描述了一些基本信息,如类型、签名算法:
|
|
{ "typ": "JWT", "alg": "HS256" } |
上面JSON对象的alg字段指明了采用HS256签名算法。对该JSON对象进行Base64编码即可得到字符串:
|
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 |
2.Payload
载荷里面是token最实质的具体内容,其中部分为标准字段,也可自定义字段满足需求:
|
|
{ "iss": "JWT Demo", //签发者 "iat": 1416797419, //发行时间戳 "exp": 1448333419, //过期时间戳 "aud": "www.yaochenkun.cn", //认证站点 "sub": "yaochenkun@gmail.com", //用户信息 "Email": "yaochenkun@gmail.com", //邮箱 "Role": [ "Manager"] //角色信息 } |
同理进行Base64编码即可得到如下字符串:
|
|
eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ |
3.Signature
这部分其实就是对上面得到的Header与Payload进行:
- 拼接组装
- Base64编码
- 加密签名(结合秘钥串)
- 生成签名值
最后就会得到如下的字符串:
-
|
|
SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc |
4.生成token
最后对Header、Payload、Signature进行字符串拼接(以句点.连接)即可获得token字符串:
|
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ. SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc |
该token生成的时机是在用户登录系统并验证通过后,生成并下发给前端的,前端一旦拿到该token串即可在之后每次要访问后端api服务接口的时候,在异步请求的HTTP头部携带上该token串,如下面beforeSend字段的回调函数所示:
|
|
$.ajax( { url : SERVER_ADDRESS + '/api/business', type : 'POST', contentType: 'application/json', data : JSON.stringify({userName : 'ken'}), dataType : 'json', beforeSend: (request) => request.setRequestHeader(SESSION.TOKEN, sessionStorage.getItem(SESSION.TOKEN)), success : (result) => {...} }); |
后端拿到头部中的token后进行解密验证,若token合法则放行。
JWT实现
本节记录如何使用JJWT(开源库)在Java后端程序中实现。我将token的生成和认证都整合到了一个叫做TokenUtil的类中,其中的静态方法createToken()负责生成,parseToken()负责解析。
不过在这之前我封装了一个身份信息类Identity,因为我们后续业务中往往涉及角色权限拦截,或是使用到用户名之类的,经常会用到登录用户的必要信息,所以为方便维护构建了这个类,如下所示:
|
|
class Identity { private String token; //token序列 private String id; // 对应user的id private String issuer; //签发者 private String role; // 角色 private String userName; //用户名 private Long duration; // 有效时长,单位毫秒 } |
1.Token生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public static String createToken(Identity identity, String apiKeySecret) { //JWT采用的签名算法 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //获取当前时间戳 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //封装好加密算法与私钥apiKeySecret byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(apiKeySecret); Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); //采用建造者模式定制化token属性 JwtBuilder builder = Jwts.builder().setId(identity.getId()) .setIssuedAt(now) .setSubject(identity.getId() + "," + identity.getUserName() + "," + identity.getRole()) .setIssuer(identity.getIssuer()) .signWith(signatureAlgorithm, signingKey); //设置失效时间戳 long ttlMillis = identity.getDuration(); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); identity.setDuration(exp.getTime()); } //生成token并序列化编码成一个URL安全的字符串 logger.info("token生成成功"); return builder.compact(); } |
何时使用createToken下发token呢,一般就是在登录成功后下发,比如这样:
|
|
@RequestMapping(value = "login", method = RequestMethod.POST) @ResponseBody public CommonResult login(@RequestBody Map<String, String> params) { //1.验证用户名与密码... //2.通过后从数据库中读取该合法user... //3.生成token Identity identity = new Identity(); identity.setId(user.getId()); identity.setIssuer(issuer); identity.setDuration(duration); identity.setPhone(user.getUserName()); identity.setRole(user.getRole()); String token = TokenUtil.createToken(identity, apiKeySecret); //4.返回并下发token } |
2.Token认证
对于需要权限认证的业务接口可以为他们统一设置权限认证拦截器,在进入业务接口之前会先进入该拦截器进行token解析,若token合法则放行。Token的解析细节如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
public static Identity parseToken(String token, String apiKeySecret) throws Exception { Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(apiKeySecret)) .parseClaimsJws(token).getBody(); String[] subjectInfos = claims.getSubject().split(","); String id = subjectInfos[0]; String userName = subjectInfos[1]; String role = subjectInfos[2]; // 封装成pojo Identity identity = new Identity(); identity.setId(id); identity.setPhone(userName); identity.setRole(role); identity.setDuration(claims.getExpiration().getTime()); logger.info("已登录的用户,有效token"); return identity; } |
拦截器中进行认证的过程如下所示:
|
|
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 验证token的有效性 try { String token = request.getHeader(Constant.TOKEN); Identity identity = TokenUtil.parseToken(token, propertyService.apiKeySecret); //把identity存入session中 request.getSession().setAttribute(Constant.IDENTITY, identity); logger.info("用户:{}token有效", identity.getUserName()); return true; } catch (Exception e) { logger.info("token无效,转到登录界面"); response.sendRedirect("/api/auth/login_denied"); return false; } } |
从上面进行token解析的过程不难发现,我们后端完全不用存储该用户之前登录的任何状态信息,没有用session也没有用到全局状态表,仅仅是解析前端发送过来的token串,从而得到identity身份认证信息的。因此token认证在跨域访问时造成session失效的问题也迎刃而解了,因为它压根就不用session…
结语
Token认证在前后端分离架构、单点登录系统、分布式系统等方面颇具优势,使得它的应用也越加广泛,目前正被很多大型公司的网站使用,比如Facebook、Google+、Twitter、Github等。而JWT只是token认证的其中一个标准,我们也可以采用其他标准实现,只是JWT标准的受众更多更为主流。
本章到此结束,如果需要完整代码可以到我的GitHub中的SSM-jwt仓库下载。
作者:阿堃堃堃堃
链接:https://www.jianshu.com/p/dad76f5caaa5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
JWT优点与缺点及对应的解决方案考虑JWT的实现,上面所述的关于session,cookies的缺点都不复存在了,不易被攻击者利用,安全性提高。利用Authorization首部传输token,无跨域问题。额外信息存储在客户端,服务端占用资源不多,也不存在session共享问题。感觉JWT优势很明显,但其仍然有一些缺点:
登录状态信息续签问题。比如设置token的有效期为一个小时,那么一个小时后,如果用户仍然在这个web应用上,这个时候当然不能指望用户再登录一次。目前可用的解决办法是在每次用户发出请求都返回一个新的token,前端再用这个新的token来替代旧的,这样每一次请求都会刷新token的有效期。但是这样,需要频繁的生成token。另外一种方案是判断还有多久这个token会过期,在token快要过期时,返回一个新的token。
用户主动注销,后台无法让用户强制下线。JWT并不支持用户主动退出登录,当然,可以在客户端删除这个token,但在别处使用的token仍然可以正常访问。为了支持注销和后台主动强制让用户下线, 解决方案是在注销时将该token加入黑名单。当用户发出请求后,如果该token在黑名单中,则阻止用户的后续操作,返回Invalid token错误。给名单的维护可以使用redis,token的过期时间和redis中数据的存活时间保持一致。
————————————————
版权声明:本文为CSDN博主「码农的世界,你不懂」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010395024/article/details/103694011
「三年博客,如果觉得我的文章对您有用,请帮助本站成长」
共有 0 - Token认证之JWT