# jwt-demo **Repository Path**: zing173/jwt-demo ## Basic Information - **Project Name**: jwt-demo - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2021-11-30 - **Last Updated**: 2021-12-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # JWT ## 一、概念 Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准([(RFC 7519](https://link.jianshu.com?t=https://tools.ietf.org/html/rfc7519))。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。 JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。 JWT 是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。 >官网:https://jwt.io/ ## 二、跨域认证的问题 互联网服务离不开用户认证。一般流程是下面这样。 > 1、用户向服务器发送用户名和密码。 > > 2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。 > > 3、服务器向用户返回一个 session_id,写入用户的 Cookie。 > > 4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。 > > 5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。 这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。 举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现? - 一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。 - 另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。 ## 三、JWT 的原理 JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。 ```js { "姓名": "张三", "角色": "管理员", "到期时间": "2018年7月1日0点0分" } ``` 以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。 服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。 ## 四、JWT 的数据结构 实际的 JWT 大概就像下面这样。 ![编码的 JWT](images/encoded-jwt4.png) 它是一个很长的字符串,中间用点(`.`)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。 JWT 的三个部分依次如下: - Header(头部) - Payload(负载) - Signature(签名) 写成一行为:`Header.Payload.Signature` ![img](images/bg2018072303.jpg) 下面依次介绍这三个部分。 ### 1、Header Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。 ```js { "alg": "HS256", "typ": "JWT" } ``` 上面代码中,`alg`属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);`typ`属性表示这个令牌(token)的类型(type),JWT 令牌统一写为`JWT`。 最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串,从而形成JWT的第一部分。 ### 2、 Payload 令牌的第二部分是负载,其中包含声明。声明是关于实体(通常是用户)和附加元数据的声明。存在三种类型的声明:*reserved*、*public*和*private*声明。 Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。 **1)保留声明**:这些是一组预定义的声明,这些声明不是强制性的,而是推荐的,旨在提供一组有用的、可互操作的声明。JWT 规定了7个官方字段,供选用,如: - iss (issuer):签发人 - exp (expiration time):过期时间 - sub (subject):主题 - aud (audience):受众 - nbf (Not Before):生效时间 - iat (Issued At):签发时间 - jti (JWT ID):编号 **请注意,声明名称只有三个字符,因为 JWT 是紧凑的** **2)公共声明**:这些可以由使用 JWT 的人随意定义。但是为了避免冲突,它们应该在 IANA JSON Web Token Registry 中定义或定义为包含抗冲突命名空间的 URI。 **3)私人声明**:这些自定义声明是为了在同意使用它们的各方之间共享信息而创建的。 ```js { "sub": "1234567890", "name": "John Doe", "admin": true } ``` 注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。 同样的,这个 JSON 对象也要使用 Base64URL 算法转成字符串,从而形成JWT的第二部分。 ### 3、 Signature Signature 部分是对前两部分的签名,防止数据篡改。 首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。 ```js HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) ``` 算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(`.`)分隔,就可以返回给用户。 ### 4、Base64URL 前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。 JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符`+`、`/`和`=`,在 URL 里面有特殊含义,所以要被替换掉:`=`被省略、`+`替换成`-`,`/`替换成`_` 。这就是 Base64URL 算法。 ### 5、代码实现 ```java package org.jwt.test; import com.alibaba.fastjson.JSONObject; import org.apache.commons.codec.binary.Base64; import org.jwt.entity.User; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; /** * @author zqx * @date 2021-11-20 */ public class JwtSelfTest { private static final String CHARSET = "UTF-8" ; private static final String KEY = "112233abc" ; public static void create() throws Exception { // 1.Header JSONObject originHeader = new JSONObject(); originHeader.put("typ","JWT"); originHeader.put("alg","HS256") ; String header = Base64 .encodeBase64URLSafeString(originHeader.toJSONString().getBytes(CHARSET)); // 2.Payload JSONObject originPayload = new JSONObject(); User user = new User("zing","123","男",18) ; originPayload.put("user",user); String payload = Base64 .encodeBase64URLSafeString(originPayload.toJSONString().getBytes(CHARSET)); System.out.println(payload); // 3.signature String signature = hmacSha256Encode(header, payload); String token = String.format("%s.%s.%s",header,payload,signature) ; System.out.println(token); } private static String hmacSha256Encode(String header,String payload) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException { // 获取某个摘要算法对象 Mac hmacSHA256 = Mac.getInstance("HmacSHA256") ; // 指定摘要算法的key SecretKeySpec key = new SecretKeySpec(KEY.getBytes(CHARSET), "HmacSHA256"); hmacSHA256.init(key); hmacSHA256.update(header.getBytes(CHARSET)); hmacSHA256.update(".".getBytes(CHARSET)); hmacSHA256.update(payload.getBytes(CHARSET)); byte[] bytes = hmacSHA256.doFinal(payload.getBytes(CHARSET)); return Base64.encodeBase64URLSafeString(bytes) ; } public static void main(String[] args) throws Exception{ create(); } } ``` ## 五、JWT 的使用方式 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。 此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息`Authorization`字段里面。 ```js Authorization: Bearer ``` 另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。 ![JSON 网络令牌如何工作](images/17.png) ## 六、JWT 的几个特点 (1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。 (2)JWT 不加密的情况下,不能将秘密数据写入 JWT。 (3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。 (4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。 (5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。 (6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。 >https://zhuanlan.zhihu.com/p/86937325 > >https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html ## 七、JWT实现用户登录 ### 1、前端页面 #### 1)登录页面 ```html 用户登录
帐号:
密码:
错误的帐号或密码
``` 前端存储token可以使用以下两种方式: - localStorage - sessionStorage #### 2)登录成功页面 ```html 后面页面

