Java架构师方案—基于JWT的Token认证(附完整项目代码)


作者:空白

1. JWT鉴权、session认证

谈论两者区别之前,我们先认识下认证、鉴权的定义。

Authentication/认证

几乎所有的系统都会需要用户登入来做进一步操作,像淘宝、京东电商系统的商品浏览、评论查看等功能是不需要用户登入的,但是如果用户下单、购买支付等功能就需要用户登入。因此在所有软件系统中,功能可区分为要求登入的操作和不要求登入的操作。

系统操作分类:需要用户登入的操作、不需要用户登入的操作。

用户登入、输入用户名和密码确认登入的过程就是认证

Authorization/鉴权

鉴权是指在用户登入认证之后,验证用户是否拥有访问系统的权利。传统的鉴权是通过密码来验证的。这种方式的前提是,每个获得密码的用户都已经被授权。如果没有鉴权逻辑,那么每次用户使用一些需要用户登入的功能时,都要做一次登入认证操作。这样的体验是很糟糕的。

当用户点击一些功能或进行操作时候,系统会对用户进行校验权限,如果用户权限token无效,则用户就不能做任何事。

基于token认证和session认证

这是目前主流的两种认证方式,下面来看看相关逻辑过程。

session认证

基于session认证流程如下图所示:

alt

流程分析:

1)用户在登入页面,输入用户名/密码等登入信息。

2)服务器后端验证登入信息是否正确,登入成功后创建一个sessionid,这个id具有唯一性,放入到cookie或请求header中。

3)用户后续的请求都会携带sessionid数据,服务端会验证sessionid的有效性,选择是否接受请求。

4)用户退出登入时候,服务端会删除内存中的session信息,sessionid失效,后续的请求将不再被接受。

问题:用户登入后,session信息会保存在服务器内存中,sessionid会保存在请求中,sessionid在服务器端生成的所有session中具有唯一性。如果sessinid被盗用,那么其他用户也能不用登入就能访问 需要登入的网页。

解决方案:

1)cookie设置HttpOnly属性,js脚本就无法读取到cookie信息,能有效的防止XSS攻击,窃取cookie内容,增加安全性。

2)不使用固定sessionid,每次登入都是另外一个唯一性的sessinid值,设置过期时间,因此就算sessionid值泄露,系统也会防住攻击。

token认证

token认证,服务器内存不会存放session信息。token数据将承载着用户登入状态信息。

流程图如下:

alt

1)用户输入用户名/密码登入信息。

2)服务器验证登入信息,并返回jwt签名token。

3)jwt签名token存储在客户端的cookie或local storage中。

4)后续的请求都将携带token信息。

5)服务器解码token,验证token有效性,判断是否接受请求。

6)用户退出登入,客户端删除token,不会请求服务器,因为服务器没有存储token以及任何用户登入的状态信息。

2. 什么是JWT

一张图来快速了解jwt token的结构

alt

运行demo项目:jackdking-login-jwt-token。登入后查看页面cookie信息如下所示:

Bearer+eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTYwMzQ2NzcwNywiZXhwIjoxNjAzNDcxMzA3fQ.LeuTshSD_mwD7xG9ZunAbyGNPaHwhAL7cD5Z5i2qPYc

jwt token数据可从浏览器cookie中查看到。

alt

JWT Token结构

token串由三个信息文本拼接成的,连接的符合是“.“,那这三个信息文本又是如何生成的、有什么样的意义?

这三个部分的信息文本分别是header(头部)、playload(载荷)、signature(验签),都是json数据。

header

jwt头部包括两部分信息:

  • token类型

  • 加密算法,通常使用 HMAC SHA256。

完整的json数据如下所示:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

虽然我们使用JWT工具类生成jwt token,不需要自己去手动创建header数据,但是还是需要了解header信息文本的内容结构的。header的信息文本就是通过对这个json字符串进行base64加密得到的(属于对称加密)。

playload

