CAS认证下的前后端分离的解决方案

一、简介

目前,许多高校的微服务体系都采用基于CAS认证的方式。在CAS认证机制下,CAS服务器和后端接口可能运行在不同的域名或端口上。然而,主流的前后端分离架构中,前端通常运行在独立的端口上。由于浏览器的同源策略的限制,跨域请求可能会受到限制,因此需要进行额外的配置和处理来解决跨域问题。如果前后端不分离,前端的开发将受到较大的制约。为了兼顾开发的效率以及又不对之前的微服务产生影响,得出了以下方案。

二、项目环境

  • 前端:Vue+Axios+VueRouter
  • 后端:Nginx+Catalyst+Docker

三、前后端不分离的CAS认证流程

  • Browser发起请求到后端
  • Backend检测到未登录301重定向Browser到CAS SERVER
  • Browser被重定向到CAS SERVER并进行登录
  • CAS SERVER认证成功后发放ticket到Browser
  • Browser拿到ticket向Backend重新发起请求
  • CAS client检测到ticket向CAS server校验ticket合法性
  • CAS server对ticket校验校验后返回校验信息
  • CAS client验票成功后发放cookie到Browser

完整流程图如下:

前后端不分离.png

详细流程如下:

  • 当用户请求应用时,应用检测到用户未登录,将用户重定向到CAS服务器的登录页面。

    • 资源的请求url

    2609b781c6218ad59b9eea8791f7b7a.png

    • 检测到状态未登录重定向的url,service参数为加密后的原本应用的url

    cas跳转地址.png

  • 当用户点击登录后,CAS服务器验证用户的凭证(用户名和密码)。如果凭证有效,CAS服务器会颁发一个唯一的票据(Ticket)给用户,并重定向回应用页面。
    ticket.png

  • 验证通过后,CAS服务器将用户信息存入Cookie(或使用Token),并保存在用户的浏览器中。之后的每次请求都会携带Cookie。
    cookie.png

四、前后端分离的CAS认证流程

  • Browser向Static server发起请求
  • Static serveri返回html给Browser
  • Browser html判断是否认证,如果没有则跳转CAS server
  • 跳转CAS server,此时的callback地址指向API server的CAS client(或自定义CAS验票函数)
  • 登录成功CAS serveri跳转到API Backend并携带ticket
  • 向CAS server发起验票
  • 返回验票结果,如果验票通过跳转到用户发起请求的地址,并设置cooke
  • Browser拿到cookie可以正常发起API调用请求

完整流程图如下:

前后端分离.png

1、CAS认证前端逻辑

  1. 前端APP的入口,检测localStorage中是否含有某个flag, 这里我们设置了一个名为authDate的key,value为认证的时间
  2. 当没有检测到authDate的时候,跳转前端配置的CAS server地址,并将CAS server的回调地址改写为接口后端的认证路由,这里我们在接口后端编写了一个名为serviceValidate的GET调用的方法

一级跳转:CAS回调地址

http://cas-server/authserver?service=http://backend-api/serviceValidate?service=http://vue-project

二级跳转:backend的serviceValidate 回调地址

http://backend-api/serviceValidate?service=http://vue-project

  1. 到CAS server认证成功后,会发放一个ticket添加到二级跳转后面,所以当CAS server重定向会浏览器时并跳转http://backend-api/serviceValidate的时候会带有两个参数,一个是service和ticket,其中service用于验票成功后跳转回前端的地址,ticket是CAS server发放的票据。

2、CAS认证后端逻辑(serviceValidate接口的逻辑)

  1. 当检测到ticket时,利用CAS client向CAS server进行验票或者自己通过CAS协议去进行验票,当验票通过,CAS server将会返回票据所属的用户信息
  2. 拿到用户信息后,设置cookie给当前调用域,届时前端调用接口的时候走cookie调用
  3. 根据service的地址,进行重定向返回到service

说明:

因为CAS server一级返回的地址会让浏览器重新重定向到接口服务的serviceValidate方法,所以浏览器在认证过程中可以得到接口的cookie,这个的且整个过程中不需要使用JS操作cookie,同时cookie也能设置且应该设置httponly

在这个过程中CAS server应该设置ticket只能使用一次, 防止ticket泄漏后可被多次使用

五、核心实现

1、前端相关配置

  • 入口文件main.js

在页面挂载前,先进行CAS认证,如果没有登录过,先挂载loading组件,如果通过则返回应用。

