JWT
1.为什么要使用JWT
比方在一个商城系统中(这个商城为微服务架构),比如现在有这么这么几个服务:
eureka服务注册中心(端口10000)
用户服务(端口10001),
购物车服务(端口10002),
订单服务(端口10003),
zuul网关(端口9000)
其中购物车服务和订单服务需要在登录后才能访问,那么我们如何判断用户是否登录了呢?
我们可能会想到Session。Session是存储在服务器端,现在用户访问用户访问登录并在用户访问中存储了Session,那么我们需要考虑的是在其他服务我们能获取到这个Session吗?答案是不能,因为它们不是同一个服务,如果要访问其他服务需要自己实现Session共享!
这时候我们可以使用token来解决这个问题。在微服务架构中实现token的标准方案是JWT
2.JWT的简介
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;它是分布式服务权限控制的标准解决方案!
Token官网:https://jwt.io
3.简单的介绍Token的数据格式
普通token:32位UUID
JWT生成的Token
如图所示JWT生成的token包含了三部分数据:
- Header:头部,通常头部有两部分信息:
- 声明类型type,这里是JWT(type=jwt)
- 加密算法,自定义的(使用rs256/base64/hs256)
我们会对头部进行base64加密(可解密),得到第一部分数据
- Payload:载荷,就是有效数据,一般包含下面信息
- 用户身份信息userid,username(注意:这里因为采用base64加密是可以解密的,因此不要存放敏感信息)
- 注册声明:如token的签发时间,过期时间,签发人等
这部分也会采用base64加密,得到第二部分数据
- Signature:base64加密,签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的密钥(secret,盐。不要泄露,最好声明周期跟换),通过加密算法生成,用于验证整个数据完整和可靠
总结
1.JWT生成的token是一个有规则的
2.它有三部分组成:Header.payload.signature,每部分都是通过base64加密而成
3.JWT每个部分都是可以解密的
4.快速上手
1.项目结构
项目架构分析
在该微服务架构中一个有四个微服务,分别是:
1.auth-service:身份验证服务,该微服务主要用来对用户的身份进行验证以及生成token
2.eureka-server:服务注册中心
3.shopping_cart-service:购物车服务,处理购物车相关的业务
4.zuul-service:网关,用来实现间权,动态路由,负载均衡
common:相当于一个工具包,用来存放常被复用的类,例如:JWT工具类JWTUtil.java,接口响应对象Result.java接口响应对象,接口响应状态管理工具类StatusCode.java
在启动好个个微服务后,个个微服务会将自己注册到eureka当中,并将自己的地址信息交给eureka管理。在不同服务接口的调用不再是直接去访问对应访问中的接口,而是通过zuul网关的动态路由来访问访问中的接口,访问方式如下所举例:
localhost:9000/api/auth-service/auth/login
localhost:9000:网关服务的地址
/api:网关的前缀路径
auth-service:服务的名称
auth:该服务业务层的窄路径
login:对应的是业务层中的接口名
在网关中还会对请求进行鉴权。(在访问需要登录后的接口)
网关中集成了负载均衡,我们在配置文件中进行配置即可
实现的需求
发起请求,请求后端接口,在网关中对请求进行鉴权,(实际上就是在网关中编写了一个拦截器拦截请求),判断请求的uri,如果请求的是用户身份验证服务中的登录接口,直接放行,在该接口中对用户提交的登录信息进行验证,如果身份信息正确颁发jwt凭证,生成token,并将token响应到客户端存储在localStorage中,(客户端每次请求都会携带上这个token)如果失败响应错误信息。`
如果请求的uri不是指定被放行接口的uri,将会拦截该请求,通过HttpServletRequest获取头部信息中的token,判断token是否为空,如果不为空对token进行解析,解析成功放行,负责响应信息提示用户先登录再进行访问。
主要:我这里简单的模拟该需求,并没有使用到关系型数据MySQL
2.导入JWT相关依赖(common)
<properties>
<jjwt.version>0.7.0</jjwt.version>
<joda-time.version>2.9.6</joda-time.version>
</properties>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda-time.version}</version>
</dependency>
</dependencies>
3.编写JWTUtil.java(common/com.jn.util.JWTUtil.java)
这个类主要是用来生成token和解析token
package com.jn.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
/**
* @Description: token工具类
*/
public class JWTUtil {
/**
* 获取token中的参数
*
* @param token
*Z
*/
public static Claims parseToken(String token, String key) {
if ("".equals(token)) {
return null;
}
try {
return Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(key))
.parseClaimsJws(token).getBody();
} catch (Exception ex) {
return null;
}
}
/**
* 生成token
*
* @param userId
* @return
*/
public static String createToken(Integer userId,String username,String key, int expireMinutes) {
//TODO
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成签名密钥
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(key);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//添加构成JWT的参数
JwtBuilder builder = Jwts.builder()
.setHeaderParam("type", "JWT")
.setSubject(userId.toString())
.claim("userId", userId) // 设置载荷信息
.claim("username",username)
.claim("age",23)
.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())// 设置超时时间
.signWith(signatureAlgorithm, signingKey);
//生成JWT
return builder.compact();
}
}
4.在zuul网关中编写拦截器,拦截请求(zuul-service/com.jn.filter.JWTFilter.java)
package com.jn.filter;
import com.jn.util.JWTUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @Author ScholarTang
* @Date 2020/3/15 10:36 PM
* @Desc 权限校验(过滤器)
*/
@Component
public class JWTFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String uri = request.getRequestURI();
if (uri.contains("/login")) {
System.out.println("无需拦截");
return null;
}
String token = request.getHeader("authorization");
if (token != null) {
Claims claims = JWTUtil.parseToken(token, "user");
if (claims != null) {
return null;
}
}
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(401);
currentContext.setResponseBody("{'flag':'false','msg':'请登录'}");
currentContext.getResponse().setContentType("text/html;charset=utf-8");
return null;
}
}
5.在用户身份验证服务中编写一个校验用户信息的接口(auth-service/com.jn.controller.AuthController.java)
package com.jn.controller;
import com.jn.domain.User;
import com.jn.util.JWTUtil;
import com.jn.vo.Result;
import com.jn.vo.ResultUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author ScholarTang
* @Date 2020/3/15 10:36 AM
* @Desc 处理用户登录业务
*/
@RestController
@RequestMapping("/auth")
public class LoginController {
//正确的用户信息
private final User USER_DATA = new User(1,"123","123");
/**
* 模拟登录的方法
* @param user
* @return
*/
@PostMapping("/login")
public ResponseEntity<Result> login (@RequestBody User user) {
//简单的校验用户名和密码
if (user.getAccount().equals(USER_DATA.getAccount())) {
if (user.getPassword().equals(USER_DATA.getPassword())) {
//生成token
String token = JWTUtil.createToken(USER_DATA.getUid(), USER_DATA.getAccount(), "user", 30);
return ResponseEntity.ok(ResultUtil.SUCCESSFUL(token));
}
}
return ResponseEntity.ok(ResultUtil.FAILURE());
}
}
6.在购物车服务中编写一个接口模拟获取当前用户的购物车中的商品信息这里我简单处理,只是为了模拟
(shopping_cart-service/com.jn.controller.ShoppingCartController.java)
package com.jn.controller;
import com.jn.vo.Result;
import com.jn.vo.ResultUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author ScholarTang
* @Date 2020/3/15 11:04 AM
* @Desc 处理购物车相关的业务
*/
@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {
/**
* 模拟获取所有购物车中的商品列表
* @return
*/
@GetMapping("/findAll")
public ResponseEntity<Result> findAllShoppingCart(){
return ResponseEntity.ok(ResultUtil.SUCCESSFUL("获取商品列表信息成功"));
}
}
7.测试
1.在没有登录的情况下通过网关去调用shopping_cart-service服务中的接口,效果如图所示
分析
网关的拦截器中拦截了该请求(该请求的uri并不是制定被放行的uri),判断了该请求是否携带token,或者说它携带的token在拦截器中无法解析(解析方式是我定义的,该token使用我的解析方式无法解析),这时向客户端响应对应的提示信息
2.在用户进行登录后再去调用shopping_cart-service服务中的接口
1.登录
2.访问shopping_cart-service服务中的接口
结果
测试结果为预想结果,OK!
5.码云地址
https://gitee.com/Tang1314521/jwt-demo.git