You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
28 KiB
28 KiB
SSO 统一身份认证集成方案(前后端分离 CAS 模式)
一、方案概述
1.1 架构设计
采用前后端分离的 CAS 单点登录模式,流程如下:
用户访问 → 前端检测未登录 → 重定向到 SSO 服务端 → 用户认证
→ SSO 返回票据 → 后端验证票据 → 生成本地 Token
→ 返回前端 → 后续请求使用本地 Token
1.2 为什么需要生成本地 Token?
原因:
- 性能考虑:只需首次验证调用 SSO 服务器,后续请求本地验证
- 解耦考虑:SSO 短暂故障不影响已登录用户
- 兼容性:与现有 RuoYi 框架的 Token 机制无缝集成
- 灵活性:可独立管理本系统的会话和权限
SSO 票据 vs 本地 Token:
- SSO 票据 = 护照(跨系统通用,用于首次认证)
- 本地 Token = 身份证(系统内使用,用于会话维持)
二、前端代码实现
2.1 新增文件:src/api/sso.js
import request from '@/utils/request'
/**
* 获取 SSO 登录 URL
* @param {string} redirect - 重定向地址
*/
export function getSsoLoginUrl(redirect) {
return request({
url: '/sso/loginUrl',
method: 'get',
params: { redirect },
headers: {
isToken: false
}
})
}
/**
* SSO 回调处理,验证票据并登录
* @param {Object} data - 包含 ticket 等参数
*/
export function ssoCallback(data) {
return request({
url: '/sso/callback',
method: 'post',
data: data,
headers: {
isToken: false
}
})
}
/**
* SSO 登出
*/
export function ssoLogout() {
return request({
url: '/sso/logout',
method: 'post'
})
}
2.2 新增文件:src/views/sso/login.vue
<!--
* SSO 统一身份认证登录页面
-->
<template>
<div class="sso-login-container">
<div class="sso-login-box" v-loading="loading" element-loading-text="正在跳转统一身份认证平台...">
<div class="loading-content">
<i class="el-icon-loading loading-icon"></i>
<p class="loading-text">正在跳转到统一身份认证平台,请稍候...</p>
<p class="loading-hint">如果长时间没有跳转,请检查网络连接或尝试普通登录</p>
</div>
</div>
</div>
</template>
<script>
import { getSsoLoginUrl } from '@/api/sso'
export default {
name: 'SsoLogin',
data() {
return {
loading: true,
redirect: undefined
}
},
created() {
// 获取重定向地址
this.redirect = this.$route.query && this.$route.query.redirect
// 发起 SSO 登录
this.handleSsoLogin()
},
methods: {
async handleSsoLogin() {
try {
// 调用后端接口获取 SSO 登录 URL
const res = await getSsoLoginUrl(this.redirect)
if (res.code === 200 && res.data) {
const loginUrl = res.data
console.log('SSO 登录 URL:', loginUrl)
// 重定向到 SSO 服务器登录页面
window.location.href = loginUrl
} else {
throw new Error(res.msg || '获取 SSO 登录地址失败')
}
} catch (error) {
console.error('SSO 登录异常:', error)
this.$message({
message: error.message || 'SSO 登录异常,请尝试普通登录',
type: 'error',
duration: 5000
})
// 延迟跳转到普通登录页
setTimeout(() => {
this.$router.push({
path: '/login',
query: this.redirect ? { redirect: this.redirect } : {}
})
}, 2000)
} finally {
this.loading = false
}
}
}
}
</script>
<style lang="scss" scoped>
.sso-login-container {
width: 100%;
height: 100%;
background: url('../assets/images/loginbg.jpg') no-repeat;
background-size: 100% 100%;
display: flex;
justify-content: center;
align-items: center;
}
.sso-login-box {
background: url("../assets/images/loginbox.png") no-repeat center bottom;
width: 1920px;
height: 1080px;
position: absolute;
top: 0;
right: 0;
margin-left: -699px;
z-index: 1;
background-size: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loading-content {
text-align: center;
color: #fff;
background: rgba(7, 34, 111, 0.9);
padding: 40px 60px;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.loading-icon {
font-size: 80px;
color: #409EFF;
animation: rotating 2s linear infinite;
display: block;
margin-bottom: 20px;
}
.loading-text {
font-size: 24px;
font-weight: bold;
margin-bottom: 15px;
background: linear-gradient(to bottom, #ddfbff, #2fa9ff);
-webkit-background-clip: text;
color: transparent;
display: block;
}
.loading-hint {
font-size: 16px;
color: #ccc;
line-height: 1.6;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
2.3 新增文件:src/views/sso/callback.vue
<!--
* SSO 统一身份认证回调页面
* 处理 SSO 返回的票据并完成登录
-->
<template>
<div class="sso-callback-container">
<div class="sso-callback-box" v-loading="loading" element-loading-text="正在完成登录验证...">
<div class="loading-content">
<i class="el-icon-check loading-icon" :class="{ 'icon-success': success }"></i>
<p class="loading-text">{{ loadingText }}</p>
<p class="loading-hint">{{ loadingHint }}</p>
</div>
</div>
</div>
</template>
<script>
import { ssoCallback } from '@/api/sso'
import { setToken } from '@/utils/auth'
export default {
name: 'SsoCallback',
data() {
return {
loading: true,
success: false,
loadingText: '正在验证 SSO 票据...',
loadingHint: '请稍候...'
}
},
created() {
// 处理回调
this.handleCallback()
},
methods: {
async handleCallback() {
try {
// 从 URL 参数获取 ticket
const query = this.$route.query
const ticket = query.ticket
if (!ticket) {
throw new Error('未找到 SSO 票据,请重新登录')
}
const redirect = query.redirect || '/questionModule/index'
console.log('SSO 回调,ticket:', ticket)
// 调用后端验证接口
const res = await ssoCallback({
ticket,
redirect
})
// 验证响应
if (res.code === 200 && res.data && res.data.token) {
// 保存 token
setToken(res.data.token)
console.log('SSO 登录成功,token 已保存')
this.success = true
this.loadingText = '登录成功!'
this.loadingHint = '正在跳转到系统首页...'
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
this.$router.push(redirect).catch(() => {})
}, 1000)
} else {
throw new Error(res.msg || 'SSO 验证失败')
}
} catch (error) {
console.error('SSO 回调错误:', error)
this.loadingText = '登录失败'
this.loadingHint = error.message || '请稍后重试或联系管理员'
this.$message({
message: error.message || 'SSO 登录失败',
type: 'error',
duration: 5000
})
// 延迟跳转到登录页
setTimeout(() => {
this.$router.push('/login')
}, 2000)
} finally {
this.loading = false
}
}
}
}
</script>
<style lang="scss" scoped>
.sso-callback-container {
width: 100%;
height: 100%;
background: url('../assets/images/loginbg.jpg') no-repeat;
background-size: 100% 100%;
display: flex;
justify-content: center;
align-items: center;
}
.sso-callback-box {
background: url("../assets/images/loginbox.png") no-repeat center bottom;
width: 1920px;
height: 1080px;
position: absolute;
top: 0;
right: 0;
margin-left: -699px;
z-index: 1;
background-size: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loading-content {
text-align: center;
color: #fff;
background: rgba(7, 34, 111, 0.9);
padding: 40px 60px;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.loading-icon {
font-size: 80px;
color: #409EFF;
animation: rotating 2s linear infinite;
display: block;
margin-bottom: 20px;
&.icon-success {
color: #67C23A;
animation: none;
}
}
.loading-text {
font-size: 24px;
font-weight: bold;
margin-bottom: 15px;
background: linear-gradient(to bottom, #ddfbff, #2fa9ff);
-webkit-background-clip: text;
color: transparent;
display: block;
}
.loading-hint {
font-size: 16px;
color: #ccc;
line-height: 1.6;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
2.4 修改文件:public/static/config.js
在文件末尾添加 SSO 配置:
const MyConfig = {
BASE_API: 'http://localhost:8050/gd_dataserver',
FILE_BASE_URL: 'http://localhost:8050/gd_dataserver',
TEMPLATE_FILE_URL: 'http://localhost:9527/asset/template',
MQTTconnection: {
host: "bjsdqc.cn",
port: "8083",
endpoint: "/mqtt",
},
apk_url: "http://bjsdqc.cn:9527/asset/apk/gd_v1.7.2.apk",
isFlat: false,
// ========== SSO 统一身份认证配置 ==========
SSO: {
// 是否启用 SSO 登录(生产环境根据实际需求配置)
enabled: true,
// SSO 服务器基础 URL(需要根据实际 SSO 服务器地址修改)
serverUrl: 'http://localhost:8080/sso',
// 本地回调地址(前端自动获取当前域名)
callbackUrl: window.location.origin + '/sso/callback'
}
}
2.5 修改文件:src/views/login.vue
(1)导入配置文件
在 <script> 标签中添加:
import MyConfig from '@/../public/static/config'
(2)添加 data 属性
在 data() 函数的 return 对象中添加:
ssoEnabled: MyConfig.SSO?.enabled || false
(3)添加 SSO 登录按钮
在模板中的 .loginname 后面添加:
<!-- SSO 统一身份认证登录按钮 -->
<div v-if="ssoEnabled" class="sso-login-section">
<el-button
type="primary"
icon="el-icon-lock"
@click="handleSsoLogin"
round
size="medium"
style="width: 80%; margin-bottom: 20px; background: linear-gradient(to right, #409EFF, #67C23A); border: none;"
>
统一身份认证登录
</el-button>
<el-divider>或者使用账号密码登录</el-divider>
</div>
(4)添加 SSO 登录方法
在 methods 中添加:
/**
* SSO 登录处理
*/
handleSsoLogin() {
// 跳转到 SSO 登录页,携带重定向参数
this.$router.push({
path: '/sso/login',
query: { redirect: this.redirect }
})
},
(5)添加样式
在 <style> 末尾添加:
// SSO 登录区域样式
.sso-login-section {
text-align: center;
margin-bottom: 15px;
::v-deep .el-divider__text {
color: #ccc;
font-size: 14px;
}
}
2.6 修改文件:src/router/index.js
在 constantRoutes 数组中,/register 路由后面添加:
{
path: '/sso',
hidden: true,
children: [
{
path: 'login',
component: () => import('@/views/sso/login'),
name: 'SsoLogin',
meta: { title: 'SSO 登录' }
},
{
path: 'callback',
component: () => import('@/views/sso/callback'),
name: 'SsoCallback',
hidden: true
}
]
},
2.7 修改文件:src/permission.js
修改白名单数组:
const whiteList = ['/login', '/register', '/checkToken', '/sso/login', '/sso/callback']
三、后端代码实现
3.1 新增文件:SsoController.java
路径:chenshanyouku/ruoyi-admin/src/main/java/com/chenshan/common/web/controller/SsoController.java
package com.chenshan.common.web.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.chenshan.common.core.domain.R;
import com.chenshan.sso.service.SsoService;
/**
* SSO 统一身份认证控制器
*/
@RestController
@RequestMapping("/sso")
public class SsoController {
@Autowired
private SsoService ssoService;
/**
* 获取 SSO 登录 URL
*/
@GetMapping("/loginUrl")
public R<String> getLoginUrl(@RequestParam(required = false) String redirect) {
try {
String loginUrl = ssoService.getLoginUrl(redirect);
return R.ok(loginUrl);
} catch (Exception e) {
return R.fail("获取 SSO 登录地址失败:" + e.getMessage());
}
}
/**
* SSO 回调处理
*/
@PostMapping("/callback")
public R<Map<String, Object>> callback(@RequestParam String ticket,
@RequestParam(required = false) String redirect) {
try {
Map<String, Object> result = ssoService.validateTicketAndLogin(ticket, redirect);
if (result != null && result.containsKey("token")) {
return R.ok(result);
} else {
return R.fail("SSO 验证失败");
}
} catch (Exception e) {
return R.fail("SSO 登录失败:" + e.getMessage());
}
}
/**
* SSO 登出
*/
@PostMapping("/logout")
public R<Void> logout() {
try {
ssoService.ssoLogout();
return R.ok();
} catch (Exception e) {
return R.fail("SSO 登出失败:" + e.getMessage());
}
}
}
3.2 新增文件:SsoService.java
路径:chenshanyouku/ruoyi-admin/src/main/java/com/chenshan/sso/service/SsoService.java
package com.chenshan.sso.service;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.chenshan.common.utils.http.HttpUtils;
import com.chenshan.common.utils.json.JSONUtils;
import com.chenshan.system.domain.SysUser;
import com.chenshan.system.service.ISysUserService;
import com.chenshan.token.service.TokenService;
/**
* SSO 统一身份认证服务
*/
@Service
public class SsoService {
@Value("${sso.server-url}")
private String ssoServerUrl;
@Value("${sso.app-id}")
private String appId;
@Value("${sso.app-secret}")
private String appSecret;
@Value("${sso.redirect-uri}")
private String redirectUri;
private final ISysUserService userService;
private final TokenService tokenService;
public SsoService(ISysUserService userService, TokenService tokenService) {
this.userService = userService;
this.tokenService = tokenService;
}
/**
* 获取 SSO 登录 URL
*/
public String getLoginUrl(String redirect) throws Exception {
String targetUrl = StringUtils.hasText(redirect)
? redirect
: redirectUri;
Map<String, String> params = new HashMap<>();
params.put("appId", appId);
params.put("redirectUri", URLEncoder.encode(targetUrl, "UTF-8"));
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
String sign = generateSign(params, appSecret);
params.put("sign", sign);
StringBuilder loginUrl = new StringBuilder(ssoServerUrl);
loginUrl.append("/login?");
for (Map.Entry<String, String> entry : params.entrySet()) {
loginUrl.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("&");
}
return loginUrl.substring(0, loginUrl.length() - 1);
}
/**
* 验证 SSO 票据并登录
*/
public Map<String, Object> validateTicketAndLogin(String ticket, String redirect) throws Exception {
// 1. 验证 SSO 票据
SsoUser ssoUser = validateTicket(ticket);
if (ssoUser == null || !StringUtils.hasText(ssoUser.getUserId())) {
throw new Exception("SSO 票据无效或已过期");
}
// 2. 查找或创建用户
SysUser localUser = findOrCreateUser(ssoUser);
if (localUser == null) {
throw new Exception("用户不存在且无法自动创建");
}
// 3. 生成本地 Token
String token = tokenService.createToken(localUser.getUserId());
// 4. 准备返回结果
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("userInfo", localUser);
return result;
}
/**
* 验证 SSO 票据
*/
private SsoUser validateTicket(String ticket) throws Exception {
Map<String, String> params = new HashMap<>();
params.put("ticket", ticket);
params.put("appId", appId);
params.put("appSecret", appSecret);
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
String sign = generateSign(params, appSecret);
params.put("sign", sign);
String response = HttpUtils.sendPost(ssoServerUrl + "/validate", params);
Map<String, Object> responseMap = JSONUtils.parseObject(response, Map.class);
Integer code = (Integer) responseMap.get("code");
if (code == null || code != 200) {
throw new Exception("SSO 票据验证失败:" + responseMap.get("msg"));
}
Object data = responseMap.get("data");
String userJson = JSONUtils.toJSONString(data);
return JSONUtils.parseObject(userJson, SsoUser.class);
}
/**
* 查找或创建用户
*/
private SysUser findOrCreateUser(SsoUser ssoUser) {
// 1. 通过 SSO 用户 ID 查找
SysUser user = userService.selectUserBySsoId(ssoUser.getUserId());
if (user != null) {
updateUserFromSso(user, ssoUser);
return user;
}
// 2. 通过用户名查找
user = userService.selectUserByUserName(ssoUser.getUsername());
if (user != null) {
associateSsoId(user, ssoUser.getUserId());
updateUserFromSso(user, ssoUser);
return user;
}
// 3. 创建新用户
return createUserFromSso(ssoUser);
}
/**
* 从 SSO 用户创建本地用户
*/
private SysUser createUserFromSso(SsoUser ssoUser) {
SysUser user = new SysUser();
user.setUserName(ssoUser.getUsername());
user.setNickName(ssoUser.getRealName());
user.setEmail(ssoUser.getEmail());
user.setPhonenumber(ssoUser.getPhone());
user.setSex(ssoUser.getGender());
user.setStatus("0");
user.setRemark("SSO 自动创建用户");
userService.insertUser(user);
return user;
}
/**
* 更新用户信息
*/
private void updateUserFromSso(SysUser user, SsoUser ssoUser) {
user.setNickName(ssoUser.getRealName());
user.setEmail(ssoUser.getEmail());
user.setPhonenumber(ssoUser.getPhone());
user.setSex(ssoUser.getGender());
userService.updateUser(user);
}
/**
* 关联 SSO ID
*/
private void associateSsoId(SysUser user, String ssoId) {
// 需要在 sys_user 表中添加 sso_id 字段
// user.setSsoId(ssoId);
// userService.updateUser(user);
}
/**
* SSO 登出
*/
public void ssoLogout() throws Exception {
// 调用 SSO 服务器的登出接口
}
/**
* 生成签名(需根据实际 SSO 服务器实现)
*/
private String generateSign(Map<String, String> params, String secret) {
// TODO: 根据实际 SSO 服务器的签名算法实现
// 示例代码:
/*
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder sb = new StringBuilder();
for (String key : keys) {
sb.append(key).append("=").append(params.get(key)).append("&");
}
sb.append("secret=").append(secret);
return DigestUtils.md5Hex(sb.toString());
*/
return "SIGNATURE_PLACEHOLDER";
}
/**
* SSO 用户信息类
*/
public static class SsoUser {
private String userId;
private String username;
private String realName;
private String email;
private String phone;
private String gender;
// Getters and Setters
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getRealName() { return realName; }
public void setRealName(String realName) { this.realName = realName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
}
}
3.3 配置文件:application.yml
在后端项目的配置文件中添加:
# SSO 统一身份认证配置
sso:
# SSO 服务器地址(⚠️ 必须修改为实际地址)
server-url: http://localhost:8080/sso
# SP 应用 ID(⚠️ 从 SSO 管理员获取)
app-id: chenshan_oil_system
# SP 应用密钥(⚠️ 从 SSO 管理员获取)
app-secret: your-app-secret-here
# 重定向 URL(⚠️ 修改为实际前端地址)
redirect-uri: http://localhost:9527/sso/callback
# 是否启用 SSO
enabled: true
# 票据有效期(秒)
ticket-validity: 300
# 签名算法(⚠️ 根据 SSO 服务器要求)
sign-algorithm: MD5
3.4 数据库迁移脚本
-- ============================================
-- SSO 统一身份认证 - 数据库迁移脚本
-- ============================================
-- 1. 在 sys_user 表中添加 SSO ID 字段
ALTER TABLE sys_user
ADD COLUMN sso_id VARCHAR(64) DEFAULT NULL COMMENT 'SSO 系统用户 ID' AFTER user_id;
-- 2. 为 sso_id 字段添加索引
CREATE INDEX idx_sso_id ON sys_user(sso_id);
-- 3. 创建 SSO 日志表(可选)
CREATE TABLE IF NOT EXISTS sys_sso_log (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键 ID',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户 ID',
`username` varchar(30) DEFAULT '' COMMENT '用户名',
`sso_ticket` varchar(128) DEFAULT '' COMMENT 'SSO 票据',
`login_time` datetime DEFAULT NULL COMMENT '登录时间',
`ip_address` varchar(50) DEFAULT '' COMMENT '登录 IP',
`status` char(1) DEFAULT '0' COMMENT '状态(0 成功 1 失败)',
`message` varchar(500) DEFAULT '' COMMENT '消息',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_login_time` (`login_time`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='SSO 登录日志表';
3.5 后端补充代码
在 ISysUserService.java 中添加:
/**
* 通过 SSO ID 查询用户
* @param ssoId SSO 用户 ID
* @return 用户信息
*/
SysUser selectUserBySsoId(String ssoId);
在对应的 Mapper XML 中添加:
<select id="selectUserBySsoId" parameterType="String" resultMap="SysUserResult">
select user_id, user_name, nick_name, user_type, email, phonenumber, sex, status
from sys_user
where sso_id = #{ssoId}
</select>
四、集成步骤清单
4.1 准备工作
从单点登录 Demo 获取以下信息:
- SSO 服务器地址
- App ID
- App Secret
- 签名算法说明
- 接口文档
4.2 前端实施步骤
- ✅ 创建
src/api/sso.js - ✅ 创建
src/views/sso/login.vue - ✅ 创建
src/views/sso/callback.vue - ✅ 修改
public/static/config.js(添加 SSO 配置) - ✅ 修改
src/views/login.vue(添加 SSO 按钮) - ✅ 修改
src/router/index.js(添加 SSO 路由) - ✅ 修改
src/permission.js(添加 SSO 白名单)
4.3 后端实施步骤
- ✅ 创建
SsoController.java - ✅ 创建
SsoService.java - ✅ 在
application.yml添加 SSO 配置 - ✅ 执行数据库迁移脚本
- ✅ 在
ISysUserService添加selectUserBySsoId()方法 - ✅ 实现
generateSign()方法(根据实际 SSO 服务器)
4.4 测试验证
# 启动后端
cd D:\gitworkspace\chenshan\chenshanyouku
# 运行启动命令
# 启动前端
cd D:\gitworkspace\chenshan-client\web
npm run dev
测试流程:
- 访问登录页,查看是否有"统一身份认证登录"按钮
- 点击按钮,检查是否跳转到 SSO 服务器
- 在 SSO 服务器登录
- 检查是否正确回调
- 检查是否成功获取 token
- 验证能否正常访问系统
五、关键配置说明
5.1 前端配置
| 配置项 | 位置 | 说明 |
|---|---|---|
| SSO 服务器地址 | public/static/config.js |
必须与后端一致 |
| enabled | config.js |
控制是否显示 SSO 按钮 |
5.2 后端配置
| 配置项 | 说明 | 注意事项 |
|---|---|---|
| sso.server-url | SSO 服务器地址 | 生产环境用 HTTPS |
| sso.app-id | SP 应用 ID | 从 SSO 管理员获取 |
| sso.app-secret | SP 应用密钥 | 妥善保管,不能提交 Git |
| sso.redirect-uri | 回调地址 | 必须与 SSO 服务器配置一致 |
| sso.sign-algorithm | 签名算法 | 必须与 SSO 服务器一致 |
5.3 数据库配置
需要添加的字段:
sys_user.sso_id:存储 SSO 系统的用户 ID
六、常见问题及解决方案
6.1 跨域问题
现象:SSO 回调时出现跨域错误
解决:
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/sso': {
target: process.env.VUE_APP_SSO_SERVER_URL,
changeOrigin: true,
secure: false
}
}
}
}
6.2 签名验证失败
排查步骤:
- 检查签名算法是否与 SSO 服务器一致
- 检查参数排序是否正确
- 检查 app-secret 是否正确
- 检查时间戳是否在有效期内
6.3 票据验证失败
可能原因:
- 票据已过期
- 票据已被使用
- SSO 服务器时间不同步
- 网络问题导致验证请求失败
七、安全注意事项
- HTTPS 传输:生产环境必须使用 HTTPS
- 密钥保护:app-secret 不能提交到版本控制系统
- 日志脱敏:SSO 票据不能在日志中明文显示
- 会话管理:设置合理的 session 超时时间
- 防重放攻击:使用时间戳和 nonce 机制
八、部署上线
8.1 开发环境
sso:
server-url: http://dev-sso.example.com/sso
app-id: chenshan_dev
app-secret: dev-secret-key
redirect-uri: http://localhost:9527/sso/callback
enabled: true
8.2 生产环境
sso:
server-url: https://sso.example.com/sso
app-id: chenshan_prod
app-secret: ${SSO_APP_SECRET} # 从环境变量读取
redirect-uri: https://your-domain.com/sso/callback
enabled: true
文档生成时间:2026-03-31
适用项目:陈山智慧油库系统
SSO 版本:SPForJava1.5-V3.8