之前做一个公司项目的时候甲方要求集成他们指定的CAS服务端实现登录,要求不影响原有业务。
CAS服务端提供的文档都是基于前后端不分离的应用,对前后端分离应用没有任何说明,找官方人问也是爱答不理的,近期正好有时间就想着研究下这个集成过程。
于是有了这篇文章,主要为了记录下集成过程和相关配置,方便后续类似的对接。
当然也希望能帮助到需要的朋友。
{
path: '/callback',
hidden: true
},
{
path: '/tologin',
hidden: true
},
这里只是为了路由守卫的拦截,可以不写vue页面,只需要在路由列表中添加定义即可,亲测无误。
import {getToken} from '@/utils/auth'; // get token from cookie
import getPageTitle from '@/utils/get-page-title'
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
import router from './router'
import store from './store'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
//是否支持cas登录的开关
const enableCasLogin = true
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
const hasToken = getToken()
//判断是否是去login页面
if(to.path === '/tologin') {
if(enableCasLogin){
//实际访问的cas登录地址
window.location.href = `http://localhost:9009/cas/login?service=http://localhost:8989/test1/index&redirect=${to.params.redirect}`
} else {
next(`/login`)
}
NProgress.done()
} else if (whiteList.indexOf(to.path) !== -1) {
next();
} else if (to.path === '/callback') {
if(!hasToken) {
await store.dispatch(`user/resetToken`)
}
next('/')
NProgress.done()
} else {
if(hasToken) {
next()
} else {
if(to.path === '/') {
next(`/tologin`)
} else {
next(`/tologin?redirect=${to.path}`)
}
}
NProgress.done()
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
import request from '@/utils/request'
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data
})
}
export function getCaptcha() {
return request({
url: '/auth/captcha',
method: 'get',
})
}
export function getCasToken() {
return request({
url: '/auth/getToken',
method: 'get',
})
}
export function logout() {
return request({
url: '/vue-admin-template/user/logout',
method: 'post'
})
}
import { getCasToken, login, logout } from '@/api/user'
import { resetRouter } from '@/router'
import { getToken, removeToken, setToken } from '@/utils/auth'
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: ''
}
}
const state = getDefaultState()
const mutations = {
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
}
}
const actions = {
// user login
login({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
const { data } = response
commit('SET_TOKEN', data.tokenType + " " + data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// user logout
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
removeToken() // must remove token first
commit('RESET_STATE')
getCasToken().then(response => {
const { data } = response
commit('SET_TOKEN', data.tokenType + " " + data.token)
commit('SET_NAME', data.user.username)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
module.exports = {
/**
* You will need to set publicPath if you plan to deploy your site under a sub path,
* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then publicPath should be set to "/bar/".
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
publicPath: '/',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: false,
devServer: {
port: port,
open: true,
overlay: {
warnings: false,
errors: true
},
// before: require('./mock/mock-server.js')
proxy: {
'/dev-api': {
target: 'http://localhost:8989',
pathRewrite: { '^/dev-api': '' }
}
}
},
configureWebpack: {
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
name: name,
resolve: {
alias: {
'@': resolve('src')
}
}
},
chainWebpack(config) {
// it can improve the speed of the first screen, it is recommended to turn on preload
config.plugin('preload').tap(() => [
{
rel: 'preload',
// to ignore runtime.js
// https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
include: 'initial'
}
])
// when there are many pages, it will cause too many meaningless requests
config.plugins.delete('prefetch')
// set svg-sprite-loader
config.module
.rule('svg')
.exclude.add(resolve('src/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
config
.when(process.env.NODE_ENV !== 'development',
config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime\..*\.js$/
}])
.end()
config
.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
config.optimization.runtimeChunk('single')
}
)
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.13version>
<relativePath/>
parent>
<groupId>com.zjtx.techgroupId>
<artifactId>auth-cas-apiartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>auth-cas-apiname>
<description>CAS APIdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>net.unicon.casgroupId>
<artifactId>cas-client-autoconfig-supportartifactId>
<version>2.3.0-GAversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
依赖中主要添加的是cas-client-autoconfig-support
这个jar包,提供了cas需要的一些配置和拦截器
server:
port: 8989
cas:
# 配置实际的cas地址
server-url-prefix: http://localhost:9009/cas
# 配置实际的cas登录地址
server-login-url: http://localhost:9009/cas/login
client-host-url: http://localhost:8989/
# 这里可以选择cas 和 cas3 区别是请求的部分地址不一样,如ticket验证的接口
validation-type: cas
# 拦截的URL地址
authentication-url-patterns:
- /*
spring:
jackson:
serialization:
FAIL_ON_EMPTY_BEANS: false
src/main/java/com/zjtx/tech/controller/AuthController.java
package com.zjtx.tech.contorller;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/auth")
public class AuthController {
@GetMapping("getToken")
public ResultBean<TokenUser> getToken(HttpServletRequest request){
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
System.out.println("assertion = " + assertion.getPrincipal().getAttributes());
String username = assertion.getPrincipal().getName();
System.out.println(username);
//这里仅为了演示直接new了一个简单对象返回给前端
SimpleUserBean user = new SimpleUserBean("1", username, "123456", "456789");
return new ResultBean<>(200, "success", new TokenUser(user, "123456", "Bearer"));
}
}
src/main/java/com/zjtx/tech/controller/TestController.java
package com.zjtx.tech.contorller;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
public class TestController {
@GetMapping("test1/index")
public void index(HttpServletRequest request, HttpServletResponse resp) throws IOException {
String token = request.getParameter("congress");
System.out.println("congress : " + token);
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
System.out.println("assertion = " + assertion.getPrincipal().getAttributes());
String username = assertion.getPrincipal().getName();
System.out.println(username);
resp.sendRedirect("http://localhost:9528/#/callback");
}
@GetMapping("test1/index1")
public String index1(HttpServletRequest request) {
String token = request.getParameter("token");
System.out.println("token : " + token);
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
String username = assertion.getPrincipal().getName();
System.out.println(username);
return "test index cas拦截正常,登录账号:" + username;
}
/**
* 不走cas认证,无法获取登录信息
*
* @param request
* @return
*/
@GetMapping("test1/index2")
public String index2(HttpServletRequest request) {
return "cas 未拦截";
}
}
涉及到的简单bean就不在此列举了。
本文主要记录了前后端分离场景下集成CAS单点登录的基本流程。
作为记录的同时也希望能帮助到需要的朋友,有任何疑问欢迎留言评论。
创作不易,欢迎一键三连~