Chào mừng bạn đến với Cafedev, nơi chúng tôi chia sẻ kiến thức và kinh nghiệm về lập trình và công nghệ. Trong chủ đề mới nhất của chúng tôi về Xác thực Keycloak với Vue3 + Pinia, chúng tôi sẽ khám phá cách tích hợp và sử dụng dịch vụ Keycloak trong ứng dụng Vue3 sử dụng thư viện Pinia. Với sự kết hợp này, bạn sẽ có thể xây dựng các ứng dụng web an toàn và bảo mật một cách hiệu quả, đồng thời tăng cường tính linh hoạt và tiện lợi cho người dùng. Hãy cùng khám phá cách triển khai tính năng xác thực mạnh mẽ này trên nền tảng Vue3 và Pinia tại Cafedev!

Có khả năng bạn đến đây vì bạn đã cố gắng tích hợp xác thực Keycloak vào ứng dụng Vue 3 của mình với Pinia nhưng đã gặp khó khăn vì thiếu ví dụ và tài liệu?
Dù sao, đây là một hướng dẫn về cách bạn có thể làm điều đó. 😜

1. Nhưng trước hết…

Mục đích của hướng dẫn này là chỉ bạn cách tích hợp Keycloak vào ứng dụng Vue 3 của bạn với Pinia. Nếu bạn đến đây hy vọng học cách cài đặt Keycloak thì chắc chắn bạn có thể tìm thấy các tài liệu để làm điều đó ở một nơi khác.
Giờ khi đã rõ ràng, hãy bắt đầu!

2. Những gì bạn cần

Tên Realm, Client ID & URL của xác thực Keycloak của bạn, mà bạn có thể tìm thấy ở một nơi nào đó trong cài đặt bảng điều khiển quản trị Keycloak của bạn.

3. Tạo một dự án Vue 3 mới

Đầu tiên, chúng ta cần một dự án Vue 3 mới, vì vậy hãy tạo một dự án mới!
Vui lòng tham khảo hướng dẫn chính thức

4. Cài đặt các gói npm

Chúng ta sẽ cần các gói npm này được cài đặt vào ứng dụng của chúng ta để nó hoạt động.

npm i axios cors keycloak-js pinia pinia-plugin-persistedstate vue-router

Bạn có thể tìm hiểu thêm về mỗi gói qua tài liệu chính thức của chúng.
Tuy nhiên, điều quan trọng cần lưu ý là chúng tôi sẽ sử dụng pinia-plugin-persistedstate với Pinia vì nếu không, trạng thái của store của chúng tôi sẽ bị mất mỗi khi chúng ta làm mới ứng dụng của mình.

5. Tạo một cửa hàng Pinia đơn giản

Hãy tạo một tệp mới trong src > stores ** gọi là authStore.js** .

// file: src/stores/authStore.js

import { defineStore } from "pinia";

export const useAuthStore = defineStore({
id: "storeAuth",
state: () => {
return {
authenticated: false,
user: {},
test: false
}
},
persist: true,
getters: {},
actions: {
testAction() {
this.test = !this.test;
}
}
});

Để có thể gọi cửa hàng của chúng tôi trên toàn cầu trong ứng dụng của chúng tôi, chúng ta cũng cần tạo một plugin cho nó trong thư mục src > plugins.

// file: src/plugins/authStore.js

import { useAuthStore } from "@/stores/authStore.js";

// Setup auth store as a plugin so it can be accessed globally in our FE
const authStorePlugin = {
install(app, option) {
app.config.globalProperties.$store = useAuthStore(option.pinia);
}
}

export default authStorePlugin;

Bây giờ hãy chuyển đến tệp main.js và thêm các dòng này vào.

// file: src/main.js

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue';
import router from './router';
import AuthStorePlugin from './plugins/authStore';

// Styles
import './style.css';

// Create Pinia instance
const pinia = createPinia();

// Use persisted state with Pinia so our store data will persist even after page refresh
pinia.use(piniaPluginPersistedstate);

const renderApp = () => {
const app = createApp(App);
app.use(AuthStorePlugin, { pinia });
app.use(pinia);
app.use(router);
app.mount('#app');
}

renderApp();

Hãy tạo một tệp định tuyến trong thư mục router, nơi chúng ta sẽ khai báo tất cả các định tuyến cho ứng dụng của chúng ta.

// file: src/router/index.js

import { createWebHistory, createRouter } from "vue-router";

// COMPONENTS
import Home from '@components/Home.vue';

const routes = [
{
path: "/",
name: "Home",
component: Home
}
];

