├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── devhao
│ │ └── springdojo
│ │ ├── Application.java
│ │ ├── filter
│ │ └── JwtFilter.java
│ │ ├── resource
│ │ ├── ApiResource.java
│ │ └── RegistrationResource.java
│ │ └── service
│ │ └── JwtService.java
└── resources
│ └── application.properties
└── test
└── java
└── com
└── devhao
└── springdojo
├── ApplicationTests.java
└── service
└── JwtServiceTest.java
/README.md:
--------------------------------------------------------------------------------
1 | # 什么是JWT
2 |
3 | JWT(JSON Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各个系统之间用JSON作为对象安全地传输信息,并且可以保证所传输的信息不会被篡改。
4 |
5 | JWT通常有两种应用场景:
6 |
7 | - 授权。这是最常见的JWT使用场景。一旦用户登录,每个后续请求将包含一个JWT,作为该用户访问资源的令牌。这也是此文章讨论的内容。
8 | - 信息交换。可以利用JWT在各个系统之间安全地传输信息,JWT的特性使得接收方可以验证收到的内容是否被篡改。
9 |
10 | # JWT的结构
11 |
12 |
13 | 
14 |
15 | JWT由三部分组成,用`.`分割开。
16 |
17 | ## Header
18 |
19 | 第一部分为`Header`,通常由两部分组成:令牌的类型,即JWT,以及所使用的加密算法。
20 |
21 | ```
22 | {
23 | "alg": "HS256",
24 | "typ": "JWT"
25 | }
26 | ```
27 |
28 | `Base64`编码(非加密)后,就变成了:
29 | ```
30 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
31 | ```
32 |
33 |
34 | ## Payload
35 | 第二部分为`Payload`,里面可以放置自定义的信息,以及过期时间、发行人等。
36 |
37 | ```
38 | {
39 | "sub": "1234567890",
40 | "name": "John Doe",
41 | "iat": 1516239022
42 | }
43 | ```
44 |
45 | `Base64`编码(非加密)后,就变成了:
46 | ```
47 | eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
48 | ```
49 |
50 | ## Signature
51 |
52 | 第三部分为`Signature`,计算此签名需要四部分信息:
53 |
54 | - `Header`里的算法信息
55 | - `Header`
56 | - `Payload`
57 | - 一个自定义的秘钥
58 |
59 | 接收到JWT后,利用相同的信息再计算一次签名,然年与JWT中的签名对比,如果不相同则说明JWT中的内容被篡改。
60 |
61 | ## 解码后的JWT
62 |
63 | 
64 |
65 | 将上面三部分都编码后再合在一起就得到了JWT。
66 |
67 | 需要注意的是,**JWT的内容并不是加密的,只是简单的`Base64`编码。** 也就是说,JWT一旦泄露,里面的信息可以被轻松获取,因此不应该用JWT保存任何敏感信息。
68 |
69 |
70 | # JWT是怎样工作的
71 |
72 | 
73 |
74 | 1. 应用程序或客户端向授权服务器请求授权。这里的授权服务器可以是单独的一个应用,也可以和API集成在同一个应用里。
75 | 2. 授权服务器向应用程序返回一个JWT。
76 | 3. 应用程序将JWT放入到请求里(通常放在`HTTP`的`Authorization`头里)
77 | 4. 服务端接收到请求后,验证JWT并执行对应逻辑。
78 |
79 | # 在JAVA里使用JWT
80 |
81 |
82 | ## 引入依赖
83 |
84 | ```
85 |
86 | io.jsonwebtoken
87 | jjwt
88 |
89 | ```
90 |
91 | 这里使用了一个叫JJWT(Java JWT)的库。
92 |
93 | ## JWT Service
94 |
95 | ### 生成JWT
96 |
97 | ```
98 | public String generateToken(String payload) {
99 | return Jwts.builder()
100 | .setSubject(payload)
101 | .setExpiration(new Date(System.currentTimeMillis() + 10000))
102 | .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
103 | .compact();
104 | }
105 | ```
106 |
107 | - 这里设置过期时间为10秒,因此生成的JWT只在10秒内能通过验证。
108 | - 需要提供一个自定义的秘钥。
109 |
110 | ### 解码JWT
111 |
112 | ```
113 | public String parseToken(String jwt) {
114 | return Jwts.parser()
115 | .setSigningKey(SECRET_KEY)
116 | .parseClaimsJws(jwt)
117 | .getBody()
118 | .getSubject();
119 | }
120 | ```
121 |
122 | - 解码时会检查JWT的签名,因此需要提供秘钥。
123 |
124 | ### 验证JWT
125 |
126 | ```
127 | public boolean isTokenValid(String jwt) {
128 | try {
129 | parseToken(jwt);
130 | } catch (Throwable e) {
131 | return false;
132 | }
133 | return true;
134 | }
135 | ```
136 |
137 | - `JJWT`并没有提供判断JWT是否合法的方法,但是在解码非法JWT时会抛出异常,因此可以通过捕获异常的方式来判断是否合法。
138 |
139 | ## 注册/登录
140 |
141 | ```
142 | @GetMapping("/registration")
143 | public String register(@RequestParam String username, HttpServletResponse response) {
144 | String jwt = jwtService.generateToken(username);
145 | response.setHeader(JWT_HEADER_NAME, jwt);
146 |
147 | return String.format("JWT for %s :\n%s", username, jwt);
148 | }
149 | ```
150 |
151 | - 需要为还没有获取到JWT的用户提供一个这样的注册或者登录入口,来获取JWT。
152 | - 获取到响应里的JWT后,要在后续的请求里包含JWT,这里放在请求的`Authorization`头里。
153 |
154 | ## 验证JWT
155 |
156 | ```
157 | @Override
158 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
159 | HttpServletRequest httpServletRequest = (HttpServletRequest) request;
160 | HttpServletResponse httpServletResponse = (HttpServletResponse) response;
161 |
162 | String jwt = httpServletRequest.getHeader(JWT_HEADER_NAME);
163 | if (WHITE_LIST.contains(httpServletRequest.getRequestURI())) {
164 | chain.doFilter(request, response);
165 | } else if (isTokenValid(jwt)) {
166 | updateToken(httpServletResponse, jwt);
167 | chain.doFilter(request, response);
168 | } else {
169 | httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
170 | }
171 | }
172 |
173 | private void updateToken(HttpServletResponse httpServletResponse, String jwt) {
174 | String payload = jwtService.parseToken(jwt);
175 | String newToken = jwtService.generateToken(payload);
176 | httpServletResponse.setHeader(JWT_HEADER_NAME, newToken);
177 | }
178 | ```
179 |
180 | - 将验证操作放在`Filter`里,这样除了登录入口,其它的业务代码将感觉不到JWT的存在。
181 | - 将登录入口放在`WHITE_LIST`里,跳过对这些入口的验证。
182 | - **需要刷新JWT**。如果JWT是合法的,那么应该用同样的`Payload`来生成一个新的JWT,这样新的JWT就会有新的过期时间,用此操作来刷新JWT,以防过期。
183 | - 如果使用`Filter`,那么刷新的操作要在调用`doFilter()`之前,因为调用之后就无法再修改`response`了。
184 |
185 | ## API
186 |
187 | ```
188 | private final static String JWT_HEADER_NAME = "Authorization";
189 |
190 | @GetMapping("/api")
191 | public String testApi(HttpServletRequest request, HttpServletResponse response) {
192 | String oldJwt = request.getHeader(JWT_HEADER_NAME);
193 | String newJwt = response.getHeader(JWT_HEADER_NAME);
194 |
195 | return String.format("Your old JWT is:\n%s \nYour new JWT is:\n%s\n", oldJwt, newJwt);
196 | }
197 | ```
198 |
199 | 这时候API就处于JWT的保护下了。API可以完全不用感知到JWT的存在,同时也可以主动获取JWT并解码,以得到JWT里的信息。如上所示。
200 |
201 |
202 | # 尾注
203 |
204 | - 完整的DEMO可以在这里找到:https://github.com/Beginner258/jwt-demo
205 | - 参考资料:https://jwt.io/
206 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.devhao
7 | spring-dojo
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | jwt-demo
12 | Demo project for Spring Boot
13 |
14 |
15 | org.springframework.boot
16 | spring-boot-starter-parent
17 | 2.0.4.RELEASE
18 |
19 |
20 |
21 |
22 | UTF-8
23 | UTF-8
24 | 1.8
25 |
26 |
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-aop
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-web
35 |
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-starter-test
40 | test
41 |
42 |
43 |
44 | io.jsonwebtoken
45 | jjwt
46 | 0.9.1
47 |
48 |
49 |
50 |
51 |
52 |
53 | org.springframework.boot
54 | spring-boot-maven-plugin
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/main/java/com/devhao/springdojo/Application.java:
--------------------------------------------------------------------------------
1 | package com.devhao.springdojo;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.boot.web.servlet.ServletComponentScan;
6 |
7 | @SpringBootApplication
8 | @ServletComponentScan
9 | public class Application {
10 |
11 | public static void main(String[] args) {
12 | SpringApplication.run(Application.class, args);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/devhao/springdojo/filter/JwtFilter.java:
--------------------------------------------------------------------------------
1 | package com.devhao.springdojo.filter;
2 |
3 | import com.devhao.springdojo.service.JwtService;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.stereotype.Component;
6 |
7 | import javax.servlet.*;
8 | import javax.servlet.annotation.WebFilter;
9 | import javax.servlet.http.HttpServletRequest;
10 | import javax.servlet.http.HttpServletResponse;
11 | import java.io.IOException;
12 | import java.util.Collections;
13 | import java.util.List;
14 |
15 | @WebFilter(urlPatterns = "/*")
16 | @Component
17 | public class JwtFilter implements Filter {
18 | private static final List WHITE_LIST = Collections.singletonList("/registration");
19 | private static final String JWT_HEADER_NAME = "Authorization";
20 | private JwtService jwtService;
21 |
22 | @Override
23 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
24 | HttpServletRequest httpServletRequest = (HttpServletRequest) request;
25 | HttpServletResponse httpServletResponse = (HttpServletResponse) response;
26 |
27 | String jwt = httpServletRequest.getHeader(JWT_HEADER_NAME);
28 | if (WHITE_LIST.contains(httpServletRequest.getRequestURI())) {
29 | chain.doFilter(request, response);
30 | } else if (isTokenValid(jwt)) {
31 | updateToken(httpServletResponse, jwt);
32 | chain.doFilter(request, response);
33 | } else {
34 | httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
35 | }
36 | }
37 |
38 | private void updateToken(HttpServletResponse httpServletResponse, String jwt) {
39 | String payload = jwtService.parseToken(jwt);
40 | String newToken = jwtService.generateToken(payload);
41 | httpServletResponse.setHeader(JWT_HEADER_NAME, newToken);
42 | }
43 |
44 | private boolean isTokenValid(String token) {
45 | return jwtService.isTokenValid(token);
46 | }
47 |
48 |
49 | @Override
50 | public void init(FilterConfig filterConfig) throws ServletException {
51 |
52 | }
53 |
54 | @Override
55 | public void destroy() {
56 |
57 | }
58 |
59 | @Autowired
60 | public void setJwtService(JwtService jwtService) {
61 | this.jwtService = jwtService;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/com/devhao/springdojo/resource/ApiResource.java:
--------------------------------------------------------------------------------
1 | package com.devhao.springdojo.resource;
2 |
3 | import org.springframework.web.bind.annotation.GetMapping;
4 | import org.springframework.web.bind.annotation.RestController;
5 |
6 | import javax.servlet.http.HttpServletRequest;
7 | import javax.servlet.http.HttpServletResponse;
8 |
9 | @RestController
10 | public class ApiResource {
11 | private final static String JWT_HEADER_NAME = "Authorization";
12 |
13 | @GetMapping("/api")
14 | public String testApi(HttpServletRequest request, HttpServletResponse response) {
15 | String oldJwt = request.getHeader(JWT_HEADER_NAME);
16 | String newJwt = response.getHeader(JWT_HEADER_NAME);
17 |
18 | return String.format("Your old JWT is:\n%s \nYour new JWT is:\n%s\n", oldJwt, newJwt);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/devhao/springdojo/resource/RegistrationResource.java:
--------------------------------------------------------------------------------
1 | package com.devhao.springdojo.resource;
2 |
3 | import com.devhao.springdojo.service.JwtService;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.RequestParam;
7 | import org.springframework.web.bind.annotation.RestController;
8 |
9 | import javax.servlet.http.HttpServletResponse;
10 |
11 | @RestController
12 | public class RegistrationResource {
13 | private final static String JWT_HEADER_NAME = "Authorization";
14 |
15 | private JwtService jwtService;
16 |
17 | @GetMapping("/registration")
18 | public String register(@RequestParam String username, HttpServletResponse response) {
19 | String jwt = jwtService.generateToken(username);
20 | response.setHeader(JWT_HEADER_NAME, jwt);
21 |
22 | return String.format("JWT for %s :\n%s", username, jwt);
23 | }
24 |
25 | @Autowired
26 | public void setJwtService(JwtService jwtService) {
27 | this.jwtService = jwtService;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/devhao/springdojo/service/JwtService.java:
--------------------------------------------------------------------------------
1 | package com.devhao.springdojo.service;
2 |
3 |
4 | import io.jsonwebtoken.Jwts;
5 | import io.jsonwebtoken.SignatureAlgorithm;
6 | import org.springframework.stereotype.Component;
7 |
8 | import java.util.Date;
9 |
10 | @Component
11 | public class JwtService {
12 | private static final String SECRET_KEY = "This Is Secret Key";
13 | private static final long MILLIS_PER_SECOND = 1000;
14 | private static final long TIME_OUT_SECOND = 60 * MILLIS_PER_SECOND;
15 |
16 | public String generateToken(String payload) {
17 | return Jwts.builder()
18 | .setSubject(payload)
19 | .setExpiration(new Date(System.currentTimeMillis() + TIME_OUT_SECOND))
20 | .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
21 | .compact();
22 | }
23 |
24 | public String parseToken(String jwt) {
25 | return Jwts.parser()
26 | .setSigningKey(SECRET_KEY)
27 | .parseClaimsJws(jwt)
28 | .getBody()
29 | .getSubject();
30 | }
31 |
32 | public boolean isTokenValid(String jwt) {
33 | try {
34 | parseToken(jwt);
35 | } catch (Throwable e) {
36 | return false;
37 | }
38 | return true;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyrepo/jwt-demo/0329c2fc47872512f877d0e8f4ebf99a9dba6151/src/main/resources/application.properties
--------------------------------------------------------------------------------
/src/test/java/com/devhao/springdojo/ApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.devhao.springdojo;
2 |
3 | import org.junit.Test;
4 | import org.junit.runner.RunWith;
5 | import org.springframework.boot.test.context.SpringBootTest;
6 | import org.springframework.test.context.junit4.SpringRunner;
7 |
8 | @RunWith(SpringRunner.class)
9 | @SpringBootTest
10 | public class ApplicationTests {
11 |
12 | @Test
13 | public void contextLoads() {
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/java/com/devhao/springdojo/service/JwtServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.devhao.springdojo.service;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.hamcrest.core.Is.is;
6 | import static org.junit.Assert.assertThat;
7 |
8 |
9 | public class JwtServiceTest {
10 | private JwtService jwtService = new JwtService();
11 |
12 | @Test
13 | public void shouldParseToken() {
14 | String payload = "test";
15 | String token = jwtService.generateToken(payload);
16 |
17 | assertThat(jwtService.parseToken(token), is(payload));
18 | }
19 |
20 | @Test
21 | public void shouldValidateValidToken() {
22 | String payload = "test";
23 | String token = jwtService.generateToken(payload);
24 |
25 | assertThat(jwtService.isTokenValid(token), is(true));
26 | }
27 |
28 | @Test
29 | public void shouldValidateInvalidToken() {
30 | String payload = "test";
31 | String token = jwtService.generateToken(payload) + "hack";
32 |
33 | assertThat(jwtService.isTokenValid(token), is(false));
34 | }
35 | }
--------------------------------------------------------------------------------