欢迎光临,{{user.username}} {{user.sex=='男'?'先生':'女士'}}

{{ tips }},5秒后回到登录页面

``` 前端传递token可以使用以下两种方式: - 使用请求参数的方式传递token; - 使用请求头的方式传递 ### 2、JwtUtil ```java package org.jwt.utils; import cn.hutool.core.date.DateUtil; import io.jsonwebtoken.*; import java.util.Date; import java.util.Map; import java.util.UUID; /** * @author zqx * @date 2021-11-18 */ public class JwtUtil { /** * 密钥,用于signature(签名)部分解密 */ private static final String KEY = "112233abc" ; /** * 签发者 */ private static final String ISS = "edu.nf"; public static String createToken(Map claims, long ttl) { JwtBuilder builder = Jwts.builder() //获取签名秘钥,并采用HS512算法对JWT进行的签名 .signWith(SignatureAlgorithm.HS256,KEY) //jwt唯一标识 .setId(UUID.randomUUID().toString()) //设置需要签署的内容 .setClaims(claims) //设置发证人 .setIssuer(ISS) //主题 .setSubject("JWT_AUTH") //签名时间 .setIssuedAt(new Date()); //过期时间 if(ttl >= 0){ builder.setExpiration(getExpDate(ttl)); } return builder.compact(); } /** * 创建token过期时间 * @param ttl * @return */ private static Date getExpDate(long ttl){ Date expDate = DateUtil.date(System.currentTimeMillis() + ttl); return expDate; } /** * 验证Token是否正确 */ public static void verify(String token){ try { Jwts.parser().setSigningKey(KEY).parseClaimsJws(token); } catch (SecurityException e) { throw new RuntimeException("Invalid JWT signature."); } catch (MalformedJwtException e) { throw new RuntimeException("Invalid JWT token."); } catch (ExpiredJwtException e) { throw new RuntimeException("Expired JWT token."); } catch (UnsupportedJwtException e) { throw new RuntimeException("Unsupported JWT."); } catch (IllegalArgumentException e) { throw new RuntimeException("JWT token compact of handler are invalid."); } } /** * 获取payload的内容 * @param token * @param name * @param type * @param * @return */ public static T getPayload(String token,String name,Class type) { return Jwts.parser() .setSigningKey(KEY) .parseClaimsJws(token) .getBody() .get(name, type); } } ``` JWT提供了各种语言的实现,官网如下所示: >https://jwt.io/libraries 其中,Java的实现也提供了多种实现,常用的有: - Java-jwt - jjwt-root ### 3、登录Servlet ```java package org.jwt.servlet; /** * @author zqx * @date 2021-11-19 */ @WebServlet("/api/login") public class UserLoginServlet extends BaseServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 获取客户端信息 String username = req.getParameter("username"); String password = req.getParameter("password"); // 非空帐号和密码登录成功 if(!"".equals(username) && !"".equals(password)) { // 定义payload信息 Map payload = new HashMap<>() ; payload.put("username",username) ; // 创建Token,设置有效时间为10秒钟 String token = JwtUtil.createToken(payload, 10*1000) ; // 响应客户端 print(resp,successJson(token)); return ; } // 登录失败 print(resp,errorJson("错误的帐号或密码")); } } ``` ### 4、认证用户 ```java package org.jwt.filter; import com.google.gson.Gson; import org.jwt.dto.ResultVO; import org.jwt.utils.JwtUtil; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @author zqx * @date 2021-11-14 */ @WebFilter("/api/*") public class ZAuthFilter implements Filter { private static final String LOGIN = "login" ; public static final String TOKEN_HEADER = "Authorization"; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //1.向下转型 HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String method = request.getMethod(); if("OPTIONS".equals(method)) { filterChain.doFilter(request, response); return ; } //2.设置统一编码 String requestURI = request.getRequestURI(); if(requestURI.endsWith(LOGIN)) { //3.放行 filterChain.doFilter(request, response); return; } String token = request.getHeader(TOKEN_HEADER); if(token==null || "".equals(token)) { responseView(response,"请先登录") ; return ; } try { JwtUtil.verify(token); String username = JwtUtil.getPayload(token, "username", String.class); if(username==null || username=="") { responseView(response,"解析token失败") ; return; } filterChain.doFilter(request, response); } catch (Exception e) { responseView(response,"会话已失效") ; } } @Override public void destroy() { } private void responseView(HttpServletResponse response, String message) throws IOException { ResultVO vo = new ResultVO(); vo.setCode(401); vo.setMsg(message); response.setContentType("application/json;charset=utf-8"); String json = new Gson().toJson(vo) ; PrintWriter out = response.getWriter(); out.print(json) ; out.flush(); out.close(); } } ``` ### 5、登录成功Servlet ```java package org.jwt.servlet; import org.jwt.entity.User; import org.jwt.utils.JwtUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author zqx * @date 2021-11-19 */ @WebServlet("/api/main") public class MainServlet extends BaseServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 获取客户端发送过来的token // String token = req.getParameter("token"); String token = req.getHeader("Authorization"); // 获取payload信息 String username = JwtUtil.getPayload(token, "username", String.class); // 模拟DAO,查询当前登录用户信息 User user = new User(username, null, "男", 18); // 响应客户端 print(resp, successJson(user)); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } } ``` 注意: 1)预检请求问题:对于非简单请求,会向服务器发起两个请求,先发送一个OPTIONS的预检请求(preflight request),用于获取服务器响应头信息,比如CorsFilter设置的响应头,浏览器判断返回的响应头,如果允许跨域访问,则发起第二次请求,请求到具有的资源,否则不再发送第二次请求。因此,在认证用户(ZAuthFilter)中,需要对OPTIONS请求方式放行。 >预检请求:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS 2)Filter执行顺序问题:在Servlet3.0中,使用注解方式的Filter是根据字母的排序来确定执行顺序中,必须保证跨域处理的过滤器首先被执行,解决办法为: - 使用web.xml配置过滤器 - 重命名Filter - 动态注册Filter ### 6、跨域处理 ```java package org.jwt.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author zqx * @date 2021-11-08 */ @WebFilter("/api/*") public class CorsFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("允许跨域操作.."); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse resp = (HttpServletResponse) servletResponse ; // 允许所有的域名 resp.setHeader("Access-Control-Allow-Origin", "*"); // 允许请求所有的方法 resp.setHeader("Access-Control-Allow-Methods", "*"); // 允许请求头信息字段(Authorization) resp.setHeader("Access-Control-Allow-Headers", "Authorization,Origin,X-Requested-With,Content-Type,Accept," + "content-Type,origin,x-requested-with,content-type,accept,authorization,token,id,X-Custom-Header,X-Cookie,Connection,User-Agent,Cookie,*"); filterChain.doFilter(servletRequest,resp); } @Override public void destroy() { } } ``` ### 7、ResultVO ```java package org.jwt.dto; /** * @author zqx * @date 2021-11-19 */ public class ResultVO { /** * 消息代码,默认200 */ private int code = 200; /** * 客户端消息 */ private String msg = ""; /** * 返回客户端具体的数据结果 */ private Object data; // setter/getter } ``` ### 8、BaseServlet ```java package org.jwt.servlet; /** * @author zqx * @date 2021-11-19 */ public class BaseServlet extends HttpServlet { public ResultVO successJson(Object value) { ResultVO resultVo = new ResultVO(); resultVo.setData(value); return resultVo; } public ResultVO successJson(Object value, int code, String msg) { ResultVO resultVo = new ResultVO(); resultVo.setData(value); resultVo.setCode(code); resultVo.setMsg(msg); return resultVo; } public ResultVO successJson() { return new ResultVO(); } public ResultVO errorJson() { ResultVO resultVo = new ResultVO(); resultVo.setCode(500); resultVo.setMsg("系统繁忙,请稍后再试!"); return resultVo; } public ResultVO errorJson(int code, String msg) { ResultVO resultVo = new ResultVO(); resultVo.setCode(code); resultVo.setMsg(msg); return resultVo; } public ResultVO errorJson(String msg) { ResultVO resultVo = new ResultVO(); resultVo.setCode(500); resultVo.setMsg(msg); return resultVo; } public void print(HttpServletResponse resp, ResultVO resultVO) throws IOException{ resp.setCharacterEncoding("UTF-8"); resp.setContentType("application/json;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.print(new Gson().toJson(resultVO)); out.flush(); out.close(); } } ```