jwt简介

JWT是JSON Web Token的简称,它是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
和Cookie-Session的模式不同,JWT使用Token替换了SessionID的资源访问和状态保持。

jwt的组成

1.Header: 标题包含了令牌的元数据,并且在最小包含签名和/或加密算法的类型
2.Claims: Claims包含您想要签署的任何信息
3.JSON Web Signature (JWS): 在header中指定的使用该算法的数字签名和声明

jwt的认证过程

1.用户登录系统;
2.服务端验证,将认证信息通过指定的算法(例如HS256)进行加密,例如对用户名和用户所属角色进行加密,加密私钥是保存在服务器端的,将加密后的结果发送给客户端,加密的字符串格式为三个”.” 分隔的字符串 Token,分别对应头部、载荷与签名,头部和载荷都可以通过 base64 解码出来,签名部分不可以;
3.客户端拿到返回的 Token,存储到 local storage 或本地数据库;
4.下次客户端再次发起请求,将 Token 附加到 header 中;
5.服务端获取header中的Token,通过相同的算法对Token中的用户名和所属角色进行相同的加密验证,如果验证结果相同,则说明这个请求是正常的,没有被篡改。这个过程可以完全不涉及到查询 Redis 或其他存储;

jjwt

JJWT是一个提供端到端的JWT创建和验证的Java库。
JJWT的目标是最容易使用和理解用于在JVM上创建和验证JSON Web令牌(JWTs)的库。
JJWT是基于JWT、JWS、JWE、JWK和JWA RFC规范的Java实现。

jjwt安装

在pom.xml中配置jjwt的依赖即可:

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

创建签名秘钥

1
2
Key key = MacProvider.generateKey(); 
String compactJws = Jwts.builder().setSubject("Joe").signWith(SignatureAlgorithm.HS512, key).compact();

验证JWT

1
assert Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws).getBody().getSubject().equals("Joe");

示例

下面通过一个示例代码来说明如何使用。示例程序基于springboot。
本例代码在:https://gitee.com/qincd/my-test-projects下的jwt-demo模块。

jwt的操作工具类:

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
74
public class TokenMgr {
public static SecretKey generalKey() throws Base64DecodingException {
byte[] encodedKey = Base64.decode(Constant.JWT_SECERT);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}

/**
* 签发JWT
* @param id
* @param subject
* @param ttlMillis
* @return
* @throws Exception
*/
public static String createJWT(String id, String subject, long ttlMillis) throws Base64DecodingException {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate);
}
return builder.compact();
}

