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地址