这一部分,除了jwt标准定义的属性,我们还可以放入用户的信息,但是因为这个信息文本是对称加密的,所以不建议放入敏感的数据,例如用户密码,身份证等,防止信息的泄露。载荷部分的信息分为三个部分,如下所示:

  • 标准中注册的声明字段

  • 私有的数据字段

  • 公共的声明

标准字段:JWT的标准所定义的字段如下

iss: 该JWT的签发者
sub: 该JWT所面向的用户
aud: 接收该JWT的一方
exp(expires): 什么时候过期,这里是一个Unix时间戳
iat(issued at): 在什么时候签发的
jti(jwt identify): jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
nbf: 定义在什么时间之前,该jwt都是不可用的.

公共字段:公共的部分存放用户信息,或者业务相关的数据,但是这部分是明文,在浏览器即可查看到,建议不要存放敏感的信息。

私有字段:这部分的数据是会进行base64加密的,虽然不是明文信息,在浏览器不能直接查看,但是base64是对称加密方式,解密后还是能看到数据,因此也不要存放敏感信息。

总之,jwt的token长串里不要存放敏感的用户信息或业务信息。

signature/签名

这部分是个最重要的地方,这部分决定了这个jwt token串是否有效。下面就来看看签名的生成逻辑。

jwt的第三部分是由base64加密后的串拼接而成,拼接符号是“.“,拼接后的字符串:header.payload。然后再将拼接后的串和jwt的秘钥一起进行组合加密,加密算法通常是HMAC-SHA256,这个是由header部分的alg字段声明决定的。这样就得到了第三部分文本信息。

到这里,jwt token长串的三个部分的文本信息就全部生成了,通过符号“.“拼接在一起就形成了jwt token。

但是我们还需要提醒开发者,第三部分生成签名的秘钥只能保存在服务端,不然就没办法保证jwt token的签发是由服务器完成的,如果存放在客户端泄露出去,那么任何人都能签发jwt token毫无阻碍的访问你的系统,这是极其危险的。

现在对jwt token结构有了清楚的认识吧,下面就让我门开始快速开发吧。

3. 快速开发JWT应用

实现JWT的demo获取方式,请查看文章底部。

demo项目一览

alt

登入和退出接口

@PostMapping(value = "loginCheck")
@ResponseBody
public RestResponseBo loginCheck(@RequestParam String username,
        		@RequestParam String password,
        		HttpServletRequest request,
        		HttpServletResponse response) {
	  if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){

          return RestResponseBo.fail("用户名或者密码不能为空!");
      }
	  if(!username.equals("admin") || !password.equals("admin"))
	  {

          return RestResponseBo.fail("用户名或者密码不正确!");
	  }
	  
	  String jwtToken = jwtTokenProvider.createToken(username);

      //用户浏览器会存放两种cookie: userToken,userId。
      jwtTokenProvider.saveJwtToken(jwtToken);

      return RestResponseBo.ok();
	}


@PostMapping(value = "loginOut")
@ResponseBody
public RestResponseBo loginOut() {
	
	
	jwtTokenProvider.removeJwtToken();
	

    return RestResponseBo.ok();
	
}

登入接口逻辑

1.获取username、password数据,并校验正确性。

2.如果校验通过,则根据username生成 jwt token,调用jwtTokenProvider.createToken(username);

3.成功生成token后,将token值放入到请求体request的cookie中,保存在浏览器。

退出逻辑

jwt退出逻辑,这里选择了服务器后端来删除cookie,但是其实删除cookie一般是放在用户客户端的。demo项目操作cookie都是在后端。

token存储在cookie

存储的cookie结构如代码所示。后续的请求中都会包含这个cookie。

public void saveJwtToken(String jwtToken) {
	// TODO Auto-generated method stub
	
	CookieUtil.addCookie("Authorization", "Bearer "+jwtToken);
	
}

拦截器

拦截器JdkApiInterceptor,会拦截除了登入接口和登入页面url外的所有接口。拦截代码逻辑如下。