const router = createRouter({
history: createWebHistory(),
routes, // short for `routes: routes`
});

export default router

Trước khi quên, hãy cập nhật tệp App.vue của chúng ta.

<template>
<router-view />
</template>

Thành phần Home thực sự là cùng một thành phần đã được tạo ban đầu bởi Vite, đổi tên và có một số nút bổ sung.
Chạy điều này trong thư mục FE:

npm run dev

Bạn nên thấy ứng dụng Vue 3 của bạn đang chạy trên localhost với trạng thái được duy trì hoạt động.
Để kiểm tra trạng thái được duy trì, nhấp vào nút “Test” & bạn nên thấy giá trị chuyển từ false sang true.

Làm mới trang và giá trị vẫn nên là true. Điều này có nghĩa là trạng thái được duy trì của chúng tôi đang hoạt động như mong đợi.

6. Tạo một dịch vụ cho Keycloak

Chúng tôi sẽ tạo một dịch vụ cho Keycloak trong src > services > keycloak.js để store của chúng tôi có thể sử dụng nó.
(Vui lòng xác định tất cả các biến môi trường trong tệp .env của bạn)

// file: src/services/keycloak.js

import Keycloak from 'keycloak-js';

const options = {
url: import.meta.env.VITE_KEYCLOAK_URL,
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
realm: import.meta.env.VITE_KEYCLOAK_REALM
}

const keycloak = new Keycloak(options);
let authenticated;
let store = null;

/**
* Initializes Keycloak, then run callback. This will prompt you to login.
*
* @param onAuthenticatedCallback
*/
async function init(onInitCallback) {
try {
authenticated = await keycloak.init({ onLoad: "login-required" })
onInitCallback()
} catch (error) {
console.error("Keycloak init failed")
console.error(error)
}
};

/**
* Initializes store with Keycloak user data
*
*/
async function initStore(storeInstance) {
try {
store = storeInstance
store.initOauth(keycloak)

// Show alert if user is not authenticated
if (!authenticated) { alert("not authenticated") }
} catch (error) {
console.error("Keycloak init failed")
console.error(error)
}
};

/**
* Logout user
*/
function logout(url) {
keycloak.logout({ redirectUri: url });
}

/**
* Refreshes token
*/
async function refreshToken() {
try {
await keycloak.updateToken(480);
return keycloak;
} catch (error) {
console.error('Failed to refresh token');
console.error(error);
}
}

const KeycloakService = {
CallInit: init,
CallInitStore: initStore,
CallLogout: logout,
CallTokenRefresh: refreshToken
};

export default KeycloakService;

Chúng tôi giữ dịch vụ đơn giản chỉ với 4 phương thức:
* init() sẽ khởi tạo Keycloak và yêu cầu người dùng đăng nhập.
Khá đơn giản.

7. Import dịch vụ Keycloak vào main.js

Thêm 2 dòng này vào tệp để Keycloak bảo người dùng đăng nhập khi render.

// file: src/main.js

import keycloakService from '@services/keycloak';

...

// renderApp(); // comment out the previous render app call

keycloakService.CallInit(renderApp);
// END OF FILE

Tiếp theo, hãy đến plugin authStore của bạn và cập nhật tệp với một vài dòng nữa…

// file: src/plugins/authStore.js

import { useAuthStore } from "@/stores/authStore.js";
import keycloakService from '@services/keycloak';

// Setup auth store as a plugin so it can be accessed globally in our FE
const authStorePlugin = {
install(app, option) {
const store = useAuthStore(option.pinia);

// Global store
app.config.globalProperties.$store = store;

// Store keycloak user data into store
keycloakService.CallInitStore(store);
}
}

export default authStorePlugin;

Bạn sẽ được tự động chuyển hướng đến trang đăng nhập Keycloak có vẻ như thế này.

8. Import dịch vụ Keycloak vào cửa hàng Pinia

Hãy cập nhật tệp authStore.js của chúng ta và thêm một số hành động nữa.

// file: src/stores/authStore.js

import { defineStore } from "pinia";
import keycloakService from '@services/keycloak';

...

actions: {
testAction() {
this.test = !this.test;
},
// Initialize Keycloak OAuth
async initOauth(keycloak, clearData = true) {
if(clearData) { await this.clearUserData(); }

this.authenticated = keycloak.authenticated;
this.user.username = keycloak.idTokenParsed.preferred_username;
this.user.token = keycloak.token;
this.user.refToken = keycloak.refreshToken;
},
// Logout user
async logout() {
try {
await keycloakService.CallLogout(import.meta.env.VITE_APP_URL);
await this.clearUserData();
} catch (error) {
console.error(error);
}
},
// Refresh user's token
async refreshUserToken() {
try {
const keycloak = await keycloakService.CallTokenRefresh();
this.initOauth(keycloak, false);
} catch (error) {
console.error(error);
}
},
// Clear user's store data
clearUserData() {
this.authenticated = false;
this.user = {};
}
}

