JWT(JSON WEB TOKEN) 简介

本贴最后更新于 2315 天前,其中的信息可能已经时移俗易

来源
https://jwt.io/introduction/
https://www.jianshu.com/p/227306fa28e4
https://blog.csdn.net/luckey_zh/article/details/61197587

JWT 是由三段信息构成的,将这三段信息文本用 . 链接一起就构成了 Jwt 字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT 的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品,存放自定义信息),第三部分是签名(signature).

对应的是 base64UrlEncode(header).base64UrlEncode(payload).HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
其中的 HMACSHA256 算法可以替换为其他加密算法,安全性方面前面 2 部分只是做了 base64 编码,都可以解码出原文,不能存放敏感信息,第三部分也只是对前两部分加 secret 做了一次签名
服务端验证 token 时就是做了一次验签。

一、JWT(一种基于 token 的认证方案)介绍:

(1)概述:JWT 就是一种 Token 的编码算法,服务器端负责根据一个密码和算法生成 Token,然后发给客户端,客户端只负责后面每次请求都在 HTTP header 里面带上这个 Token,服务器负责验证这个 Token 是不是合法的,有没有过期等,并可以解析出 subject 和 claim 里面的数据。

(2)相关问题:

1.为什么用 JWT?

JWT 只通过算法实现对 Token 合法性的验证,不依赖数据库,Memcached 的等存储系统,因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器程序生成的 Token 可以互相验证。

2.JWT Token 不需要持久化在任何 NoSQL 中,不然背离其算法验证的初心

3.在退出登录时怎样实现 JWT Token 失效呢?

退出登录, 只要客户端端把 Token 丢弃就可以了,服务器端不需要废弃 Token。

4.怎样保持客户端长时间保持登录状态?

服务器端提供刷新 Token 的接口, 客户端负责按一定的逻辑刷新服务器 Token。

5.服务器端是否应该从 JWT 中取出 userid 用于业务查询?

REST API 是无状态的,意味着服务器端每次请求都是独立的,即不依赖以前请求的结果,因此也不应该依赖 JWT token 做业务查询, 应该在请求报文中单独加个 userid 字段。

为了做用户水平越权的检查,可以在业务层判断传入的 userid 和从 JWT token 中解析出的 userid 是否一致, 有些业务可能会允许查不同用户的数据。


(3)其他几大认证机制:

1.HTTP Basic Auth

HTTP Basic Auth 简单点说明就是每次请求 API 时都提供用户的 username 和 password,简言之,Basic Auth 是配合 RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的 RESTful API 时,尽量避免采用 HTTP Basic Auth

2.OAuth(开放授权)

是一个开放的授权标准,允许用户让第三方应用访问该用户在某一 web 服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

这里写图片描述

这种基于 OAuth 的认证机制适用于个人消费者类的互联网产品,如社交类 APP 等应用,但是不太适合拥有自有认证权限管理的企业应用;

Cookie 认证机制就是为一次请求认证在服务端创建一个 Session 对象,同时在客户端的浏览器端创建了一个 Cookie 对象;通过客户端带上来 Cookie 对象来与服务器端的 session 对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie 会被删除。但可以通过修改 cookie 的 expire time 使 cookie 在一定时间内有效;

二、cookie 和 session 机制:

(1)概述:

Cookie 和 Session 是为了在无状态的 HTTP 协议之上维护会话状态,使得服务器可以知道当前是和哪个客户在打交道。

Session 是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;

Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式。

因为 HTTP 协议是无状态的,即每次用户请求到达服务器时,HTTP 服务器并不知道这个用户是谁、是否登录过等。现在的服务器之所以知道我们是否已经登录,是因为服务器在登录时设置了浏览器的 Cookie!Session 则是借由 Cookie 而实现的更高层的服务器与浏览器之间的会话。

(2)cookie 实现机制:

Cookie 是由客户端保存的小型文本文件,其内容为一系列的键值对。 Cookie 是由 HTTP 服务器设置的,保存在浏览器中, 在用户访问其他页面时,会在 HTTP 请求中附上该服务器之前设置的 Cookie。

