Google Code Prettify

2017年8月10日 星期四

Gradle: 編譯、測試 web 專案

Java 專案的編譯、測試、部署,從十幾年前很流行的 ant,到現在則是以 Maven 及 Gradle 最流行,這篇先簡單的介紹怎麼用 gradle 將 web 專案打包成 war 檔。
  • 安裝 Gradle
請到 Gradle 的官網下載 Gradle,並依上面步驟安裝設定好環境。
  • build.gradle


如果是用 eclipse 開發,在專案的根目錄下新增一個文字檔,檔名為 build.gradle,這個檔名不是強迫非取這個名稱不可,只不過這是預設的檔名,這一篇希望快速的用 gradle 來編譯、測試及包裝 war 檔,一切都採預設值。接下來還有一些目錄也都是 gradle 預設的,以後有空會說明怎麼使用非預設的目錄,但是這邊我們把程式都放到預設目錄,現在說明這些目錄的用處 (目錄當然要自己建立啦~)。
  1. src/main: 網站相關的檔案,像是 java、jsp、html、*.properties、*.xml ... 都放在這個目錄下的子目錄。
  2. src/main/java: 顧名思義,這裡是放 java 程式碼的地方。
  3. src/main/webapp: 這個目錄就相當於一般我們用 eclipse 產生一個 web project 時,eclipse 自動幫我們產生的 WebContent,這個目錄在部署後就會相當於網站的根目錄。
  4. src/test/java: 測試程式放這個目錄下,只要裡面有測試程式,每次執行 gradle 時,在 compile 並包裝成 war 這些工作完成後,gradle 還會來執行這些測試程式產生測試報告。
接下來直接看一個簡單的 build.gradle 吧 ~
 1 apply plugin: 'java'
 2 apply plugin: 'war'
 3 
 4 sourceCompatibility = 1.8
 5 version = '1.0'
 6 
 7 sourceSets {
 8     main {
 9         java {
10             srcDirs = ['src/main/java']
11         }
12         resources {
13             srcDirs = ['config']
14         }
15     }
16     test {
17         java {
18             srcDirs = ['src/test/java']
19         }
20     }
21 }
22 
23 buildDir = 'out'
24 
25 repositories {     
26      maven { url "http://maven.springframework.org/milestone" }
27      maven { url "http://repo.maven.apache.org/maven2" }
28      maven { url "http://repo1.maven.org/maven2/" }
29      maven { url "http://amateras.sourceforge.jp/mvn/" }
30      mavenCentral()
31 }
32 
33 configurations.all {
34     exclude group: 'org.freemarker', module: 'freemarker'
35     exclude group: 'javax.el', module: 'el-api'
36     exclude group: 'xerces', module: 'xercesImpl'
37     exclude group: 'xml-apis', module: 'xml-apis'
38     exclude group: 'jboss', module: 'javassist'
39     exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-core'
40     exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-el'
41     exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-websocket'
42     exclude group: 'org.apache.tomcat', module: 'tomcat-jdbc'
43     exclude group: 'org.apache.tomcat', module: 'tomcat-juli'
44     exclude group: 'org.apache.ant', module: 'ant'
45     
46     resolutionStrategy {
47         force group: 'org.apache.ant', name: 'ant', version: '1.10.1'
48     }
49 }
50 
51 dependencies {
52     def tomcatVersion = '9.0.0.M22'
53     def springVersion = '4.3.10.RELEASE'
54     def springBootVersion = '1.5.4.RELEASE'
55     def hibernateVersion = '5.2.10.Final'
56     def hibernateValidatorVersion = '6.0.1.Final'
57     
58     compile fileTree(dir: 'library', include: ['*.jar'])
59     
60     testCompile group: 'junit', name: 'junit', version: '4.+'
61     
62     //javax
63     compile group: 'javax.inject', name: 'javax.inject', version: '1'
64     compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
65     compile group: 'javax.activation', name: 'activation', version: '1.1.1'
66     
67     //spring framework
68     compile group: 'org.springframework', name: 'spring-context', version: "$springVersion"
69     compile group: 'org.springframework', name: 'spring-test', version: "$springVersion"
70 
71     //spring boot
72     compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: "${springBootVersion}"
73     
74     //Others
75     compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.7.0'
76 }
簡要說明如下:
  1. apply plugin: 引入插件,這裡引入 java 及 war 兩個插件,這樣就可以編譯、測試並包裝 war 檔了 (一般 console、desktop 程式不需要引入 war 這個插件)。
  2. sourceCompatibility: 指出編譯要用的 JDK 版本,這裡指出是要用 1.8 版。
  3. version: 設定我們自己的這個專案的版本編號
  4. sourceSets: 裡面比較值的注意的是 resources,設定檔我們是放在專案的根目錄下的 config 目錄裡,所以 12~14 是必要的,不然 gradle 不會知道要把這個目錄下的檔案也包裝進 war。至於 main/java、test/java 這兩個設定因為值都是 gradle 的預設值,在這裡是可省略的。
  5. buildDir: 指出編譯後的產出要放到那個目錄下。
  6. repositories: 指出 gradle 要到那些網站抓 jar 檔。
  7. configurations.all: gradle 抓 jar 檔的時候,會把相依的 jar 檔一併抓下來,不然,我們要找到所有相依 jar 檔會浪費非常大量年輕寶貴的生命,但是,有時候我們不想要其中的某些 jar 檔時,可以在這裡以 exclude group 將它排除,或是 gradle 抓下來的 jar 版本不是我們想要的,可以用 force group 強制限定版本。
  8. dependencies: 這是一般人最熟悉的部份,把我們專案需要的 jar 檔寫在這裡,有些 framework 會有非常多的 jar 檔,版號又一樣,那麼,可以先用 def 定義版號的變數,省去每次換版本的麻煩。
  9. compile fileTree: 有一些 jar 檔不是到網路上的 jar repository 裡抓的,是放在自己的電腦某處,像這裡,這類的 jar 檔是放在專案根目錄的 library 目錄下,所以要這個指令指出來。
  10. testCompile group: 指出測試時要用的 framework,這裡使用的是 JUnit 4.+ 版。