...

Bây giờ bạn sẽ có thể đăng xuất khỏi Keycloak khi nhấn vào nút “Logout”.

9. Tạo một Interceptor Token cho các cuộc gọi API

Hãy tưởng tượng rằng chúng ta cũng có một backend Node + Express sẽ xác minh mã thông báo truy cập của người dùng Keycloak, để làm điều đó, chúng ta sẽ cần một interceptor token cho tất cả các yêu cầu axios được gửi từ ứng dụng Vue của chúng ta.
Hãy tạo một dịch vụ API cho axios.

// file: src/services/api.js

import axios from "axios";

// Creating an instance for axios to be used by the token interceptor service
const instance = axios.create({
baseURL: `${import.meta.env.VITE_BE_API_URL}/api`,
headers: {
"Content-Type": "application/json",
},
});

export default instance;

VITE_BE_API_URL nên là URL đến backend Node + Express của bạn
Sau đó tạo một dịch vụ Interceptor Token.

// file: src/services/tokenInterceptors.js

import axiosInstance from "@services/api";

const setup = (store) => {
axiosInstance.interceptors.request.use(
(config) => {
// If user is authenticated, place access token in request header.
if (store.authenticated) {
config.headers["x-access-token"] = store.user.token;
}

return config;
},
(error) => {
return Promise.reject(error);
}
);

axiosInstance.interceptors.response.use(
(res) => {
return res;
},
async (error) => {
const oriConfig = error.config;

if (error.response?.status === 401 && !oriConfig._retry) {
oriConfig._retry = true;

try {
// Refresh token then retry once
await store.refreshUserToken();

// Place refreshed access token in the request header
oriConfig.headers.headers["x-access-token"] = store.user.token;

return axiosInstance(oriConfig);
} catch (_error) {
console.error("Refresh token failed");
console.error(_error);

return Promise.reject(_error);
}
}

return Promise.reject(error);
}
);
};

export default setup;

Interceptor token sẽ đặt mã thông báo truy cập của người dùng vào tiêu đề yêu cầu, nếu được xác thực. Nó cũng sẽ làm mới mã thông báo và thử lại với mã thông báo mới nếu thất bại lần đầu.
Bây giờ nhập interceptor token vào plugin authStore của chúng ta.

// file: src/plugins/authStore.js

import setupInterceptors from '@services/tokenInterceptors';

...

// Store keycloak user data into store
keycloakService.CallInitStore(store);

// Setup token interceptor so every FE API calls will have the access token for BE to verify
setupInterceptors(store);

...

10. Kết hợp mọi thứ lại với nhau

Nếu bạn đã thiết lập mọi thứ đúng cách, bạn sẽ được yêu cầu đăng nhập sau đó thấy tên người dùng và các token của bạn được hiển thị trên trang Home sau mỗi lần đăng nhập thành công.

  • Nhấp vào nút “Test” để kiểm tra trạng thái được duy trì.

Và đó là cách bạn thiết lập Vue 3 với Pinia, trạng thái được duy trì & Keycloak.
Cảm ơn bạn đã đọc hướng dẫn này cũng như tôi đã thích viết nó. Hy vọng bạn đã học được điều gì đó từ nó và áp dụng điều này vào ứng dụng của bạn. Đừng ngần ngại để lại bình luận, thảo luận hoặc vỗ tay!

Và nếu bạn cần mã nguồn để tham khảo, bạn có thể tìm thấy chúng ở đây .

Cảm ơn bạn đã đồng hành cùng Cafedev trong hành trình tìm hiểu về Xác thực Keycloak với Vue3 + Pinia. Chúng tôi hy vọng rằng thông qua bài viết này, bạn đã có cái nhìn rõ ràng hơn về cách tích hợp và tận dụng sức mạnh của Keycloak trong việc xác thực người dùng trong ứng dụng Vue3 với sự hỗ trợ của Pinia. Hãy áp dụng những kiến thức mới này vào dự án của bạn và tiến xa hơn trong việc phát triển ứng dụng web an toàn và linh hoạt. Hãy tiếp tục theo dõi các bài viết mới trên Cafedev để không bỏ lỡ những kiến thức và kinh nghiệm mới nhất!

Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Vuejs từ cơ bản tới nâng cao

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!