Keycloak 与 react-admin 的集成

问题描述

搜索一些关于如何将 keycloak 与 react admin 应用程序集成以进行身份​​验证的示例。

解决方法

App.js 示例:

// your other imports
import { ReactKeycloakProvider } from "@react-keycloak/web";
import Keycloak from "keycloak-js";
import Cookies from "js-cookie";

// we used cookies that backend is writing,because we had front as a production static build 
const initOptions = {
  url: Cookies.get("REACT_APP_KEYCLOAK_URL"),realm: Cookies.get("REACT_APP_KEYCLOAK_REALM"),clientId: Cookies.get("REACT_APP_KEYCLOAK_CLIENT_ID"),onLoad: "login-required",};

const keycloak = Keycloak(initOptions);

const onToken = () => {
  if (keycloak.token && keycloak.refreshToken) {
    localStorage.setItem("token",keycloak.token);
    localStorage.setItem("refresh-token",keycloak.refreshToken);
  }
};

const onTokenExpired = () => {
  keycloak
    .updateToken(30)
    .then(() => {
      console.log("successfully get a new token",keycloak.token);
    })
    .catch(() => {
      console.error("failed to refresh token");
    });
};

// for data provider,it writes token to an authorization header
const fetchJson = (url,options = {}) => {
  if (!options.headers) {
    options.headers = new Headers({ Accept: "application/json" });
  }
  if (keycloak.token) {
    options.headers.set("Authorization","Bearer " + keycloak.token);
  } else if (localStorage.getItem("token")) {
    options.headers.set(
      "Authorization","Bearer " + localStorage.getItem("token")
    );
  }
  return fetchUtils.fetchJson(url,options);
};

const customDataProvider = dataProvider("/api/v1",fetchJson);

const theme = createMuiTheme({
  ...defaultTheme,sidebar: {
    width: 110,closedWidth: 40,},});

const fetchResources = (permissions) => {
  let knownResources = [];
  if (permissions) {
    const resource = (
      <Resource
        name="feeds"
        list={FeedList}
        create={FeedCreate}
        edit={StateEdit}
        icon={CollectionsBookmark}
      />
    );
    knownResources.push(resource);
  } else {
    const resource = (
      <Resource name="feeds" list={FeedList} icon={CollectionsBookmark} />
    );
    knownResources.push(resource);
  }
  return knownResources;
};

const CustomAdminWithKeycloak = () => {
  const customAuthProvider = useAuthProvider(
    Cookies.get("REACT_APP_KEYCLOAK_CLIENT_ID")
  );
  return (
    <Admin
      theme={theme}
      dataProvider={customDataProvider}
      authProvider={customAuthProvider}
      loginPage={false}
      title="Inventory Splitter"
      layout={CustomLayout}
    >
      {fetchResources}
    </Admin>
  );
};

const CustomAdmin = () => {
  return (
    <Admin
      theme={theme}
      dataProvider={customDataProvider}
      loginPage={false}
      title="Inventory Splitter"
      layout={CustomLayout}
    >
      <Resource
        name="feeds"
        list={FeedList}
        create={FeedCreate}
        edit={StateEdit}
        icon={CollectionsBookmark}
      />
    </Admin>
  );
};

// we have a feature to completely switch off the authorization process through env variable on backend
const App = () => {
  const useKeycloak = Cookies.get("USE_KEYCLOAK") === "true";
  return useKeycloak ? (
    <ReactKeycloakProvider
      authClient={keycloak}
      LoadingComponent={<div></div>}
      initOptions={initOptions}
      onTokens={onToken}
      onTokenExpired={onTokenExpired}
    >
      <React.Fragment>
        <CustomAdminWithKeycloak />
        <ThemeProvider theme={theme}>
          <Footer />
        </ThemeProvider>
      </React.Fragment>
    </ReactKeycloakProvider>
  ) : (
    <React.Fragment>
      <CustomAdmin />
      <ThemeProvider theme={theme}>
        <Footer />
      </ThemeProvider>
    </React.Fragment>
  );
};

export default App;

authProvider.js

import { useKeycloak } from '@react-keycloak/web'
import jwt_decode from 'jwt-decode'

const useAuthProvider = (clientID:string) => {
    const { keycloak } = useKeycloak();
    return ({
        login: () => keycloak.login(),checkError: () => Promise.resolve(),checkAuth: () => {
            return keycloak.authenticated &&
            keycloak.token ? Promise.resolve() : Promise.reject("Failed to obtain access token.");
        },logout: () => keycloak.logout(),getIdentity: () => {
            if (keycloak.token) {
                const decoded : any = jwt_decode(keycloak.token);
                const id = decoded.sub
                const fullName = decoded.name
                return Promise.resolve({id,fullName});
            }
            return Promise.reject("Failed to get identity");
        },getPermissions:() => {
            let hasRole = false;
            if (keycloak.token) {
                const decoded : any = jwt_decode(keycloak.token);
                decoded.resource_access[clientID].roles.forEach((el: string) => {
                    if (el === "admin") {
                        hasRole = true;
                        return
                    }
                });
            }
            if (hasRole) {
                return Promise.resolve(true);
            }
            return Promise.resolve(false);
        },});
};

export default useAuthProvider;

如果你想根据权限隐藏一些组件,像这样使用 smth:

import * as React from 'react';
import {
    useListContext,usePermissions,TopToolbar,CreateButton,} from 'react-admin';
import PublishIcon from '@material-ui/icons/Publish';

const CustomListActions = (props) => {
    const permissions = usePermissions();
    const { basePath } = useListContext();
    return (
        <TopToolbar>
            {permissions.permissions && <CreateButton label='Upload' basePath={basePath} icon={<PublishIcon/>}/>}
        </TopToolbar>
    );
};

export default CustomListActions;