import Vue from 'vue'
import App from './App.vue'
import CASAuth from "./CasClient";
import Loading from './components/Loading.vue'
Vue.config.productionTip = false
if ( CASAuth(window.location.href ,true)) {
    new Vue({
      render: h => h(App),
    }).$mount('#app<img src="')"  width="70%" />
}
else{
  new Vue({
    render: h => h(Loading),
  }).$mount('#app')
}

  • 前端CAS认证流程如下图所示

未命名文件 (11).jpg

  • CASAuth.js
import config from "./config"	// 导入配置文件

function getUrlParams(url, slash = false) {
    let service_urlStr = url.split('?')
    let currentURL = service_urlStr[0].split('/');
    let obj = {"service": currentURL[0] + '//' + currentURL[2] + (slash ? '/' : '')};
    let urlStr = service_urlStr[1]
    if (!urlStr) {
        return obj
    }
    let paramsArr = urlStr.split('&')
    for (let i = 0, len = paramsArr.length; i < len; i++) {
        let arr = paramsArr[i].split('=')
        obj[arr[0]] = arr[1];
    }
    return obj
}

async function CASAuth(url, service_slash = false){
    let UrlPara = getUrlParams(url, service_slash);
    if (window.localStorage.getItem('authDate') !== null ){
        return true
    }
    let app_url=config.front_url
    let second_valid_url = `${config.backend_cas_valid_url}/serviceValidate?service=${app_url}`
    if ( ! Object.prototype.hasOwnProperty.call(UrlPara, 'ticket') ) { 
        window.location.href = `${config.cas_server}?service=${second_valid_url}`; 
        return false
    }
    if(Object.prototype.hasOwnProperty.call(UrlPara, 'ticket')){
        window.localStorage.setItem('authDate', new Date())
        window.location.href = window.location.href.split('?')[0]
        return true
    }
}



export async function RECASAuth(url, service_slash = false){ // 接口调用时如果检测到认证过期或者认证跳转时候调用
    let UrlPara = getUrlParams(url, service_slash);
    window.localStorage.removeItem('authDate');
    let app_url=config.front_url
    let second_valid_url = `${config.backend_cas_valid_url}/serviceValidate?service=${app_url}`
    if ( ! Object.prototype.hasOwnProperty.call(UrlPara, 'ticket') ) { 
        window.location.href = `${config.cas_server}?service=${second_valid_url}`;
    }
}

export default CASAuth;

参数说明:

cas_server:单点登录地址

backend_cas_valid_url:nginx反代的后端接口验证地址

app_url:nginx反代下的前端应用地址

  • config.js
let config = {
    "cas_server": "http://cas-server",		// CAS服务器地址
    "front_url":"http://vue-project",			// 前端应用地址
    "backend_cas_valid_url": "http://backend-api",	// 后端接口地址
}
export default config
  • 请求拦截和响应拦截
import axios from 'axios'
import {RECASAuth} from '@/CasClient'
import { notification } from 'ant-design-vue';
import config from "../config";
const service = axios.create({
  baseURL: config.backend_cas_valid_url
})
// axios请求拦截
service.interceptors.request.use(function (config) {
  config.headers['Accept']='application/json'
  return config
})
// axios 响应拦截
service.interceptors.response.use(async response => {
  return response
}, async err => {
   if (err.response.status==401){
    RECASAuth(window.location.href ,true)
  }
  else if (err.response.status==502){
     notification.error({
        message: '服务器内部错误',
        description: error.response.data.message,
      });
   }
  else {
      // 对其他错误进行处理
      notification.error({
        message: '网络错误',
        description: '请检查网络连接',
      });
   }
  return err
}
)


// promise接口调用


export default service

2、后端校验服务源代码(Catalyst perl)

=head2 serviceValidate
    后端验票并发放cookie
=cut

sub serviceValidate : Local {
    my ( $self, $c ) = @_;
    unless ( $c->authenticate({'service'=>$service_url})) {
        $c->stash->{RNT_CODE} = "401";
        $c->stash->{RTN_MSG} = '认证失败';
        $c->forward("View::JSON");
        return $c->detach();
    }
    my $service = $c->req->parameters->{service} || undef;
    if (!defined($service)){
        $c->stash->{RNT_CODE} = "403";
        $c->stash->{RTN_MSG} = 'service不存在,服务未知';
        $c->forward("View::JSON");
        return $c->detach();
    }
    $c->res->status(301);
    $c->log->debug( '重定向到前端服务 "'.$service )
      if $c->debug;
    return $c->res->redirect( $service.'?ticket='.$c->req->parameters->{ticket} );
}

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYeVTSJH' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片