├── 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 | ![](https://www.cnblogs.com/images/cnblogs_com/xz816111/786501/o_jwt.png) 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 | ![](https://www.cnblogs.com/images/cnblogs_com/xz816111/786501/o_jwt-decode.png) 64 | 65 | 将上面三部分都编码后再合在一起就得到了JWT。 66 | 67 | 需要注意的是,**JWT的内容并不是加密的,只是简单的`Base64`编码。** 也就是说,JWT一旦泄露,里面的信息可以被轻松获取,因此不应该用JWT保存任何敏感信息。 68 | 69 | 70 | # JWT是怎样工作的 71 | 72 | ![](https://www.cnblogs.com/images/cnblogs_com/xz816111/786501/o_jwt-work.png) 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 | } --------------------------------------------------------------------------------