程式寫好,build.gradle 也準備好了,就在專案的目錄下打入 gradle build 指令,build 是 java 插件提供的一個任務 (task),這個任務會以正確的順序編譯、測試和打包。最後結果產生後應該會在 buildDir 指定的目錄下,看到如下的結果:
  • classes: java compile 後產生的 class 檔,都放在這個目錄下。
  • libs: war 檔放在這裡。
  • resources: 上面 build.gradle 中指定的資源檔會放在這個目錄下。
  • reports、test-results: 這裡放的是測試報告,reports 放的是每個測試的結果,test-results 則有每個測試詳細資訊,有錯誤時可以來這看看是什麼錯誤。


2017年8月6日 星期日

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

在往下看之前,最好先復習一下「JWT 認證在 RESTful service 上的應用 (1) - JWT 說明」,確定 JWT 協定的規範,接下來我們會在 Angular 中製成 JWT 的 token 傳給 service 端。

這個程式會以如下的流程執行,作為 Client 端的 Angular 以帳密登入,經 service 端驗證通過後,會接收到一個 JWT 的 key,之後要 call service,都要用這個 key 產生 token 放入 header 以供認證。

  • 環境說明
請先在 Angular 專案中安裝下面兩個 package。
  1. angular2-jwt: 用來傳送 jwt token (npm install angular2-jwt)
  2. crypto-js: 用來加密及處理 Base64 字串 (npm install crypto-js)
安裝好之後,於 app.module.ts 中加入下面紅色部份的程式,這是 angular2-jwt 初始化所需要的。注意看一下第 4 行,這表示在 angular 送出 request 給 service 前,會從 sessionStorage 中的 'token' 取得 JWT token 傳給 service,有些人可能會存在 localStorage,這沒有強制規定,可自行修改,放在 sessionStorage 中也不一定要取名 'token'。
 1 export function authHttpServiceFactory(http: Http, options: RequestOptions) {
 2   return new AuthHttp(new AuthConfig({
 3         tokenName: 'token',
 4         tokenGetter: (() => sessionStorage.getItem('token')),
 5         globalHeaders: [{'Content-Type':'application/json'}],
 6     }), http, options);
 7 }
 8 
 9 @NgModule({
10   declarations: [
11     AppComponent,
12     LoginComponent,
13     HelloComponent
14   ],
15   imports: [
16     BrowserModule,
17     FormsModule,
18     HttpModule,
19     RouterModule.forRoot(appRoutes)
20   ],
21   providers: [
22       AuthenticationService,
23       HelloService,
24       {
25         provide: AuthHttp,
26         useFactory: authHttpServiceFactory,
27         deps: [Http, RequestOptions]
28       }
29   ],
30   bootstrap: [AppComponent]
31 })
32 export class AppModule { }
  • service 端程式修改