/**
* 验证JWT
* @param jwtStr
* @return
*/
public static CheckResult validateJWT(String jwtStr) {
CheckResult checkResult = new CheckResult();
Claims claims = null;
try {
claims = parseJWT(jwtStr);
checkResult.setSuccess(true);
checkResult.setClaims(claims);
} catch (ExpiredJwtException e) {
checkResult.setErrCode(Constant.JWT_ERRCODE_EXPIRE);
checkResult.setSuccess(false);
} catch (SignatureException e) {
checkResult.setErrCode(Constant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(Constant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
}

/**
*
* 解析JWT字符串
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}

}

jwt校验实体类:

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
public class CheckResult {
private boolean success;
private Claims claims;
private String errCode;

public boolean isSuccess() {
return success;
}

public void setSuccess(boolean success) {
this.success = success;
}

public Claims getClaims() {
return claims;
}

public void setClaims(Claims claims) {
this.claims = claims;
}

public String getErrCode() {
return errCode;
}

public void setErrCode(String errCode) {
this.errCode = errCode;
}
}

常量类:

1
2
3
4
5
public class Constant {
public static final String JWT_SECERT = "security";
public static final String JWT_ERRCODE_EXPIRE = "expire";
public static final String JWT_ERRCODE_FAIL = "fail";
}

JwtFilter:用于拦截/api/*请求

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
public class JwtFilter extends GenericFilter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// jwt验证
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 1.从Cookie获取jwt
String token = getTokenFromCookie(request);

if (StringUtils.isEmpty(token)) {
// 2.从headers中获取
token = request.getHeader("Authorization");
}
if (StringUtils.isEmpty(token)) {
// 3.从请求参数获取
token = request.getParameter("token");
}

if (StringUtils.isEmpty(token)) {
throw new ServletException("Missing or invalid token.");
}

CheckResult checkResult = TokenMgr.validateJWT(token);
if (checkResult.isSuccess()) {
request.setAttribute("claims",checkResult.getClaims());
filterChain.doFilter(servletRequest,servletResponse);
}
else {
if (checkResult.getErrCode().equals(Constant.JWT_ERRCODE_EXPIRE)) {
servletResponse.setCharacterEncoding("utf-8");
PrintWriter printWriter = servletResponse.getWriter();
printWriter.write("token过期,请重新登录");
printWriter.flush();
printWriter.close();
}
else if (checkResult.getErrCode().equals(Constant.JWT_ERRCODE_FAIL)) {
PrintWriter printWriter = servletResponse.getWriter();
servletResponse.setCharacterEncoding("utf-8");
printWriter.write("token验证失败!");
printWriter.flush();
printWriter.close();
}
}
}

private String getTokenFromCookie(HttpServletRequest request) {
String token = null;
Cookie[] cookies = request.getCookies();
int len = null == cookies ? 0:cookies.length;
if (len > 0) {
for (int i=0;i<cookies.length;i++) {
Cookie cookie = cookies[i];
if (cookie.getName().equals("token")) {
token = cookie.getValue();
break;
}
}
}

return token;
}

@Override
public void destroy() {

}
}

LoginController:

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
@Controller
public class LoginController {

@RequestMapping(value = "/login",method = RequestMethod.GET)
public String toLogin() {
return "/login";
}

@RequestMapping(value = "/login",method = RequestMethod.POST)
@ResponseBody
public Map<String,String> login(String username,String password,HttpServletResponse response) {
Map<String,String> result = new HashMap<String, String>(2);
if (username.equals("admin") && password.equals("123456")) {
String id= UUID.randomUUID().toString();
String subject = "{role:1,permission:[1,2,3]}";
try {
String token = TokenMgr.createJWT(id,subject,5*60*1000L);
result.put("token",token);
result.put("code","0");
} catch (Base64DecodingException e) {
result.put("code","1");
result.put("msg", "内部错误!");
}
}
else {
result.put("code","1");
result.put("msg","用户名或密码错误!");
}
return result;
}

}

ApiController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
@RequestMapping("/api")
public class ApiController {

@RequestMapping(value = "/test",method = RequestMethod.GET)
@ResponseBody
public Map<String,Object> test() {
Map<String,Object> map = new HashMap<String, Object>(4);
map.put("id",10000L);
map.put("name","张三");
map.put("age",99);
return map;
}
}

SpringBoot应用启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SpringBootApplication
public class WebApplication extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(WebApplication.class);
}

//过滤器
@Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new JwtFilter());
registrationBean.addUrlPatterns("/api/*");
return registrationBean;
}

public static void main( String[] args )
{
SpringApplication.run(WebApplication.class, args);
}

}

增加对jwtFilter的配置。

login.jsp:

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
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<c:set var="ctx" value="${pageContext.request.contextPath}"/>
<html>
<head>
<title>用户登录</title>
</head>
<body>
<div id="loginDiv">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="button" value="登录" onclick="login();">
</div>

<script type="text/javascript" src="${ctx}/js/jquery.js"></script>
<script>
function login() {
var t = {
username:$('input[name=username]').val(),
password:$('input[name=password]').val()
};

if (t.username == '' || t.password== '') {
alert('参数不全');
return;
}

$.post('${ctx}/login',t, function(r) {
if (r.code == 0) {
location.replace('${ctx}/api/test?token='+ r.token);
}
else {
alert(r.msg);
}
});
}
</script>
</body>
</html>

测试:
进入登录页面登录

这里用户名和秘密为admin和123456

输入后,点登录按钮,会进入到/api/test页面。

上面示例代码中token的有效期为5分钟,在5分钟内,可以在任何其他机器或浏览器输入登录后的地址,可以拿到返回结果。

如果超过5分钟再访问则会提示token过期。

示例工程参考:Web安全通讯之JWT的Java实现JWT 进阶 – JJWTJWT的Java使用 (JJWT)你的JWTs存储在哪里JWT(JSON WEB TOKEN)框架 JJWT 教程jjwt的github地址