SpringBoot-Vue第三方统一认证接口——调用与对接总结
基于SpringBoot-Vue的前后端分离项目,服务器部署在CentOS8操作系统上,与第三方进行统一认证接口的对接。第三方统一认证登录可以解决用户在浏览器打开多个web应用时需要输入多次用户名密码登录的问题,实现用户一次登录,多个系统使用的效果。Web登录过程Web页面集成统一认证的原理是:当在浏览器打开集成了统一认证的某应用的页面时,页面重定向跳转到统一认证登录界面,登录之后页面重新回到
基于SpringBoot-Vue的前后端分离项目,服务器部署在CentOS8操作系统上,与第三方进行统一认证接口的对接。第三方统一认证登录可以解决用户在浏览器打开多个web应用时需要输入多次用户名密码登录的问题,实现用户一次登录,多个系统使用的效果。
Web登录过程
Web页面集成统一认证的原理是:当在浏览器打开集成了统一认证的某应用的页面时,页面重定向跳转到统一认证登录界面,登录之后页面重新回到用户请求的目标页面,而此时,如果用户在相同浏览器再打开另外一个集成了统一认证的页面时,由于该页面也会跳转到统一认证登录页,因为是在相同浏览器,统一认证登录的session仍然有效,所以会自动验证用户已经登录,不需要重新登录,便可以自动跳转到用户访问真正的目标页面,实现单点登录。
web应用接入的登录流程如下:
- 当用户首次访问Web应用时,Web应用会将请求重定向到统一认证的登录页面,重定向的过程一般是重定向到登录接口,这个过程重定向需要传三个参数:
- client_id
- response_type
- redirect_uri
- 用户在登录页面输入用户名密码后提交登录,这个过程与Web应用无关,由统一认证服务管控。
- 统一认证服务会颁发授权码(code)和id_token给Web应用,并将用户重定向到Web应用。
- Web应用通过id_token验证用户身份,完成本地登录。验证id_token之后,从id_token的playload中即可获取用户信息
登录接口
URL | http://serverIp:port/serverPath/oauth2/authorize |
---|---|
调用方式 | GET |
参数 | redirect_uri:重定向地址 response_type:重定向的时候需要附加的用户登录信息,可选code或code id_token client_id:应用唯一标识 |
返回值 | 登录页面 |
属性说明 | 用户通过登录页面输入用户名和密码登录成功后,统一认证服务会将授权码和ID Token作为参数附加在redirect_uri后面并重定向到这个地址。 客户端可以通过ID Token获取用户信息,完成本地登录。 |
备注 | ID Token是一个使用HS256算法和ClientSecret作为密钥签名的JWT,客户端必须使用自己的ClientSecret验证ID Token的有效性。 |
前端页面跳转
项目使用的是RUOYI框架
当进入登录页面时,使用vue-router路由守卫进行拦截,判断cookie里是否存在本地token,不存在或者token已过期则跳转到第三方登录界面进行登录。
修改permission.js文件
// 路由守卫:主要用来通过跳转或取消的方式守卫路由
router.beforeEach((to, from, next) => {
NProgress.start() // 开启进度条
document.title = defSet.title
// 跳转到index携参token
if (to.path.indexOf('/index') !== -1) {
console.log("token设置");
if (to.query.localToken !== null && to.query.localToken !== undefined) {
setToken(to.query.localToken)
}
}
if (getToken()) {
console.log("有token,本地登录成功");
if (store.getters.roles.length === 0) {
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => {
// 拉取user_info
const roles = res.roles
store.dispatch('GenerateRoutes', {roles}).then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
console.log("路由表添加成功");
next({...to, replace: true}) // hack方法 确保addRoutes已完成
NProgress.done()
})
})
.catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
console.log("退出登录");
window.location.href = "http://serverIp:port/sso/oauth2/logout?post_logout_redirect_uri=http://localIp:port&response_type=code id_token&client_id=clientId" //外链地址
next({path: '/'})
// next()
})
})
} else {
next()
}
} else {
console.log("无本地token,进入品高界面登录");
// 带重定向地址跳转,登录成功后跳转回来,并在地址后附带用户登录参数
window.location.href = "http://serverIp:port/sso/oauth2/authorize?redirect_uri=http://localIp:port/stage-api/api/v1/thirdParty/checkTokenThenLogin&response_type=code id_token&client_id=clientId";
next(); // 放行
// next(false);
NProgress.done(); //关闭进度条
}
后端解密token并本地登录
后端接口checkTokenThenLogin,完成对返回token的解密获取用户信息,再检验本地是否有该用户,有则本地登录获取本地token。
/**
* 本地登录,检验id_token
*/
@ApiOperation("校验id_token,完成本地登录")
@GetMapping("/checkTokenThenLogin")
public void checkTokenThenLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 获取路径的参数部分code和id_token
String queryString = request.getQueryString();
if (queryString == null) {
response.sendRedirect("http://44.85.17.9/login");
}
String[] queryParams = queryString.split("&");
String token = "";
for (String queryParam : queryParams) {
if (queryParam.contains("id_token=")) {
token = queryParam.replace("id_token=", "");
}
}
System.out.println(token);
// 校验id_token
if (JwtUtil.verify(token)) {
// 获取json类型的payload
// 本地登录(品高能登录用户必然存在,若本地不存在账号,则重新获取个人用户信息并存入本地)
String username = (String) JwtUtil.getVal(token, "username"); // 用户登录账号
String name = (String) JwtUtil.getVal(token, "name"); // 用户名
System.out.println(username + ":" + name);
// 本地查询是否有该账号
SysUser sysUser = userService.selectUserByUserName(username);
if (sysUser == null) {
SysUser user = SysUser.build(username, name);
userService.insertUser(user);
}
String login = loginService.login(username, PASSWORD, null, null);
System.out.println(login);
response.sendRedirect("http://44.85.17.9/index?localToken=" + login);
} else {
response.sendRedirect("http://44.85.17.9/login");
}
}
前端获取到本地token,跳转到应用首页。
在permission.js中,登录的路由守卫之前,先把token保存到本地的session里。
// 跳转到index携参token
if (to.path.indexOf('/index') !== -1) {
console.log("token设置");
if (to.query.localToken !== null && to.query.localToken !== undefined) {
setToken(to.query.localToken)
}
}
后端登录接口,解密第三方token获取用户信息,使用该信息进行本地登录,获取本地token。接口将本地token作为参数拼接在跳转的地址里,前端响应拦截器拦截到后,setToken方法将本地token存入session。路由守卫getToken方法拿到本地token后,拉取用户的基本信息和路由权限,加载完跳转到应用首页。
Web注销过程
单点登录完成之后,当用户需要注销时,必须通过单点注销注销所有已经登录的应用系统。单点注销需要完成以下两步:
- 注销所有已登陆的系统
- 注销统一认证
流程如下:
- 这里的注销地址在浏览器直接重定向即可,接口说明请看注销接口。
- ua会在注销接口中直接返回一个注销页面,这个页面中包含了所有已登录的应用的注销地址。
- 注销所有已登录的应用
- 注销统一认证服务
- 重定向到最初传给注销接口的url。
注销接口
URL | http://serverIp:port/serverPath/oauth2/logout |
---|---|
调用方式 | GET |
参数 | post_logout_redirect_uri:可选,注销完成后跳转地址,没有传的话默认会跳转到统一认证根目录。 |
注销不需要写后端接口,只需要重定向到统一认证注销地址,项目里需要写重定向的位置:
- permission.js里,路由守卫拦截时,判断当前用户是否已拉取完user_info信息,如果有报错,则退出登录。
- 页面相应拦截器,当检测到本地token过期时,跳出重新登录提示,确认后需要跳转到第三方登录界面。
- 导航组件有退出登录的功能,需要匹配第三方注销跳转。
前端界面跳转
src\permission.js
// 路由守卫:主要用来通过跳转或取消的方式守卫路由
router.beforeEach((to, from, next) => {
NProgress.start() // 开启进度条
document.title = defSet.title
// 跳转到index携参token
if (to.path.indexOf('/index') !== -1) {
console.log("token设置");
if (to.query.localToken !== null && to.query.localToken !== undefined) {
setToken(to.query.localToken)
}
}
if (getToken()) {
console.log("有token,本地登录成功");
if (store.getters.roles.length === 0) {
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => {
// 拉取user_info
const roles = res.roles
store.dispatch('GenerateRoutes', {roles}).then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
console.log("路由表添加成功");
next({...to, replace: true}) // hack方法 确保addRoutes已完成
NProgress.done()
})
})
.catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
console.log("退出登录");
window.location.href = "http://serverIp:port/sso/oauth2/logout?post_logout_redirect_uri=http://localIp:port&response_type=code id_token&client_id=clientId" //外链地址
next({path: '/'})
// next()
})
})
} else {
next()
}
} else {
console.log("无本地token,进入品高界面登录");
window.location.href = "http://serverIp:port/sso/oauth2/authorize?redirect_uri=http://localIp:port/stage-api/api/v1/thirdParty/checkTokenThenLogin&response_type=code id_token&client_id=clientId";
next(); // 放行
// next(false);
NProgress.done(); //关闭进度条
}
src\utils\request.js
// 响应拦截器
service.interceptors.response.use(res => {
const code = res.data.code
if (code === 401) {
MessageBox.confirm(
'登录状态已过期,您可以继续留在该页面,或者重新登录',
'系统提示',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
store.dispatch('LogOut').then(() => {
// location.href = '/index';
// 重定位到外链地址
location.reload() // 为了重新实例化vue-router对象 避免bug
window.location.href = "http://serverIp:port/sso/oauth2/logout?post_logout_redirect_uri=http://localIp:port&response_type=code id_token&client_id=clientId" //外链地址
})
})
} else if (code !== 200) {
Notification.error({
title: res.data.msg
})
return Promise.reject('error')
} else {
return res.data
}
},
error => {
console.log('err' + error)
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
src\layout\components\Navbar.vue
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
location.reload()
// location.href = '/index';
window.location.href = "http://serverIp:port/sso/oauth2/logout?post_logout_redirect_uri=http://localIp:port&response_type=code id_token&client_id=clientId" //外链地址
})
})
})
}
}
更多推荐
所有评论(0)