Cookie 的传递流程

1.浏览器向某个 URL 发起 HTTP 请求(可以是任何请求,比如 GET 一个页面、POST 一个登录表单等)
2.对应的服务器收到该 HTTP 请求,并计算应当返回给浏览器的 HTTP 响应。(HTTP 响应包括请求头和请求体两部分)
3.在响应头加入 Set-Cookie 字段,它的值是要设置的 Cookie。
4.浏览器收到来自服务器的 HTTP 响应。
5.浏览器在响应头中发现 Set-Cookie 字段,就会将该字段的值保存在内存或者硬盘中。(Set-Cookie 字段的值可以是很多项 Cookie,每一项都可以指定过期时间 Expires。 默认的过期时间是用户关闭浏览器时。)
6.浏览器下次给该服务器发送 HTTP 请求时, 会将服务器设置的 Cookie 附加在 HTTP 请求的头字段 Cookie 中。(浏览器可以存储多个域名下的 Cookie,但只发送当前请求的域名曾经指定的 Cookie, 这个域名也可以在 Set-Cookie 字段中指定)。)
7.服务器收到这个 HTTP 请求,发现请求头中有 Cookie 字段, 便知道之前就和这个用户打过交道了.
8.过期的 Cookie 会被浏览器删除。

总之,服务器通过 Set-Cookie 响应头字段来指示浏览器保存 Cookie, 浏览器通过 Cookie 请求头字段来告诉服务器之前的状态。 Cookie 中包含若干个键值对,每个键值对可以设置过期时间。

Cookie 提供了一种手段使得 HTTP 请求可以附加当前状态, 现今的网站也是靠 Cookie 来标识用户的登录状态的:

1.用户提交用户名和密码的表单,这通常是一个 POST HTTP 请求。
2.服务器验证用户名与密码,如果合法则返回 200(OK)并设置 Set-Cookie 为 authed=true。
3.浏览器存储该 Cookie。
4.浏览器发送请求时,设置 Cookie 字段为 authed=true。
5.服务器收到第二次请求,从 Cookie 字段得知该用户已经登录。 按照已登录用户的权限来处理此次请求。

问题是什么??风险是什么??

我们知道可以发送 HTTP 请求的不只是浏览器,很多 HTTP 客户端软件(包括 curl、Node.js)都可以发送任意的 HTTP 请求,可以设置任何头字段。 假如我们直接设置 Cookie 字段为 authed=true 并发送该 HTTP 请求, 服务器岂不是被欺骗了?这种攻击非常容易,Cookie 是可以被篡改的

服务器可以为每个 Cookie 项生成签名,由于用户篡改 Cookie 后无法生成对应的签名, 服务器便可以得知用户对 Cookie 进行了篡改。

例子:一个简单的校验过程:

1.在服务器中配置一个不为人知的字符串(我们叫它 Secret),比如:x$sfz32。
2.当服务器需要设置 Cookie 时(比如 authed=false),不仅设置 authed 的值为 false, 在值的后面进一步设置一个签名,最终设置的 Cookie 是 authed=false|6hTiBl7lVpd1P。
3.签名 6hTiBl7lVpd1P 是这样生成的:Hash('x$sfz32'+'true')。 要设置的值与 Secret 相加再取哈希。
5.用户在发送 HTTP 请求时,篡改了 authed 值,设置头字段 Cookie: authed=true|???。 因为用户不知道 Secret,无法生成签名,只能随便填一个。
6.服务器收到 HTTP 请求,发现 Cookie: authed=true|???。服务器开始进行校验: Hash('true'+'x$sfz32'),便会发现用户提供的签名不正确。

通过给 Cookie 添加签名,使得服务器得以知道 Cookie 被篡改。但是!!还是有风险!

因为 Cookie 是明文传输的, 只要服务器设置过一次 authed=true|xxxx 我不就知道 true 的签名是 xxxx 了么, 以后就可以用这个签名来欺骗服务器了。因此 Cookie 中最好不要放敏感数据。 一般来讲 Cookie 中只会放一个 Session Id,而 Session 存储在服务器端。

(3)session 的实现机制:

