Google Code Prettify

2017年8月3日 星期四

JWT 認證在 RESTful service 上的應用 (2) - RESTful service

上一篇 (JWT 認證在 RESTful service 上的應用 (1) - JWT 說明) 簡要說明了 JWT,這裡要用 spring 寫個 RESTful service,並用 JUnit 進行單元測試,這個程式很簡單,只是個 say hello 的 service,client 傳給 service 名字,例如 "Steven",那麼 service 會回覆 "Hello Steven"。先來看一下 RESTful 程式:
 1 @RestController
 2 @RequestMapping("/say")
 3 public class HelloController {
 4 
 5     @JwtValidation
 6     @RequestMapping(value = {"/hello/{name}"}, method = RequestMethod.GET)
 7     public ResponseEntity<?> hello(@PathVariable("name") String name) {
 8         
 9         return new ResponseEntity<HelloModel>(new HelloModel(name), HttpStatus.OK);
10     }
11 }
就如上所說,client 傳來了 name,service 端傳回一個 HelloModel 的 object,HelloModel 如下所示,只是在名稱前加上 Hello,因為是正常回覆,狀態設定為 OK (200)。特別注意第 5 行,這個 annotation 是我自己定義的,等一下我會用 spring AOP 的方式,針對有加 @JwtValidation 的 method 檢查 JWT token。
 1 @Data
 2 public class HelloModel implements Serializable {
 3     private String sentence;
 4     
 5     public HelloModel() { }
 6     
 7     public HelloModel(String name) {
 8         this.sentence = "Hello " + name;
 9     }
10 }
@JwtValidation 的程式碼如下:
1 @Retention(RetentionPolicy.RUNTIME)
2 @Target(ElementType.METHOD)
3 public @interface JwtValidation {
4 }
第 2 行指出,這個 annotation 只能用在 method。
spring AOP 不是這裡說明的重點,所以,直接看 @AspectJ 裡的程式怎麼檢查 JWT token。
 1 @Aspect
 2 @Log4j
 3 public class JwtValidator {
 4     @Value("${jwt.key}")
 5     protected String key;
 6     
 7     @Inject
 8     protected HttpServletRequest httpRequest;
 9     
10     //@Around("execution(* idv.steven.demo.rest.controller.*.*(..))")
11     @Around("@annotation(idv.steven.demo.JwtValidation)")
12     public ResponseEntity<?> validate(ProceedingJoinPoint jp) {
13         String token = httpRequest.getHeader("Authorization");
14         
15         if (!StringUtils.isEmpty(token)) {
16             try {
17                 token = token.replace("Bearer ", "");
18                 
19                 //當 client 與 server 的 key 不同,會產生 exception。
20                 Jws<Claims> jws = Jwts.parser()         
21                            .setSigningKey(DatatypeConverter.parseBase64Binary(key))
22                            .parseClaimsJws(token);
23                 
24                 Claims body = jws.getBody(); //payload
25                 Date issuedAt = body.getIssuedAt();
26                 
27                 if (issuedAt == null) {
28                     return new ResponseEntity<ResponseModel>(new ResponseModel(), HttpStatus.REQUEST_TIMEOUT);
29                 }
30                 else {
31                     //由 client 發出 request 到 server 端,最多可以歷經 30 秒! (各機器不一定有校時,要有一點容許誤差!)
32                     int diffSec = DateUtil.diffSec(issuedAt);
33                     if (diffSec > 30) {
34                         return new ResponseEntity<ResponseModel>(new ResponseModel(), HttpStatus.REQUEST_TIMEOUT);
35                     }
36                 }
37             }
38             catch (Exception ex) {
39                 log.error(ex.getMessage(), ex);
40                 return new ResponseEntity<ResponseModel>(new ResponseModel(), HttpStatus.UNAUTHORIZED);
41             }
42         }
43         else {
44             log.error("JWT token not found");
45             return new ResponseEntity<ResponseModel>(new ResponseModel(), HttpStatus.UNAUTHORIZED);
46         }
47         
48         try {
49             return (ResponseEntity<?>) jp.proceed();
50         } catch (Throwable e) {
51             return new ResponseEntity<ResponseModel>(new ResponseModel(), HttpStatus.SERVICE_UNAVAILABLE);
52         }
53     }
54 }
第 11 行指出,當 method 前加了 @JwtValidation,該 method 就會被這個 validate "包圍" (Around),於檢查完確定沒問題了,才執行第 49 行,第 49 行就是去執行被包圍的那個 method,在這裡指的就是最前面那個 hello method。在這裡可以發現,只要有任何錯誤,都會直接回傳帶有狀態碼非 HttpStatus.OK (200) 的其它值,這是 RESTful service 的習慣,充份利用 http 的特性,在 http 的狀態碼中挑一個接近於我們所要表達的錯誤,將它傳回給 client,完全不需要再自行定義任何狀態碼。

