VueJS 与 Keycloak 集成

3,042 阅读2分钟

VueJS 与 Keycloak 集成

Keycloak是一个开源软件产品,旨在为现代的应用程序和服务,提供包含身份管理和访问管理功能的单点登录工具。从概念的角度上来说,该工具的目的是,只用少量编码甚至不用编码,就能很容易地使应用程序和服务更安全。

这里使用的是vue-keycloak-js,官方文档 vue-keycloak-js

vue-keycloak-js 下载与安装

npm i --save @dsb-norge/vue-keycloak-js 

Vue 相关配置

main.js中引入并使用 keycloak


import keycloak from '@dsb-norge/vue-keycloak-js';
...

Vue.use(keycloak , {
  init: {
    // Use 'login-required' to always require authentication
    // If using 'login-required', there is no need for the router guards in router.js
    // 
    checkLoginIframe:false,
    onLoad: 'check-sso'
  },
  config: {
    url: 'http://auth.xxxx.net/auth',
    realm: 'portal',
    clientId: 'portal-client',
  },
  onReady: (keycloak) => {
    new Vue({
      router,
      keycloak,
      render: h => h(App)
    }).$mount('#app')
  }
});

因为该插件引用的keycloak-js版本过低("keycloak-js": "4.8.3"),checkLoginIframe 配置在才用check-sso模式时必不可少(如果直接使用最新keycloak-js则不用考虑),否则会导致

Refused to frame 'http://auth.xxxx.net/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self'".

具体原因请参考: www.keycloak.org/docs/latest…

vue-router相关设置

因为不是采用的login-required模式,而是对某些链接才需要添加相关的控制,我们可以在router中添加相关的配置:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  ...
    {
        path: '/labs',
        name: 'lab_status_history',
        component: () => import('../components/lab_mgmt/LabSummary'),
        meta:{
            //设置当前链接是否需要登录
            requiresAuth:true
        }
    }
    ...
]

const router = new VueRouter({
    routes
})

router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.requiresAuth)) {
        if (router.app.$keycloak.authenticated) {
            console.log("Authenticated ")
            next()
        } else {
            const loginUrl = router.app.$keycloak.createLoginUrl()
            window.location.replace(loginUrl)
        }
    } else {
        console.log("Authenticated not required ")
        next()
    }
})
export default router

path需要登录时,使用$keycloak.createLoginUrl()来生成相应的login URL, 当登录完成时,会自动跳转到当前页面。

可能的坑

  • CORS 错误
    • Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.
      • 因为本项目使用axios作为 http客户端,在发出真正的请求(该请求为非简单请求)之前,它会先发出一个OPTIONS请求以获得服务端支持的操作和header 列表,如果在服务端无相应的配置,则会出现上面的错误。具体配置参看后续Nginx设置
    • keycloak登录页面跳转不正常
      • keyclok Client配置中,Valid Redirect URIs 配置的匹配不正确

Axios 配置

本例中使用axios和后端通信,相关的配置如下

import axios from 'axios';
import Qs from 'qs';
import Vue from 'vue';


export const baseURL = `http://api.xxx.net/`;

const service = axios.create({
  baseURL: baseURL
});
// 请求拦截器
service.interceptors.request.use(
    function(config) {
      config.baseURL = fp_baseURL;
      config.withCredentials = true;
      config.timeout = 5000;
      let headers= config.headers
      let token = Vue.prototype.$keycloak.token;
      if (token) {
        //注入得到的token
        headers['Authorization']= 'Bearer '+token;
      } 
      config.headers= headers
      return config;
    },
    function(error) {
      return Promise.reject(error);
    }
);
...

export default service;

可能的坑

  • CORS 错误
  • Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.
    • 服务端的OPTIONS 响应未包含 刚才注入的 Authorization
  • Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
    • 对于CORS请求,后端要求'Access-Control-Allow-Origin'头,但是程序发出的请求头里并没有包含
  • The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed.
    • 看上去像是本地发出的request里包含了重复的Access-Control-Allow-Origin,实际上的原因是,对于Authorization头,*通配符模式并不适用,具体原因

NGINX 设置


