Authentication trong SPAs thường là một chủ đề hot, đối với những người không chắc chắn về cách thức triển khai một hệ thống authentication với đầy đủ tính năng – registration, login and access token refreshing thông qua refresh tokens.
Trong bài viết này, chúng ta sẽ cùng thảo luận về cách triển khai một endpoints thay vì triển khai một JWT (Json web tokens) API. Vì vậy, chúng ta có thể linh hoạt trong việc viết JWT API cho riêng mình.
Tham khảo JWT Laravel tại https://sal.vn/oMStwi.
Triển khai
Với giao diện người dùng, chúng ta sẽ sử dụng các packages như sau: vuex-persistedstate, js-cookie và @nuxtjs/axios. Chúng ta cần cho phép chúng lưu tokens và thông tin user có thể truy cập song song vào cả server và client, do đó việc xác thực có thể thực hiện ở cả 2.
Cài đặt packages:
1 2 | npm install --save vuex-persistedstate js-cookie @nuxtjs/axios |
VueX State Persistence
Để thực hiện gọi authenticated API từ server và brower (client), chúng ta cần đảm bảo tokens được quyền truy cập giữa 2 điểm. Vuex-persistedstate đơn giản hóa việc này với hỗ trợ của js-cookie sẽ duy trì tokens trên tcookie.
Sau khi cài đặt packages, chúng ta cần cấu hình cho vuex-persistedstate như một plugin.
plugins/local-storage.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <span class="token keyword">import</span> createPersistedState <span class="token keyword">from</span> <span class="token string">'vuex-persistedstate'</span> <span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> Cookies <span class="token keyword">from</span> <span class="token string">'js-cookie'</span> <span class="token keyword">import</span> cookie <span class="token keyword">from</span> <span class="token string">'cookie'</span> <span class="token comment">// access the store, http request and environment from the Nuxt context</span> <span class="token comment">// https://nuxtjs.org/api/context/</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> store<span class="token punctuation">,</span> req<span class="token punctuation">,</span> isDev <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token function">createPersistedState</span><span class="token punctuation">(</span><span class="token punctuation">{</span> key<span class="token operator">:</span> <span class="token string">'authentication-cookie'</span><span class="token punctuation">,</span> <span class="token comment">// choose any name for your cookie</span> paths<span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token comment">// persist the access_token and refresh_token values from the "auth" store module</span> <span class="token string">'auth.access_token'</span><span class="token punctuation">,</span> <span class="token string">'auth.refresh_token'</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> storage<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">// if on the browser, parse the cookies using js-cookie otherwise parse from the raw http request</span> <span class="token function-variable function">getItem</span><span class="token operator">:</span> <span class="token parameter">key</span> <span class="token operator">=></span> process<span class="token punctuation">.</span>client <span class="token operator">?</span> Cookies<span class="token punctuation">.</span><span class="token function">getJSON</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span> <span class="token operator">:</span> cookie<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>cookie <span class="token operator">||</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token comment">// js-cookie can handle setting both client-side and server-side cookies with one method</span> <span class="token comment">// use isDev to determine if the cookies is accessible via https only (i.e. localhost likely won't be using https)</span> <span class="token function-variable function">setItem</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">key<span class="token punctuation">,</span> value</span><span class="token punctuation">)</span> <span class="token operator">=></span> Cookies<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> value<span class="token punctuation">,</span> <span class="token punctuation">{</span> expires<span class="token operator">:</span> <span class="token number">14</span><span class="token punctuation">,</span> secure<span class="token operator">:</span> <span class="token operator">!</span>isDev <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// also allow js-cookie to handle removing cookies</span> <span class="token function-variable function">removeItem</span><span class="token operator">:</span> <span class="token parameter">key</span> <span class="token operator">=></span> Cookies<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span>store<span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Tiếp đó là add plugin này vào nuxt.config.js:
1 2 3 4 | plugins<span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">'~/plugins/local-storage'</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> |
VueX Store
Chúng ta cần thiết lập VueX store, đó là nơi sẽ lưu trữ dữ liệu về người dùng, access token và refresh token. Chúng sẽ bao gồm các actions cho việc gọi API để register, login và refresh user, cũng như các mutations để chuyển dữ liệu được trả về tới state.
store/auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | <span class="token comment">// reusable aliases for mutations</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token constant">SET_USER</span><span class="token operator">:</span> <span class="token string">'SET_USER'</span><span class="token punctuation">,</span> <span class="token constant">SET_PAYLOAD</span><span class="token operator">:</span> <span class="token string">'SET_PAYLOAD'</span><span class="token punctuation">,</span> <span class="token constant">LOGOUT</span><span class="token operator">:</span> <span class="token string">'LOGOUT'</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">state</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span> access_token<span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token comment">// JWT access token</span> refresh_token<span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token comment">// JWT refresh token</span> id<span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token comment">// user id</span> email_address<span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token comment">// user email address</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">const</span> mutations <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token comment">// store the logged in user in the state</span> <span class="token punctuation">[</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">SET_USER</span><span class="token punctuation">]</span> <span class="token punctuation">(</span><span class="token parameter">state<span class="token punctuation">,</span> <span class="token punctuation">{</span> id<span class="token punctuation">,</span> email_address <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> state<span class="token punctuation">.</span>id <span class="token operator">=</span> id state<span class="token punctuation">.</span>email_address <span class="token operator">=</span> email_address <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// store new or updated token fields in the state</span> <span class="token punctuation">[</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">SET_PAYLOAD</span><span class="token punctuation">]</span> <span class="token punctuation">(</span><span class="token parameter">state<span class="token punctuation">,</span> <span class="token punctuation">{</span> access_token<span class="token punctuation">,</span> refresh_token <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> state<span class="token punctuation">.</span>access_token <span class="token operator">=</span> access_token <span class="token comment">// refresh token is optional, only set it if present</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>refresh_token<span class="token punctuation">)</span> <span class="token punctuation">{</span> state<span class="token punctuation">.</span>refresh_token <span class="token operator">=</span> refresh_token <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// clear our the state, essentially logging out the user</span> <span class="token punctuation">[</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">LOGOUT</span><span class="token punctuation">]</span> <span class="token punctuation">(</span><span class="token parameter">state</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> state<span class="token punctuation">.</span>id <span class="token operator">=</span> <span class="token keyword">null</span> state<span class="token punctuation">.</span>email_address <span class="token operator">=</span> <span class="token keyword">null</span> state<span class="token punctuation">.</span>access_token <span class="token operator">=</span> <span class="token keyword">null</span> state<span class="token punctuation">.</span>refresh_token <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">const</span> actions <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token keyword">async</span> <span class="token function">login</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> commit<span class="token punctuation">,</span> dispatch <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> email_address<span class="token punctuation">,</span> password <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// make an API call to login the user with an email address and password</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> user<span class="token punctuation">,</span> payload <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>$axios<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span> <span class="token string">'/api/auth/login'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> email_address<span class="token punctuation">,</span> password <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token comment">// commit the user and tokens to the state</span> <span class="token function">commit</span><span class="token punctuation">(</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">SET_USER</span><span class="token punctuation">,</span> user<span class="token punctuation">)</span> <span class="token function">commit</span><span class="token punctuation">(</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">SET_PAYLOAD</span><span class="token punctuation">,</span> payload<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token function">register</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> commit <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> email_addr<span class="token punctuation">,</span> password <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// make an API call to register the user</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> user<span class="token punctuation">,</span> payload <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>$axios<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span> <span class="token string">'/api/auth/register'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> email_address<span class="token punctuation">,</span> password <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token comment">// commit the user and tokens to the state</span> <span class="token function">commit</span><span class="token punctuation">(</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">SET_USER</span><span class="token punctuation">,</span> user<span class="token punctuation">)</span> <span class="token function">commit</span><span class="token punctuation">(</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">SET_PAYLOAD</span><span class="token punctuation">,</span> payload<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// given the current refresh token, refresh the user's access token to prevent expiry</span> <span class="token keyword">async</span> <span class="token function">refresh</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> commit<span class="token punctuation">,</span> state <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> refresh_token <span class="token punctuation">}</span> <span class="token operator">=</span> state <span class="token comment">// make an API call using the refresh token to generate a new access token</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> payload <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>$axios<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span> <span class="token string">'/api/auth/refresh'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> refresh_token <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token function">commit</span><span class="token punctuation">(</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">SET_PAYLOAD</span><span class="token punctuation">,</span> payload<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// logout the user</span> <span class="token function">logout</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> commit<span class="token punctuation">,</span> state <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">commit</span><span class="token punctuation">(</span><span class="token constant">AUTH_MUTATIONS</span><span class="token punctuation">.</span><span class="token constant">LOGOUT</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">const</span> getters <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token comment">// determine if the user is authenticated based on the presence of the access token</span> <span class="token function-variable function">isAuthenticated</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">state</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> state<span class="token punctuation">.</span>access_token <span class="token operator">&&</span> state<span class="token punctuation">.</span>access_token <span class="token operator">!==</span> <span class="token string">''</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> |
Tiếp đó, chúng ta cần tạo một Form Components cho trang đăng nhập và đăng ký (registration). Phần này chúng ta sẽ đề cập chi tiết sau. Cơ bản, form của chúng ta nên gọi các authentication module actions để đăng nhập hoặc đăng ký thông tin người dùng.
1 2 3 4 5 | <span class="token keyword">const</span> email_address <span class="token operator">=</span> <span class="token string">'me@example.com'</span> <span class="token keyword">const</span> password <span class="token operator">=</span> <span class="token string">'abc123'</span> <span class="token keyword">await</span> $store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'auth/login'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> email_address<span class="token punctuation">,</span> password <span class="token punctuation">}</span><span class="token punctuation">)</span> |
Authenticated API Requests
Phần này, chúng ta sẽ sử dụng tính năng Interceptors có sẵn của Axios, nó cho phép chúng ta thay đổi request và responses cũng như handle tất cả các lỗi trả về. @nuxtjs/axios cung cấp đầy đủ: https://axios.nuxtjs.org/extend/#adding-interceptors
Chúng ta sẽ sử dụng 1 request interceptor để đính kèm access token với mỗi request.
plugins/axios.js
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token comment">// expose the store, axios client and redirect method from the Nuxt context</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> store<span class="token punctuation">,</span> app<span class="token operator">:</span> <span class="token punctuation">{</span> $axios <span class="token punctuation">}</span><span class="token punctuation">,</span> redirect <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> $axios<span class="token punctuation">.</span><span class="token function">onRequest</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">config</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// check if the user is authenticated</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>store<span class="token punctuation">.</span>state<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>access_token<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// set the Authorization header using the access token</span> config<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>Authorization <span class="token operator">=</span> <span class="token string">'Bearer '</span> <span class="token operator">+</span> store<span class="token punctuation">.</span>state<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>access_token <span class="token punctuation">}</span> <span class="token keyword">return</span> config <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Plugin này khá đơn giản, nó sẽ nắm bắt mọi request và nếu người dùng được xác thực, sẽ thêm một Authorization header.
Thêm vào nuxt.config.js
:
1 2 3 4 5 | plugins: [ '~/plugins/local-storage', '~/plugins/axios', ], |
Refresh Tokens
Vì lý do bảo mật, nên 1 mã access tokens không nên để tồn tại quá lâu và nên dễ dàng thu hồi nếu cần thiết. Khi access token hết hạn hoặc không hợp lệ nhưng ứng dụng vẫn cần bảo vệ, vậy ứng dụng cần tạo một access token mới mà người dùng không cần cấp quyền truy cập một lần nữa.
Để giải quyết vấn đề này, chúng ta có thể sửa đổi interceptor plugin, thêm trình xử lý lỗi để tự động tạo mã access token mới trong trường hợp nó hết hạn.
Trong trường hợp access token hết hạn, API sẽ cần thông báo cho client rằng token không hợp lệ và cần được làm mới. Thường chúng ta sẽ trả về với status code là 401.
1 2 3 4 5 6 7 | <span class="token punctuation">{</span> <span class="token property">"status"</span><span class="token operator">:</span> <span class="token string">"failed"</span><span class="token punctuation">,</span> <span class="token property">"text_code"</span><span class="token operator">:</span> <span class="token string">"TOKEN_EXPIRED"</span><span class="token punctuation">,</span> <span class="token property">"message"</span><span class="token operator">:</span> <span class="token string">"The JWT token is expired"</span><span class="token punctuation">,</span> <span class="token property">"status_code"</span><span class="token operator">:</span> <span class="token number">401</span> <span class="token punctuation">}</span> |
Lúc này, client đã nhận biết được token hết hạn, có thể chuyển tới để làm mới lại token, trước khi thử lại mộ request như ban đầu.
Thường endpoint Refresh Token cung cấp 1 giá trị refresh_token thông qua POST request, nên API sẽ cần tạo ra một access token mới để trả về phía client. Nếu token mới hết hạn, bị thu hồi hoặc không hợp lệ, nó có thể trả về mã lỗi để phía client là không thể xác thực lại và cần logout ra.
Chúng ta sẽ cần chỉnh sửa lại interceptor plugin để bắt lỗi plugins/axios.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | <span class="token comment">// expose the store, axios client and redirect method from the Nuxt context</span> <span class="token comment">// https://nuxtjs.org/api/context/</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> store<span class="token punctuation">,</span> app<span class="token operator">:</span> <span class="token punctuation">{</span> $axios <span class="token punctuation">}</span><span class="token punctuation">,</span> redirect <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token constant">IGNORED_PATHS</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">'/auth/login'</span><span class="token punctuation">,</span> <span class="token string">'/auth/logout'</span><span class="token punctuation">,</span> <span class="token string">'/auth/refresh'</span><span class="token punctuation">]</span> $axios<span class="token punctuation">.</span><span class="token function">onRequest</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">config</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// check if the user is authenticated</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>store<span class="token punctuation">.</span>state<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>access_token<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// set the Authorization header using the access token</span> config<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>Authorization <span class="token operator">=</span> <span class="token string">'Bearer '</span> <span class="token operator">+</span> store<span class="token punctuation">.</span>state<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>access_token <span class="token punctuation">}</span> <span class="token keyword">return</span> config <span class="token punctuation">}</span><span class="token punctuation">)</span> $axios<span class="token punctuation">.</span><span class="token function">onError</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">resolve<span class="token punctuation">,</span> reject</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// ignore certain paths (i.e. paths relating to authentication)</span> <span class="token keyword">const</span> isIgnored <span class="token operator">=</span> <span class="token constant">IGNORED_PATHS</span><span class="token punctuation">.</span><span class="token function">some</span><span class="token punctuation">(</span><span class="token parameter">path</span> <span class="token operator">=></span> error<span class="token punctuation">.</span>config<span class="token punctuation">.</span>url<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>path<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment">// get the status code from the response</span> <span class="token keyword">const</span> statusCode <span class="token operator">=</span> error<span class="token punctuation">.</span>response <span class="token operator">?</span> error<span class="token punctuation">.</span>response<span class="token punctuation">.</span>status <span class="token operator">:</span> <span class="token operator">-</span><span class="token number">1</span> <span class="token comment">// only handle authentication errors or errors involving the validity of the token</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>statusCode <span class="token operator">===</span> <span class="token number">401</span> <span class="token operator">||</span> statusCode <span class="token operator">===</span> <span class="token number">422</span><span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token operator">!</span>isIgnored<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// API should return a reason for the error, represented here by the text_code property</span> <span class="token comment">// Example API response: </span> <span class="token comment">// { </span> <span class="token comment">// status: 'failed', </span> <span class="token comment">// text_code: 'TOKEN_EXPIRED',</span> <span class="token comment">// message: 'The JWT token is expired',</span> <span class="token comment">// status_code: 401</span> <span class="token comment">// }</span> <span class="token comment">// retrieve the text_code property from the response, or default to null</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> text_code <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token punctuation">{</span> text_code<span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> error<span class="token punctuation">.</span>response <span class="token operator">||</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token comment">// get the refresh token from the state if it exists</span> <span class="token keyword">const</span> refreshToken <span class="token operator">=</span> store<span class="token punctuation">.</span>state<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>refresh_token <span class="token comment">// determine if the error is a result of an expired access token</span> <span class="token comment">// also ensure that the refresh token is present</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>text_code <span class="token operator">===</span> <span class="token string">'TOKEN_EXPIRED'</span> <span class="token operator">&&</span> refreshToken<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// see below - consider the refresh process failed if this is a 2nd attempt at the request</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>error<span class="token punctuation">.</span>config<span class="token punctuation">.</span><span class="token function">hasOwnProperty</span><span class="token punctuation">(</span><span class="token string">'retryAttempts'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// immediately logout if already attempted refresh</span> <span class="token keyword">await</span> store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'auth/logout'</span><span class="token punctuation">)</span> <span class="token comment">// redirect the user home</span> <span class="token keyword">return</span> <span class="token function">redirect</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token comment">// merge a new retryAttempts property into the original request config to prevent infinite-loop if refresh fails</span> <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span> retryAttempts<span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token operator">...</span>error<span class="token punctuation">.</span>config <span class="token punctuation">}</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment">// attempt to refresh access token using refresh token</span> <span class="token keyword">await</span> store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'auth/refresh'</span><span class="token punctuation">)</span> <span class="token comment">// re-run the initial request using the new request config after a successful refresh</span> <span class="token comment">// this response will be returned to the initial calling method</span> <span class="token keyword">return</span> <span class="token function">resolve</span><span class="token punctuation">(</span><span class="token function">$axios</span><span class="token punctuation">(</span>config<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// catch any error while refreshing the token</span> <span class="token keyword">await</span> store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'auth/logout'</span><span class="token punctuation">)</span> <span class="token comment">// redirect the user home</span> <span class="token keyword">return</span> <span class="token function">redirect</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>text_code <span class="token operator">===</span> <span class="token string">'TOKEN_INVALID'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// catch any other JWT-related error (i.e. malformed token) and logout the user</span> <span class="token keyword">await</span> store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'auth/logout'</span><span class="token punctuation">)</span> <span class="token comment">// redirect the user home</span> <span class="token keyword">return</span> <span class="token function">redirect</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">// ignore all other errors, let component or other error handlers handle them</span> <span class="token keyword">return</span> <span class="token function">reject</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Ở đây, plugin đã được ghi chú rất cụ thể, nhưng về cơ bản trình đánh chặn mới sẽ kiểm tra xem lỗi có liên quan đến token đã hết hạn hay không và sau đó cố gắng làm mới access token nếu có.
Nếu xử lý thành công Promise sẽ trả về một bản sao request ban đầu, làm cho chức năng gọi hoàn toàn không biết rằng token đã được làm mới trước khi nhận được phản hồi của nó. Tuy nhiên, nếu quá trình xử lý làm mới không thành công Interceptor sẽ tự động logout và điều đướng tới trang chủ.
1 2 | <span class="token keyword">const</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> $axios<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/api/my-account'</span><span class="token punctuation">)</span> |
Nuxt cung cấp nuxtServerInit hook cho SSR request tới server. Chúng ta có thể tự động làm mới access token khi người dùng đã đăng nhập với kết nối đầu tiên tới server.
Với SPA sẽ không cần thiết phải làm mới thường xuyên, nên khi nó xảy ra, chúng ta có thể cug cấp một token ngắn hạn.
Để thực hiện điều này, cần thêm vào root store:
store/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token comment">// ....</span> <span class="token keyword">export</span> <span class="token keyword">const</span> actions <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token comment">// https://nuxtjs.org/guide/vuex-store/#the-nuxtserverinit-action</span> <span class="token comment">// automatically refresh the access token on the initial request to the server, if possible</span> <span class="token keyword">async</span> <span class="token function">nuxtServerInit</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> dispatch<span class="token punctuation">,</span> commit<span class="token punctuation">,</span> state <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> access_token<span class="token punctuation">,</span> refresh_token <span class="token punctuation">}</span> <span class="token operator">=</span> state<span class="token punctuation">.</span>auth <span class="token keyword">if</span> <span class="token punctuation">(</span>access_token <span class="token operator">&&</span> refresh_token<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment">// refresh the access token</span> <span class="token keyword">await</span> <span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'auth/refresh'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// catch any errors and automatically logout the user</span> <span class="token keyword">await</span> <span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'auth/logout'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token comment">// ...</span> |
Bây giờ, khi người dùng điều hướng ứng dụng thông qua URL hoặc liên kết đến bên ngoài, chúng ta sẽ tự động làm mới access token của họ nếu họ đã đăng nhập trước đó.
Kết Luận
Ở bài viết này, chúng ta sẽ chỉ thảo luận sơ qua về việc triển khai Authentication trong SPAs nhưng cung cấp cho chúng ta những khí niệm cần thiết để có thể triển khai một universal client và server-side JWT authentication trong Nuxt. Bài viết sau chúng ta sẽ cùng xây dựng một ứng dụng cụ thể bao gồm đây đủ hơn về Authentication trong SPAs.