Google Code Prettify

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 端進行認證了!




沒有留言:

張貼留言