第 4、5 行,這是 spring framework 提供的功能,spring 會從設定檔 (application.properties 或其它 properties 檔) 載入 jwt.key 的值到 key 這個變數。當然,我在某個標有 @Configuration 的類別前會有 @PropertySource("classpath:application.properties") 這樣的標注。

第 13 行,從 http 的 header 中的 Authorization 取出 JWT token,還記得上一篇有提到,在 token 前會有 "Bearer " 的字串,在第 17 行將它移除,第 20 ~ 22 行是用 key 驗證 token 中的 header、payload 是否被竄改,如果沒有,將所有欄位的值放到 jws 那個變數裡,方便程式人員處理,這裡使用的是 JJWT 這個 open source,算是目前比較流行的。在 24 ~ 46 行就是在檢查 token 裡的一些值,不正確回覆非 HttpStatus.OK (200) 的值,這裡其實只檢查有沒有逾時。

接下來看一下測試程式怎麼寫 …
 1     @Test
 2     public void hello() {
 3         String url = "http://localhost:8080/demo/say/hello/Steven";
 4         
 5         Date iat = Calendar.getInstance().getTime();
 6         
 7         String compactJws = Jwts.builder()
 8                   .setIssuedAt(iat)
 9                   .signWith(SignatureAlgorithm.HS512, DatatypeConverter.parseBase64Binary(key))
10                   .compact();
11         
12         HttpHeaders headers = new HttpHeaders();
13         headers.set("Authorization", "Bearer " + compactJws);
14         
15         JSONObject parm = new JSONObject();
16         HttpEntity<JSONObject> entity = new HttpEntity<JSONObject>(parm, headers);
17         
18         try {
19             ResponseEntity<?> response = rest.exchange(url, HttpMethod.GET, entity, HelloModel.class);
20             HelloModel body = (HelloModel) response.getBody();
21             System.out.println(body.getSentence());
22             System.out.println("status code = " + response.getStatusCode());
23         }
24         catch (HttpClientErrorException ex) {
25             System.out.println("status code = " + ex.getStatusCode());
26         }
27     }
第 3 行,在網址後帶個名字當參數,這裡帶入 Steven,如果正確的話,應該會在第 21 行輸出 Hello Steven。第 7 ~ 10 行,用 JJWT 產生 JWT token,這裡的 key 因為我是以 Base64 儲存,所以要先轉成 binary,簽章演算法指定為 HS512 (SHA-512),產生後於 12 ~ 13 行放入 http header 的 Authorization 欄位,依慣例在 token 前加上 "Bearer "。第 19 行的 rest 是 spring 提供的 RestTemplate,用它來呼叫 RESTful service,如果傳回的狀態值為 OK (200),會執行 20 ~ 22 行,否則會產生 exception,執行 25 行印出帶回的 Http Status Code,這樣 client 就可以知道錯誤原因。




沒有留言:

張貼留言