1.概述:Session 是存储在服务器端的,避免了在客户端 Cookie 中存储敏感数据。 Session 可以存储在 HTTP 服务器的内存中,也可以存在内存数据库(如 redis)中, 对于重量级的应用甚至可以存储在数据库中。

例子:存储在 redis 中的 Session 为例,考察如何验证用户登录状态的问题。

1.用户提交包含用户名和密码的表单,发送 HTTP 请求。

2.服务器验证用户发来的用户名密码。

3.如果正确则把当前用户名(通常是用户对象)存储到 redis 中,并生成它在 redis 中的 ID。

这个 ID 称为 Session ID,通过 Session ID 可以从 Redis 中取出对应的用户对象, 敏感数据(比如 authed=true)都存储在这个用户对象中。

4.设置 Cookie 为 sessionId=xxxxxx|checksum 并发送 HTTP 响应, 仍然为每一项 Cookie 都设置签名。

5.用户收到 HTTP 响应后,便看不到任何敏感数据了。在此后的请求中发送该 Cookie 给服务器。

6.服务器收到此后的 HTTP 请求后,发现 Cookie 中有 SessionID,进行放篡改验证。

7.如果通过了验证,根据该 ID 从 Redis 中取出对应的用户对象, 查看该对象的状态并继续执行业务逻辑。

实现上述过程,在 Web 应用中可以直接获得当前用户。 相当于在 HTTP 协议之上,通过 Cookie 实现了持久的会话。这个会话便称为 Session。


三、Token 认证机制相对于 Cookie 等机制的好处:

1. 支持跨域访问: Cookie 是不允许垮域访问的,这一点对 Token 机制是不存在的,前提是传输的用户认证信息通过 HTTP 头传输。(垮域访问:两个域名之间不能跨过域名来发送请求或者请求数据)

2.无状态(也称:服务端可扩展行):Token 机制在服务端不需要存储 session 信息,因为 Token 自身包含了所有登录用户的信息,只需要在客户端的 cookie 或本地介质存储状态信息.

3.更适用 CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供 API 即可.

4.去耦: 不需要绑定到一个特定的身份验证方案。Token 可以在任何地方生成,只要在你的 API 被调用的时候,你可以进行 Token 生成调用即可.

5.更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8 等)时,Cookie 是不被支持的(你需要通过 Cookie 容器进行处理),这时采用 Token 认证机制就会简单得多。

6. CSRF:因为不再依赖于 Cookie,所以你就不需要考虑对 CSRF(跨站请求伪造)的防范。

7.性能: 一次网络往返时间(通过数据库查询 session 信息)总比做一次 HMACSHA256 计算 的 Token 验证和解析要费时得多.

8.不需要为登录页面做特殊处理: 如果你使用 Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.

9.基于标准化:你的 API 可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).


四、JWT 的 Java 实现:

概述:一个 JWT 实际上就是一个字符串,它由三部分组成,头部、载荷与签名。这里我们只使用简单的载荷,并将 JSON 对象进行 base64 编码得到 token

过程:登录为例子

第一次认证:第一次登录,用户从浏览器输入用户名/密码,提交后到服务器的登录处理的 Action 层(controller)
Login Action 调用认证服务进行用户名密码认证,如果认证通过,Login Action 层调用用户信息服务获取用户信息(包括完整的用户信息及对应权限信息);
返回用户信息后,Login Action 从配置文件再经过工具类处理获取 Token 签名生成的秘钥信息,进行 Token 的生成
生成 Token 的过程中可以调用第三方的 JWT Lib 生成签名后的 JWT 数据;
完成 JWT 数据签名后,将其设置到 COOKIE 对象中,并重定向到首页,完成登录过程;

请求认证

使用:基于 Token 的认证机制会在每一次请求中都带上完成签名的 Token 信息,这个 Token 信息可能在 COOKIE 中,也可能在 HTTP 的 Authorization 头中;

注意:

客户端(APP 客户端或浏览器)通过 GET 或 POST 请求访问资源(页面或调用 API);
认证服务作为一个 Middleware HOOK 对请求进行拦截,首先在 cookie 中查找 Token 信息,如果没有找到,则在 HTTP Authorization Head 中查找;
如果找到 Token 信息,则根据配置文件中的签名加密秘钥,调用 JWT Lib 对 Token 信息进行解密和解码;
完成解码并验证签名通过后,对 Token 中的 exp、nbf、aud 等信息进行验证;
全部通过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;
如果权限逻辑判断通过则通过 Response 对象返回;否则则返回 HTTP 401;

(1)使用 JWT 的包:maven 导入

 <!--JSON WEB TOKEN -->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.6.0</version>
    </dependency>

(2)一个生成 token 的工具类:

public class JavaWebToken {

    private static Logger log = Logger.getLogger(JavaWebToken.class);

    private static Key getKeyInstance() {
//        return MacProvider.generateKey();
        //We will sign our JavaWebToken with our ApiKey secret
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary("APP");
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        return signingKey;
    }

    public static String createJavaWebToken(Map<String, Object> claims) {
        return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, getKeyInstance()).compact();
    }

    public static Map<String, Object> verifyJavaWebToken(String jwt) {
        try {

            Map<String, Object> jwtClaims =
                    Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwt).getBody();
            return jwtClaims;
        } catch (Exception e) {
            log.error("json web token verify failed");
            return null;
        }
    }

}

(3)一个从 request 拿去 session,并且解密 session 得到 token 得到用户 id 的类

public class AuthUtil {
    private static Map<String, Object> getClientLoginInfo(HttpServletRequest request) throws Exception {
        Map<String, Object> r = new HashMap<>();
        String sessionId = request.getHeader("sessionId");
        if (sessionId != null) {
            r = decodeSession(sessionId);
            return r;
        }
        throw new Exception("session解析错误");
    }

    public static Long getUserId(HttpServletRequest request) throws Exception {
        return  Long.valueOf((Integer)getClientLoginInfo(request).get("userId"));

    }

    /**
     * session解密
     */
    public static Map<String, Object> decodeSession(String sessionId) {
        try {
            return verifyJavaWebToken(sessionId);
        } catch (Exception e) {
            System.err.println("");
            return null;
        }
    }
}

使用例子:

登录的时候把信息放进 session,存到 map 里,再交由 JWT 得到 token 保存起来

这里写图片描述

//登录
    @RequestMapping(value = "/login", method = {RequestMethod.GET, RequestMethod.POST}, produces = "text/html;charset=UTF-8")
    public String login(String account) {
        User user = userService.login(account);

        DTO dto = new DTO();
        if (user == null) {
            dto.code = "-1";
            dto.msg = "Have not registered";
        } else {
            //把用户登录信息放进Session
            Map<String, Object> loginInfo = new HashMap<>();
            loginInfo.put("userId", user.getId());
            String sessionId = JavaWebToken.createJavaWebToken(loginInfo);
            System.out.println("sessionID"+sessionId);
            dto.data = sessionId;
        }
        return JSON.toJSONString(dto);
    }

用户登录以后,其他的用户性知道的操作就可以使用 token 进行了,安全快捷方便:

这里写图片描述

//修改昵称
    @RequestMapping(value = "/updateName", method = {RequestMethod.GET, RequestMethod.POST})
    public String updateName(HttpServletRequest request, String name) {
        DTO dto = new DTO();
        try {
        //从session拿到token,再解密得到userid
            Long userId = AuthUtil.getUserId(request);
            boolean userIsExist = userService.updateName(userId, name);
            if (userIsExist == false) {
                dto.code = "-1";
                dto.msg = "Have not updateAvatar";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return JSON.toJSONString(dto);
    }

这就是 token 机制的登录认证功能简单实现了,安全机制等,以后会有博客补充。


源码下载:WEB 后台--基于 Token 的 WEB 后台登录认证机制(并讲解其他认证机制以及 cookie 和 session 机制)

  • JWT

    JWT(JSON Web Token)是一种用于双方之间传递信息的简洁的、安全的表述性声明规范。JWT 作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以 JSON 的形式安全的传递信息。

    20 引用 • 15 回帖 • 6 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...