听说微信搜索《Java鱼仔》会变更强哦!
本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看哦
(一)前言
在前面一篇讲分布式session的时候,有读者问了一句用JWT不香吗,连Redis服务器都省了,又可以实现分布式环境下的人员信息认证。正好我也还没写过关于JWT的文章,刚好在项目中又用过这项技术,今天就来分享一下。还是一样,先理论后实践,有问题评论一起头脑风暴。
(二)什么是JWT
JWT全程JSON Web Token,用户会话信息存储在客户端浏览器中,各方之间通过JSON对象安全传输信息,这些信息可以通过加密算法进行加密。
这样描述可能比较难懂,简单来讲,JWT就是在你登陆的时候生成一串加密字符串token,在每次请求的时候都带上这串token,服务器如果可以解密这段字符串,说明就是登陆状态的。
我们一般会将非敏感数据加密生成token,如果服务器端需要用到人员信息,就解密取人员信息。
你会发现,使用JWT直接就解决了分布式集群环境下的人员认证问题,因为生成的这串token在哪台服务器上都可以解析出来。如下图所示:
了解了原理之后,来讲讲实际的,JWT的数据包含三个部分:
1、Header 2、Payload 3、Signature
三者通过"."组合在一起,经过加密后生成一段token:
Header.Payload.Signature
下面是我生成的一段token
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYXZheXoiLCJjcmVhdGVkYXRlIjoxNjExNzU1MDIxNjk1LCJpZCI6MSwiZXhwIjoxNjEyMzU5ODIxLCJ1c2VyTGV2ZWxJZCI6bnVsbH0.ahFWQ_BJ1WNWp9GnlTrSNThVa3i3dydzcaNxLmPb7HI
Header用来描述JWT元数据,包含两个数据,其中alg表示签名的算法,默认HS256,typ属性表示令牌类型,这里就是JWT。上面生成的token中第一个“.”之前的数据base64解码后就是下面的json
{ alg : "HS256", typ : "JWT"}
Payload用来以JSON格式记录用户信息,这里的用户信息可以自定义,上面第一个“.”和第二个“.”之间的数据base64解码后就是下面的json
{ "sub":"javayz", "createdate":1611755021695, "id":1, "exp":1612359821, "userLevelId":null}
Signature存放加密串,通过指定算法对Header和Payload加盐加密,各部分通过“.”分割。组成了token。
(三)JWT身份认证流程
看完上面的内容,我相信你对JWT的认证已经有认识了,其身份认证的流程如下所示:
1、用户输入用户名和密码登陆
2、服务器端校验用户名和密码是否正确,如果正确就返回token给客户端
3、客户端将token存放在cookie或者local storage中
4、客户端后续的每次请求,都要带上这个token
5、服务器通过token判断是哪个用户
(四)代码实践
接下通过代码模拟JWT的认证实现,我写这段代码时的环境为springBoot2.4.2,引入JWT依赖:
<!--JWT依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>
在配置文件中配置一些接下来会用到的属性:
jwt: tokenHeader: Authorizationsecret: javayzexpiration: 604800tokenHead: Bearer
写个类来读取这些参数:
prefix="jwt") (publicclassJwtProperties { privateStringtokenHeader; privateStringsecret; privateLongexpiration; privateStringtokenHead; }
编写JWT工具类,实现生成token和解码token的功能:
publicclassJwtKit { privateJwtPropertiesjwtProperties; /*** 创建token* @param user* @return*/publicStringgenerateJwtToken(Useruser){ //所有的用户数据放在claims中Map<String,Object>claims=newHashMap<>(); claims.put("sub",user.getUsername()); claims.put("createdate",newDate()); claims.put("id",user.getId()); claims.put("userLevelId",user.getLevelId()); returnJwts.builder() .setClaims(claims) .setHeaderParam("typ", "JWT") .setExpiration(newDate(System.currentTimeMillis()+jwtProperties.getExpiration()*1000)) .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret()) .compact(); } /*** 解码token* @param jwtToken* @return*/publicClaimsparseJwtToken(StringjwtToken){ Claimsclaims=null; try { claims=Jwts.parser() .setSigningKey(jwtProperties.getSecret()) .parseClaimsJws(jwtToken) .getBody(); }catch (ExpiredJwtExceptione){ thrownewMyException("JWTtoken过期"); }catch (UnsupportedJwtExceptione){ thrownewMyException("JWTtoken格式不支持"); }catch (MalformedJwtExceptione){ thrownewMyException("无效的token"); }catch (SignatureExceptione){ thrownewMyException("无效的token"); }catch (IllegalArgumentExceptione){ thrownewMyException("无效的token"); } returnclaims; } }
再将这个工具类注入到Bean容器中:
publicclassJwtConfiguration { publicJwtKitjwtKit(){ returnnewJwtKit(); } }
然后就可以愉快地使用了
"/sso") (publicclassUserControllerextendsBaseController { privateUserServiceuserService; privateJwtKitjwtKit; privateJwtPropertiesjwtProperties; "jwtlogin") (publicCommonResultjwtLogin(Stringusername,Stringpassword){ Useruser=userService.login(username, password); if (user!=null){ Map<String,Object>map=newHashMap<>(); Stringtoken=jwtKit.generateJwtToken(user); map.put("tokenHead",jwtProperties.getTokenHead()); map.put("token",token); returnnewCommonResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),map); } returnnewCommonResult(ResponseCode.USER_NOT_EXISTS.getCode(),ResponseCode.USER_NOT_EXISTS.getMsg(),""); } }
登陆成功的话将token的开头和实际token值返回给后端。
接着编写拦截器,拦截除/sso/*外的其他请求,这些请求是需要登陆才可以访问的。
publicclassIntercepterConfigurationimplementsWebMvcConfigurer { publicvoidaddInterceptors(InterceptorRegistryregistry) { Listlist=newArrayList(); list.add("/sso/**"); registry.addInterceptor(authInterceptorHandler()) .addPathPatterns("/**") .excludePathPatterns(list); } publicAuthInterceptorHandlerauthInterceptorHandler(){ returnnewAuthInterceptorHandler(); } }
接着是对拦截的处理,首先判断header中是否有key为Authorization的数据,如果没有,说明未登陆,将未登录的结果返回出去。如果header中存在key为Authorization的数据,则先判断是否以Bearer开头(这个可以自定义),如果是的话说明登陆了,就直接返回true不拦截。
publicclassAuthInterceptorHandlerimplementsHandlerInterceptor { privateJwtKitjwtKit; privateJwtPropertiesjwtProperties; publicbooleanpreHandle(HttpServletRequestrequest, HttpServletResponseresponse, Objecthandler) throwsException { log.info("进入拦截器"); Stringaa=request.getHeader("aa"); Stringauthorization=request.getHeader(jwtProperties.getTokenHeader()); log.info("Authorization"+authorization); //如果不为空,说明head里存了数据,if(StringUtils.isNotEmpty(authorization) &&authorization.startsWith(jwtProperties.getTokenHead())){ StringauthToken=authorization.substring(jwtProperties.getTokenHead().length()); Claimsclaims=null; try { claims=jwtKit.parseJwtToken(authToken); if (claims!=null){ returntrue; } }catch (MyExceptione){ log.info(e.getMessage()+":"+authToken); } } response.setHeader("Content-Type","application/json"); response.setCharacterEncoding("UTF-8"); Stringresult=newObjectMapper().writeValueAsString(newCommonResult(ResponseCode.NEED_LOGIN.getCode(), ResponseCode.NEED_LOGIN.getMsg(), "")); response.getWriter().println(result); returnfalse; } }
最后写一个测试类:
publicclassIndexControllerextendsBaseController{ privateJwtPropertiesjwtProperties; privateJwtKitjwtKit; value="/index",method=RequestMethod.GET) (publicCommonResultindex(){ Stringauthorization=getRequest().getHeader(jwtProperties.getTokenHeader()); StringauthToken=authorization.substring(jwtProperties.getTokenHead().length()); Claimsclaims=jwtKit.parseJwtToken(authToken); returnnewCommonResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),claims.get("sub")); } }
(五)效果测试
首先我直接访问http://localhost:8189/index,返回结果为登陆失效,因为没有传header
于是先访问登陆接口:http://localhost:8189/sso/jwtlogin
返回了具体的tokenHead和token实际值。 将这个token放进header里,再次访问index
接口:
操作成功,并且可以取到用户信息。
(六)JWT和Session的对比
前面一篇文章我用了Session实现了认证的功能,但是需要额外的Redis服务器才可以实现分布式的认证。而使用JWT不需要额外的服务器,它是把token放在header中的。
但是JWT同样存在缺点,最明显的就是这段token无法让他手动失效,生成token后,就算注销登陆,token依然是有效的。
第二点缺点是人员信息是base64后的数据,相当于只要拿到就可以被使用,因此JWT只能传输非敏感的人员数据。
第三点缺点是由于每次请求都需要在header中携带token信息,增大了带宽的压力。别觉得一个请求的header就多占用了那么一点带宽,如果是一万个或者是十万个请求呢?带宽的资源是很珍贵的。
综上所述,JWT和Session各有优劣,如何使用就看你的系统情况了。好了,有什么问题评论区留言,我们下期再见!