上一篇的 RESTful service 程式只寫了一個 say hello 的 controller,為了完成上面循序圖的流程,這裡增加一個 AuthenticationController 並提一個 login method,程式如下:
 1 @RestController
 2 @RequestMapping("/auth")
 3 public class AuthenticationController {
 4     @Value("${jwt.key}")
 5     protected String jwtKey;
 6     
 7     @RequestMapping(value = {"/login/{username}/{password}"}, method = RequestMethod.GET)
 8     public ResponseEntity<?> login(@PathVariable("username") String username, @PathVariable("password") String password) {
 9         
10         //檢查使用的帳密是否正確
11         
12         return new ResponseEntity<ResponseModel>(new ResponseModel(jwtKey), HttpStatus.OK);
13     }
14 }
檢查帳密的過程這裡省略了,直接回傳 JWT 加密的 key,上面程式傳回值時,是傳回一個 ResponseModel,程式碼如下,以 ResponseEntity 的方式傳回,spring 會將它處理成 JSON 格式。
 1 @Data
 2 public class ResponseModel implements Serializable {
 3     private String msg;
 4     
 5     public ResponseModel() { }
 6     
 7     public ResponseModel(String msg) {
 8         this.msg = msg;
 9     }
10 }
  • angular
在 angular 端,與 RESTful service 溝通的 client 程式,我習慣寫成 service,讓 component 呼叫,這裡先看一下呼叫 AuthenticationController 中 login method 的 client 怎麼寫。
 1 const SERVICE_URL = 'http://localhost:8080/demo/auth/';
 2 
 3 @Injectable()
 4 export class AuthenticationService {
 5     constructor(private http: Http) {
 6     }
 7 
 8     login(username: string, password: string): Observable<any[]> {
 9         
10         return this.http.get(SERVICE_URL + 'login/' + username + '/' + password)
11             .map(resp => resp.json())
12             .catch(this.handleError);
13     }
14 
15     private handleError(error: any) {
16         
17         return Observable.throw('login fail');
18     }
19 }
紅色部份即是傳給 service 端帳密,service 的回應會傳到 resp。登入的畫面如下,輸入帳密,按【登入】。

