Spring Cloud中国社区博客

JWT在Spring Cloud中的使用

1.JWT的概念

jwt是通过HMAC(Hash-based Message Authentication Code)计算信息摘要,也可以用RSA公私钥中的私钥进行签名,这个可以根据业务场景选择,对应jwt最终展示实际上就是一个字符串,它一般右三部分组成,头部(header), 载荷(payload), 签名(signature)。

头部(header)

头部一般是关于jwt的描述信息,如其类型以及签名所用的算法等等信息,可以自行定义。如:
{“alg”: “HS256”,“typ”:”JWT”}对它进行base64之后,生成的字符串就成了JWT的Header base64后字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

载荷(PayLoad)

载荷一般来讲就是实际业务中需要在token中传递的数据信息,一般来说是一些非敏感数据,比如用户ID,过期时间等等其他一些与业务相关的数据,如:
{ “accountId”: “admin”, “expTime”: “1498795200”}base64之后字符串:eyJhY2NvdW50SWQiOiJhZG1pbiIsImV4cFRpbWUiOiIxNDk4Nzk1MjAwIn0

签名(Signature)

将上面的两个编码后的字符串都用句号 . 连接在一起(头部在前),就形成了
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOiJhZG1pbiIsImV4cFRpbWUiOiIxNDk4Nzk1MjAwIn0
最后将上面连接结果进行HS256算法进行加密(也可以是其他加密算法),某些算法需要提供加密密钥,如密钥为:secret,则加密完后得到的字符串就是签名。
HS256算法加密后字符串:GsGmzc6su0imWlnoyaCexgq1jNurdD8ObEMuPD_q2a8

最终整个JWT字符串就是:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOiJhZG1pbiIsImV4cFRpbWUiOiIxNDk4Nzk1MjAwIn0.GsGmzc6su0imWlnoyaCexgq1jNurdD8ObEMuPD_q2a8

2.JWT生成及使用流程

  1. 用户导航到登录页,输入用户名、密码,进行登录
  2. 服务器验证登录鉴权,如果用户合法,根据用户的信息和服务器的规则生成JWT Token
  3. 服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
  4. 用户得到token,存在localStorage、cookie或其它数据存储形式中。
  5. 以后用户请求系统中的API时,在请求的header或者Cookie 中加入 X-Token 。
  6. 服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
  7. 用户取得结果

JWT工作流程图

3.JWT的JAVA API库

jwt的java开源库很多,如:java-jwt, jose4j,
nimbus-jose-jwt, jjwt。它们之间对加密方法支持会有些差别,具体可详见以下链接:https://jwt.io/#libraries-io。笔者本文demo采用jjwt进行演示。

4.JWT在Spring Cloud中应用

接下来我们直接上代码,来了解JWT在spring cloud中应用,以下只截取与JWT相关代码,关于spring cloud代码可参考其他资料。另一般在真实系统中,会把整个JWT的生成和校验会单独抽取成一个独立的鉴权中心服务,本文只是为演示使用,并未进行抽取深入。

核心代码

1.增加maven依赖(此处指列出JWT依赖jjwt,spring cloud相关依赖可查看其它资料)

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>

2.在eurekaclient项目的服务中增加生成token代码,此处是在登录时。

EurekaclientApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping("/login")
public String login(@RequestParam String name, @RequestParam String password) {
//判断用户名密码是否合法,合法否在进行token生成
// 令牌有效期30天
DateTime dt = new DateTime();
Date expiration = dt.plusDays(30).toDate();
// 生成令牌,参数可以自行组织
String accessToken = Jwts.builder().setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
.claim("accountId", name)
.claim("expTime", expiration)
.signWith(SignatureAlgorithm.HS256,
CoreConstants.SECRET).compact();
return accessToken;
}

3.在service-ribbon中增加服务调用代码

HelloController.java

1
2
3
4
@RequestMapping(value = "/login")
public String hi(@RequestParam String name, @RequestParam String password) {
return jwtService.loginService(name, password);
}

JWTService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class JWTService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "hiError")
public String loginService(String name, String password) {
return restTemplate.getForObject("http://service-hi/login?name=" + name + "&password=" + password, String.class);
}
public String hiError(String name, String password) {
return "hi," + name + ",sorry, error!";
}
}

4.在service-zuul项目中的过滤器MyFilter中添加如下代码

MyFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 具体业务逻辑
* @return
*/
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
logger.info(String.format("%s >>> %s", request.getMethod(), request.getRequestURL().toString()));
//此处可对请求方法进行刷选
if (!request.getRequestURI().contains("login")) {
// 先取Header中X-Token
String accessToken = request.getHeader(CoreConstants.X_TOKEN);
// 如果令牌为空, 再取Cookie中X-Token
if (accessToken == null || accessToken.isEmpty()) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (CoreConstants.X_TOKEN.equals(cookie.getName())) {
accessToken = cookie.getValue();
break;
}
}
}
}
// 如果令牌为空, 再取QueryString中X-Token
if (accessToken == null || accessToken.isEmpty()) {
accessToken = request.getParameter(CoreConstants.X_TOKEN);
}
if (accessToken == null || accessToken.isEmpty()) {
logger.error("token is empty");
context.setSendZuulResponse(false);
context.setResponseStatusCode(401);
try {
context.getResponse().getWriter().write("token is empty");
} catch (Exception e) {
}
return null;
} else {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(CoreConstants.SECRET).parseClaimsJws(accessToken);
// 获取用户Id
String accountId = claims.getBody().get("accountId").toString();
logger.debug("accountId = {}", accountId);
//根据accountId去获取相关信息, 本demo省略
// 过期时间
String expTime = claims.getBody().get("expTime").toString();
logger.debug("expTime = {}", expTime);
//对过期时间进行相关判断, 本demo省略
return null;
} catch (Exception ex) {
if (logger.isErrorEnabled()) {
logger.error(ExceptionUtils.getStackTrace(ex));
}
// header中令牌不对, 可能被篡改
logger.error("token is error");
context.setSendZuulResponse(false);
context.setResponseStatusCode(401);
try {
context.getResponse().getWriter().write("token is error");
} catch (Exception e) {
}
return null;
}
}
}
logger.debug("token is ok");
return null;
}

【注意】需要在service-zuul项目中的yml配置文件中添加一下配置,否则可能会出现com.netflix.zuul.exception.ZuulException:Forwarding error异常,导致服务无法正常调用

zuul:

ribbon-isolation-strategy: thread

host:

connect-timeout-millis: 10000

socket-timeout-millis: 60000

核心代码至此结束。

运行展示

依次启动eurekaserver, eurekaclient, service-ribbon, service-feign,service-zuul等项目。此时如直接调用后端服务,将会提示token is empty

如果随意带上X-Token, 会提示token is error,也无法正常调用服务

因此必须在正常登陆后,得到真正的token后,然后在调用服务时传入,才可以正常调用。 得到token

调用服务是传入token后,正常得到服务结果

代码下载:https://github.com/yzun/spring-cloud-study

参考链接:https://jwt.io/

http://blog.csdn.net/forezp/article/details/70148833

转载请标明出处:
版权归余正作者和Spring Cloud中国社区所有
本文出自Spring Cloud中国社区会员-余正