這個程式會以如下的流程執行,作為 Client 端的 Angular 以帳密登入,經 service 端驗證通過後,會接收到一個 JWT 的 key,之後要 call service,都要用這個 key 產生 token 放入 header 以供認證。
- 環境說明
請先在 Angular 專案中安裝下面兩個 package。
- angular2-jwt: 用來傳送 jwt token (npm install angular2-jwt)
- crypto-js: 用來加密及處理 Base64 字串 (npm install crypto-js)
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 端程式修改
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 }
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
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 }
程式碼如下:
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 }
很簡單的輸入名字後按【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 }
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 }
- 在第一篇中,我們知道,這個認證的方式是發送端發送含 header、payload 及 signature 的字串給接收端,接收端可以透過 signature 的簽章檢查 header、payload 是否有被竄改,借此達到認證的目的。第 23、24 行先定義 header、payload 的值,service 端因為只要求要有發送時間,所以這裡在 iat 欄位放入發送時間,至於 alg 的值 HS512,這是一定要有的,表示是用 HS512 (HmacSHA-512) 這個演算法簽章的。
- 接下來要產生的 token 需要進行 Base64 轉換及 HmacSHA-512 的簽章加密,在 JavaScript 中有一個很有名的函式庫 CryptoJS,在第 1 行引入。
- 第 26、27 行使用 CryptoJS 提供的 CryptoJS.enc.Base64.stringify 函式將 header、payload 轉換成 Base64 格式,因為傳入的參數要是二進位值,先使用 CryptJS.eng.Utf8.parse 函式轉換。
- 第 30 行對 header、payload 進行簽章,原本我們存在 sessionStorage 的 JWT key 是 Base64 格式,這裡要轉換為 binary,所以使用 CryptoJS.enc.Base64.parse 函式轉換,經 CryptoJS.HmacSHA512 函式加密後,得到的簽章值存入 mac_data 變數。
- 第 31 行,將剛才得到的簽章轉換為 Base64 格式。
- 第 33 行,依規範將 header、payload、signature 連接起來,以 . 隔開,成為 token,在 35 行存入 sessionStorage 的 'token' 中,這個 token 前面不需要加 'Bearer ',因為 angular2-jwt 會幫忙加上。
- 在第 14 行,這裡呼叫 RESTful service 使用的不是 HttpModule 的方法,而是使用 angular2-jwt 提供的 AuthHttp,它會在送出 request 時,於 http header 中加入 token,於是就可以在 RESTful service 端進行認證了!
沒有留言:
張貼留言