# Adaptation engine Product env configuration
upstream portal{

    server 10.32.219.60:8085;

    keepalive 30;
    ## tengine config
    check interval=5000 rise=2 fall=5 timeout=10000 type=http;
    check_keepalive_requests 100;
    check_http_send "HEAD /swagger-ui.html HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx;
    session_sticky;
}
server {
    listen       80;
    server_name api.xxx.net;

    charset utf-8;
    location / {
        proxy_pass  http://portal;
        #配置跨域访问
        add_header 'Access-Control-Allow-Headers' 'Access-Control-Allow-Origin,Content-Type,Access-Control-Allow-Credentials,Authorization'; 
        #对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为“*”。这是因为请求的首部中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为“*”,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 相应的host,则请求将成功执行。
        proxy_hide_header 'access-control-allow-origin';
        add_header 'Access-Control-Allow-Origin' $http_origin; 
        add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
        # 服务端响应必须包含Access-Control-Allow-Credentials 头部,否则浏览器不会发送身份凭证信息
        add_header 'Access-Control-Expose-Headers' 'Access-Control-Allow-Origin,Access-Control-Allow-Credentials';

        if ($request_method = 'OPTIONS') {
            # 当OPTIONS操作时直接返回,不需要到后端,实际上大多数的程序也没有对OPTIONS操作有特定的设置
            # 当然如果你的后端是Spring,你也可以通过拦截器的方式实现这一拦截,http://ckjava.com/2019/03/05/SpringBoot-CORS-practice/ 
            return 204;
        }
    }

    error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}
upstream portal_web{

    server 10.32.219.60:80;

    keepalive 30;
    ## tengine config
    check interval=5000 rise=2 fall=5 timeout=10000 type=http;
    check_keepalive_requests 100;
    check_http_send "HEAD / HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx;
    session_sticky;
}
server {
    listen       80;
    server_name portal.xxx.net;

    charset utf-8;
    location / {
        proxy_pass  http://fp_portal_web;
        #配置跨域访问
        add_header 'Access-Control-Allow-Headers' 'Content-Type';
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
        add_header 'Access-Control-Expose-Headers' 'Access-Control-Allow-Origin,Access-Control-Allow-Credentials';

        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }

    error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

upstream fp_portal_auth{

    server 10.32.219.58:8090;

    keepalive 30;
    ## tengine config
    check interval=5000 rise=2 fall=5 timeout=10000 type=http;
    check_keepalive_requests 100;
    check_http_send "HEAD /auth HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_3xx;
    session_sticky;
}
server {
    listen       80;
    server_name auth.xxx.net;

    charset utf-8;
    location /auth {
        
        proxy_pass  http://portal_auth;     
        # 这一段可以在nginx.conf里设置,主要是为了将请求端和后端链接起来
        proxy_set_header    Host               $host;
        proxy_set_header    X-Real-IP          $remote_addr;
        proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Host   $host;
        proxy_set_header    X-Forwarded-Server $host;
        proxy_set_header    X-Forwarded-Port   $server_port;
        proxy_set_header    X-Forwarded-Proto  $scheme;
        #配置跨域访问
        add_header 'Access-Control-Allow-Headers' 'Access-Control-Allow-Origin,Content-Type,Authorization';
        proxy_hide_header 'access-control-allow-origin';
        add_header 'Access-Control-Allow-Origin' $http_origin;
        add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
        add_header 'Access-Control-Expose-Headers' 'Access-Control-Allow-Origin,Access-Control-Allow-Credentials';

        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }

    error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

upstream fp_portal_lab_service{

    server 10.32.219.58:8091;

    keepalive 30;
    ## tengine config
    check interval=5000 rise=2 fall=5 timeout=10000 type=http;
    check_keepalive_requests 100;
    check_http_send "HEAD /actuator/health HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx;
    session_sticky;
}
server {
    listen       80;
    server_name lab-service.xxx.net;

    charset utf-8;
    location / { 
        # 这一段可以在nginx.conf里设置,主要是为了将请求端和后端链接起来
        proxy_set_header    Host               $host;
        proxy_set_header    X-Real-IP          $remote_addr;
        proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Host   $host;
        proxy_set_header    X-Forwarded-Server $host;
        proxy_set_header    X-Forwarded-Port   $server_port;
        proxy_set_header    X-Forwarded-Proto  $scheme;
        proxy_pass  http://portal_lab_service;
        #配置跨域访问
        add_header 'Access-Control-Allow-Headers' 'Access-Control-Allow-Origin,Content-Type,Access-Control-Allow-Credentials,Authorization'; 
        # add_header 'Access-Control-Allow-Origin' *;
        proxy_hide_header 'access-control-allow-origin';
        add_header 'Access-Control-Allow-Origin' $http_origin;
        add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE';
        add_header 'Access-Control-Expose-Headers' 'Access-Control-Allow-Origin,Access-Control-Allow-Credentials';
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }

    error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}