比方在一个商城系统中(这个商城为微服务架构),比如现在有这么这么几个服务:
eureka服务注册中心(端口10000)
用户服务(端口10001),
购物车服务(端口10002),
订单服务(端口10003),
zuul网关(端口9000)
其中购物车服务和订单服务需要在登录后才能访问,那么我们如何判断用户是否登录了呢?
我们可能会想到Session。Session是存储在服务器端,现在用户访问用户访问登录并在用户访问中存储了Session,那么我们需要考虑的是在其他服务我们能获取到这个Session吗?答案是不能,因为它们不是同一个服务,如果要访问其他服务需要自己实现Session共享!
这时候我们可以使用token来解决这个问题。在微服务架构中实现token的标准方案是JWT
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;它是分布式服务权限控制的标准解决方案!
Token官网:https://jwt.io
如图所示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每个部分都是可以解密的
项目架构分析
在该微服务架构中一个有四个微服务,分别是:
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
<properties>
<jjwt.version>0.7.0jjwt.version>
<joda-time.version>2.9.6joda-time.version>
properties>
<dependencies>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>${jjwt.version}version>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
<version>${joda-time.version}version>
dependency>
dependencies>
这个类主要是用来生成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();
}
}
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;
}
}
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());
}
}
这里我简单处理,只是为了模拟
(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("获取商品列表信息成功"));
}
}
网关的拦截器中拦截了该请求(该请求的uri并不是制定被放行的uri),判断了该请求是否携带token,或者说它携带的token在拦截器中无法解析(解析方式是我定义的,该token使用我的解析方式无法解析),这时向客户端响应对应的提示信息
1.登录
2.访问shopping_cart-service服务中的接口
结果
测试结果为预想结果,OK!
https://gitee.com/Tang1314521/jwt-demo.git