1.使用jwt工具类jwtTokenProvider来获取cookie中的token串。

2.如果token为空,则拒绝请求并跳转到登入页面。

3.token不为空,则使用token是否有效的校验工具进行检测jwtTokenProvider.validateToken,无效就跳转到登入页面,有效则继续响应请求。

    /**
     * 拦截请求,在controller调用之前
     * 返回 false:请求被拦截,返回
     * 返回 true :请求OK,可以继续执行,放行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception {
        String jwtToken = jwtTokenProvider.resolveTokenFromCookie();//获取用户cookies 中的jwttoken
        logger.info("解析得到的token值:{}",jwtToken);
        //放开登入接口
//        String uri = request.getRequestURI();
//        logger.info("请求uri:" + uri);
//        
//        if(uri.equals("loginCheck"))
//        	return true;
//         
        //用户id和token都不为空
        if (!StringUtils.isEmpty(jwtToken)) {
        	
        	//根据userid生成唯一key从redis中查出唯一token
        	
            
            //如果唯一token为空 ,则拦截url重定向到登入页面
            if (!jwtTokenProvider.validateToken(jwtToken)) {
                response.sendRedirect("/login");
                returnErrorResponse(response, "请登录...");

                return false;
            }
        //用户id和token有一个为空,则重定向登入页面
        } else {
            response.sendRedirect("/login");
            returnErrorResponse(response,"请登录...");
            return false;
        }

        return true;
    }

运行demo并查看效果

浏览器访问http://localhost:8080/login。

alt

输入登入用户名/密码:admin/admin,登入成功跳转到index页面。

alt

查看页面中的token cookie

可以看到,cookie中已经存放了jwt token串。

Bearer+eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTYwMzYwNDAxNywiZXhwIjoxNjAzNjA3NjE3fQ.IB4LFRV5haQE2oec7PLXFsFPqUbpnH1Du0kDqvos-cE	

alt

至此,jwt项目的开发就完成了,demo项目可直接运行。

4. JWT的优点和缺点分析

这里简单谈谈jwt鉴权的优缺点。

优点:

  • 服务端不需要保存token,那么大量用户在线情况下,服务器端就不会像sessin认证那样那么大的压力,非常好扩展。

  • 退出逻辑这块,jwt模式的退出不与服务器交互,客户端删除token的cookie即可。

  • 支持网站,app,小程序等多种用户端的应用,对平台友好。

  • jwt token串中载荷部分可以承载不敏感的用户数据和业务数据。

  • jwt结构也非常简单,数据的字节占用还是比较少的,传输快。

缺点

  • jwt的三个部分,信息承载的方式是明文和对称加密两种,不适合传输敏感信息。

  • 比起sessionId,token串的数据量还是非常多的,传输时间比较慢。

  • jwt token无法废弃。因为在服务器端是无状态的,不保存token信息,因此服务器端无法弃用。

  • jwt token的有效期无法续签(一次性的),需要重新签发新的jwt。不像session能自动刷新有效期时间。

  • 如果像更新jwt中的载荷中信息,那么就需要添加jwt的失效逻辑,就需要在服务器这边保存jwt,类似黑名单逻辑。

jwt适合的应用场景

  • 只使用一次的场景:如一些验证邮件。

  • 有效期短的场景。

  • jwt不适合单点登入和会话管理的场景,因为这些场景需要在服务器侧保存jwt,还不如使用成熟框架比较多的session框架。

其实token认证的应用非常广泛,像Twitter、微信、QQ、GitHub的公共服务API都是使用token认证。但有些大型网站架构中会选择session+token结合一起。微服务架构网站的认证逻辑非常复杂,后续我们团队会继续输出相关联的架构方案文章,并附上完整的demo项目。请关注我们团队。

完整的demo项目,请关注公众号“前沿科技bot“并发送"jwt"获取。

alt

扫码或搜索:前沿科技
发送 290992
即可立即永久解锁本站全部文章