程式碼如下:
 1 @Component({
 2   selector: 'app-login',
 3   templateUrl: './login.component.html',
 4   styleUrls: ['./login.component.css']
 5 })
 6 export class LoginComponent implements OnInit {
 7     edited: boolean = true;
 8     username: string;
 9     password: string;
10     message: string;
11 
12     constructor(private router: Router, private authService: AuthenticationService) { }
13 
14     ngOnInit() {
15     }
16 
17     onLogin(event) {
18         this.authService.login(this.username, this.password)
19             .subscribe(
20                 data => {
22                     let respModel:ResponseModel = new ResponseModel(data);
24                     sessionStorage.setItem('jwtKey', respModel.msg);
26                     this.edited = false;
27                     this.router.navigate(['/hello']);
28                 }
29                 ,
30                 error => {
31                     console.log(error.message);
32                     this.message = '帳密錯誤';
33                 }
34             );
35     }
36 }
紅色部份即是當使用者按下登入觸發 onLogin 事件,呼叫 authService 的 login 後傳回值,在取得裡面的 JWT key 後將它放入 sessionStorage,畫面則導向 /hello 下一頁。下一頁的畫面如下:
很簡單的輸入名字後按【say Hello】,就會傳回值,例如輸入 Steven,就會傳回 Hello Steven,程式碼如下:
 1 @Component({
 2   selector: 'app-hello',
 3   templateUrl: './hello.component.html',
 4   styleUrls: ['./hello.component.css']
 5 })
 6 export class HelloComponent implements OnInit {
 7     edited: boolean = false;
 8     name: string;
 9     message: string;
10 
11     constructor(private router: Router, private helloService: HelloService) { }
12 
13     ngOnInit() {
14         let jwtKey:any = sessionStorage.getItem('jwtKey');
15         if (jwtKey == undefined || jwtKey == null) {
16             this.edited = false;
17             this.router.navigate(['/']);
18         }
19         else {
20             this.edited = true;
21         }
22     }
23 
24     onSay() {
25         this.helloService.say(this.name)
26             .subscribe(
27                 data => {
29                     let respModel:ResponseModel = new ResponseModel(data);
30                     this.message = respModel.msg;
31                     this.edited = false;
32                 }
33                 ,
34                 error => {
35                     this.message = 'call service fail';
36                 }
37             );
38     }
39 }
紅色部份是呼叫 helloService 的 say 後,傳回打招呼的字串,然後顯示出來在畫面。現在來看最重要的 helloService 怎麼寫,這裡就會根據第一篇講到的 JWT 規範,實作出 JWT token。
 1 import * as CryptoJS from 'crypto-js';
 2 
 3 const SERVICE_URL = 'http://localhost:8080/demo/say/';
 4 
 5 @Injectable()
 6 export class HelloService {
 7 
 8     constructor(private authHttp: AuthHttp) {
 9     }
10 
11     say(name: string): Observable<any[]> {
12         this.genToken();
13 
14         return this.authHttp.get(SERVICE_URL + 'hello/' + name)
15             .map(resp => resp.json());
16     }
17 
18     private genToken() {
19         let now:number = new Date().getTime();
20         
21         let jwtKey: string = sessionStorage.getItem('jwtKey');
22         
23         let header:string = '{\"alg\":\"HS512\",\"typ\":\"JWT\"}';
24         let payload:string = '{\"iat\":' + now + '}';
25         
26         let header64:string = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(header));
27         let payload64:string = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(payload));
28         
29         let encodedString:string = header64 + '.' + payload64;
30         let mac_data:any = CryptoJS.HmacSHA512(encodedString, CryptoJS.enc.Base64.parse(jwtKey));
31         let signature: string = CryptoJS.enc.Base64.stringify(mac_data);
32         
33         let token: string = header64 + '.' + payload64 + '.' + signature;
34         
35         sessionStorage.setItem('token', token);
36     }
37 
38 }
第 14、15 行就是呼叫 service 端的程式,重點在第 12 行,當這個 service 是需要 JWT 認證,在呼叫前要先產生 token 放入 sessionStorage,產生的方法在 genToken() method,說明如下:
  1. 在第一篇中,我們知道,這個認證的方式是發送端發送含 header、payload 及 signature 的字串給接收端,接收端可以透過 signature 的簽章檢查 header、payload 是否有被竄改,借此達到認證的目的。第 23、24 行先定義 header、payload 的值,service 端因為只要求要有發送時間,所以這裡在 iat 欄位放入發送時間,至於 alg 的值 HS512,這是一定要有的,表示是用 HS512 (HmacSHA-512) 這個演算法簽章的。
  2. 接下來要產生的 token 需要進行 Base64 轉換及 HmacSHA-512 的簽章加密,在 JavaScript 中有一個很有名的函式庫 CryptoJS,在第 1 行引入。
  3. 第 26、27 行使用 CryptoJS 提供的 CryptoJS.enc.Base64.stringify 函式將 header、payload 轉換成 Base64 格式,因為傳入的參數要是二進位值,先使用 CryptJS.eng.Utf8.parse 函式轉換。
  4. 第 30 行對 header、payload 進行簽章,原本我們存在 sessionStorage 的 JWT key 是 Base64 格式,這裡要轉換為 binary,所以使用 CryptoJS.enc.Base64.parse 函式轉換,經 CryptoJS.HmacSHA512 函式加密後,得到的簽章值存入 mac_data 變數。
  5. 第 31 行,將剛才得到的簽章轉換為 Base64 格式。
  6. 第 33 行,依規範將 header、payload、signature 連接起來,以 . 隔開,成為 token,在 35 行存入 sessionStorage 的 'token' 中,這個 token 前面不需要加 'Bearer ',因為 angular2-jwt 會幫忙加上。
  7. 在第 14 行,這裡呼叫 RESTful service 使用的不是 HttpModule 的方法,而是使用 angular2-jwt 提供的 AuthHttp,它會在送出 request 時,於 http header 中加入 token,於是就可以在 RESTful service 端進行認證了!




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 就可以知道錯誤原因。




JWT 認證在 RESTful service 上的應用 (1) - JWT 說明

RESTful 服務現在大量取代了原本 SOAP 擔任的系統間溝通的任務,尤其是企業內部的系統間溝通,新開發的程式很少會選用 SOAP。RESTful 程式的輕薄短小是它的優點,也是廣受歡迎的最重要原因,所以系統間認證時,就流行以 JWT (JSON Web Token) 方式進行認證,JWT 只是在 http 的 header 加一段很短的訊息,不會讓兩個系統間交換的電文大量膨脹。這篇文章會有點長,所以分成三篇,如下:
  1. JWT 認證在 RESTful service 上的應用 (1) - JWT 說明
  2. JWT 認證在 RESTful service 上的應用 (2) - RESTful service
  3. JWT 認證在 RESTful service 上的應用 (3) - Angular
底下開始是第一篇 …  JWT 說明

JWT 是在 http 的 header 中放入一小段加密的字串,通常會放在命名為 Authorization 的欄位裡,這段字串分三段,每段以句點 ( . ) 隔開,這三段分別為 Header、Payload、Signature,Header 和 Payload 是 JSON 格式並且以 Base64 編碼的字串,Signature 是簽章,用來驗證前面的 header、payload 有沒有被竄改,說明如下:
  • Header
通常 header 裡會有兩個欄位,如下,alg 寫著加密的演算法,typ 固定就寫 JWT,如下 HS256 表示 SHA-256,加密演算法不限定用對稱或不對稱的方式。
{
    "alg": "HS256",
    "typ": "JWT"
}
  • Payload
這一段有一些常用的欄位 -- iss (issuer, JWT 的簽發者)、iat (issuedAt, 發出訊息的時間)、exp (expiration time, 逾時時間)、sub (subject, client端發出訊息者)、aud (audience, 接收訊息者) 等等。這些欄位算是 metadata 不一定要有值,當然啦~ 如果 service 端有要求,就要放上相關的值,iat、exp 都是整數,以1970/01/01 00:00:00 開始計算的秒數。除了那些欄位外,也可以自訂一些其它欄位,舉例如下:
{
     "iss": "steven",
     "company": "Google",
     "sub": "test"
}
company 是自訂欄位 … Header、Payload 當然會以 Base64 編碼。
  • Signature
這個簽章固定用下面的方式產生:
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, key);
也就是把 header、payload 分別以 Base64 編碼相加,中間以 . 隔開,然後再用 key (Base64編碼) 簽章,這個 key 實際的值是二進位碼,以 Base64 記錄可能像是 lTjj86wJMqJ5RmnxVO4GppLG7b/U599mPCZM40CKWXaa3MXv5TQIfjXsiPcpI/s8Ou4Q2+uDhmO3+DAcdcOhZg==,如果是以 Base64 記錄,上面第 2 行程式的 key 就要再用函數將它轉換為二進位,簽章的演算法記錄在 Header 的 alg 欄位,如果以上面所記 HS256 來看,就是 SHA-256,簽章後的結果再以 Base64 編碼,

上面的 Signature 產生方式如果以 Java 來寫,如下:
String header64 = Base64.encode(header.getBytes());
String payload64 = Base64.encode(payload.getBytes());
        
Mac sha512_HMAC = Mac.getInstance("HmacSHA512");
SecretKeySpec secret_key = new javax.crypto.spec.SecretKeySpec(Base64.decode(key), "HmacSHA512");
sha512_HMAC.init(secret_key);
byte[] mac_data = sha512_HMAC.doFinal((header64 + "." + payload64).getBytes());
String signature = Base64.encode(mac_data);
* 上面的 Java 程式是假定 header 的 alg 值為 HS512 (SHA-512)

產生 signature 後,要將整個 JWT token 放入 Header 的 Authorization,一般會在 token 前加上 "Bearer ",所以會如下: (Bearer 後面有一個空白)
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + header64 + "." + payload64 + "." + signature);