2引言
市面上太多集成soo过于复杂 不适合小白理解 包括看了也是一头雾水 这个帖子就是教会你理解和掌握单点登入的流程和实现
理解单点登入的逻辑
用户访问你的系统 → 未登录则跳ISC登录页 → ISC登录成功重定向回你的系统(URL带ticket) → 前端自动拿ticket调后端鉴权 → 后端验ticket生成Token → 前端存Token完成登录
通俗一点
你的前端(http://localhost:8080)
↓(未登录)
ISC登录页(http://localhost:8082/isc/login)
↓(用户输入账号密码登录成功)
你的前端(http://localhost:8080?ticket=xxx)
↓(前端自动提取URL中的ticket)
你的后端(http://localhost:8081/api/isc/verify)
↓(后端验ticket→生成Token→返回用户信息)
你的前端(存储Token,标记登录成功)
集成SSO单点登录的步骤
确保项目中已引入Spring Boot和Spring Security依赖 springSecurity可有可无(主要是为了做没有token的情况用于跳转首页)
这快我就已ruoyi-amdin 框架为引 大家可以去官网下载 单体包springboot版本的
- 搭建环境
前端传ticket → 调用ISC校验接口 → 拿到用户信息 → 加工后返回
先做isc前端页面
注意点根据系统的不一样 取到的票据方法不同 列如正常的 是http://localhost:8080?ticket=xxx 那前端的取就是
const urlParams = new URLSearchParams(window.location.search);
const ticket = urlParams.get('ticket');
如果是 http://localhost:8080?ticket=xxx ?变成#号 就需要这样取 主要看用什么 一般在vue中就这种写法最好
// 检查URL中是否有ticket参数(ISC回调)
let ticket = route.query?.ticket
// 如果route.query没有获取到ticket,尝试从window.location.search获取
if (!ticket && window.location.search.includes('ticket=')) {
const urlParams = new URLSearchParams(window.location.search)
ticket = urlParams.get('ticket')
}
页面我就做好了 放在包里 可以直接取

2.第二步 登入成功 ,验证票据
首先会在url返回票据列如:localhost:8080/api/isc/auth?ticket=ISC-TICKET-uu8xtx26xk8-1766546331543
我们需要的就是ticket 这个票据 所以在登入代码中 检查她如果有票据就调用我们的接口 ,如果没有票据就会在第三方接口
前端代码的实现 js
const ticket = urlParams.get('ticket');
if (ticket) {
// 有ticket参数,说明是ISC回调
pageStatus.value = 'validating'
statusMessage.value = '正在验证身份信息...'
try {
// 调用ISC API接口验证ticket
console.log('开始验证ticket:', ticket)
const result = await callback( ticket )
console.log('验证结果:', result)
if (result.code === 200 && result.token) {
// ticket验证成功,使用原始架构设置token
setToken(result.token)
userStore.token = result.token
pageStatus.value = 'success'
statusMessage.value = '登录成功,正在跳转...'
// 直接跳转到目标页面,路由守卫会自动处理用户信息和权限生成
const query = route.query
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== "service") {
acc[cur] = query[cur]
}
return acc
}, {})
setTimeout(() => {
// 确保跳转的路径不是登录页面,避免循环
let finalPath = targetUrl //自己的ip
if (finalPath.startsWith('/login') || finalPath === '/') {
finalPath = '/index' // 如果目标路径是登录页或根路径,改为首页
}
console.log('准备跳转到:', finalPath, '查询参数:', otherQueryParams)
// 使用replace而不是push,避免浏览器历史记录中的登录页
router.replace({ path: finalPath, query: otherQueryParams })
}, 1000)
} else {
// ticket验证失败,显示错误信息
pageStatus.value = 'error'
statusMessage.value = '身份验证失败,请联系数服'
}
} catch (error) {
console.error('Ticket验证失败:', error)
// 出错时显示错误信息
pageStatus.value = 'error'
statusMessage.value = '验证过程出错,请在即时通联系数服'
}
}
})
那么后端代码的实现 拿到票据了就应该回调别人的系统去拿到用户信息-返回token给前端
setToken(result.token)
userStore.token = result.token 这块是若依的 -可以下载他的vue3版本
后端代码实现
引入依赖 spring
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JWT工具 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
Security 主要就是拦截全部的访问接口 必须要带token才可以正常操作 /*login 就是不需要所以 我们验证ticket 也需要这样操作 不拦截
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/login**","/api/isc/auth").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login();
}
}
3.实现
第一步 去拿到isc的用户信息 转换实体类或者是json
package com.example.iscdemo.demos.entity;
import lombok.Data;
/**
* ISC系统验证Ticket后返回的用户信息
*/
@Data
public class IscUserInfo {
/**
* 用户名(唯一标识)
*/
private String userName;
/**
* 用户昵称
*/
private String nickName;
/**
* 部门ID
*/
private String deptId;
}
2.在创建你自己数据库的用户表的实体类
package com.example.iscdemo.demos.entity;
import lombok.Data;
/**
* 本地系统用户实体(模拟数据库)
*/
@Data
public class SysUser {
private Long userId; // 模拟自增ID
private String userName; // 与ISC的userName一致
private String nickName;
private String deptId;
private String status = "0"; // 0正常
private String delFlag = "0"; // 0未删除
}
3.创建jwt 建议使用 rouyi集成的 有现成的jwt
package com.example.iscdemo.demos.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类
*/
@Component
public class JwtUtil {
/**
* JWT密钥(至少32位)
*/
@Value("${jwt.secret:abcdefghijklmnopqrstuvwxyz1234567890abcdef}")
private String secret;
/**
* Token过期时间:2小时
*/
@Value("${jwt.expire:7200000}")
private long expire;
/**
* 生成Token
*/
public String generateToken(String userName) {
Map<String, Object> claims = new HashMap<>();
claims.put("userName", userName);
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
return Jwts.builder()
.setClaims(claims)
.setSubject(userName)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expire))
.signWith(key)
.compact();
}
/**
* 解析Token(可选,供后续接口验证用)
*/
public Claims parseToken(String token) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
4.模拟ISC系统的Ticket验证服务
package com.example.iscdemo.demos.service;
import com.example.iscdemo.demos.entity.IscUserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* 模拟ISC系统的Ticket验证服务
*/
@Slf4j
@Service
public class IscAuthService {
/**
* 调用ISC系统验证Ticket
* @param ticket 前端传入的Ticket
* @return ISC返回的用户信息(验证失败返回null)
*/
public IscUserInfo validateTicket(String ticket) {
// 模拟ISC系统的验证逻辑
try {
// 1. 模拟网络请求延迟
Thread.sleep(200);
// 2. 模拟Ticket验证规则:仅允许以"ISC-TICKET-"开头的Ticket
if (ticket == null || !ticket.startsWith("ISC-TICKET-")) {
log.error("Ticket格式错误,验证失败:{}", ticket);
return null;
}
// 3. 模拟ISC系统返回用户信息(实际场景从ISC接口响应中解析)
IscUserInfo iscUserInfo = new IscUserInfo();
// 从Ticket中截取随机字符串作为用户名,或ISC返回真实用户名
String userName = "isc_" + UUID.randomUUID().toString().substring(0, 8);
iscUserInfo.setUserName(userName);
iscUserInfo.setNickName("ISC用户_" + userName);
iscUserInfo.setDeptId("1001"); // 模拟部门ID
iscUserInfo.setPhone("13800138000"); // 模拟手机号
log.info("ISC系统验证Ticket成功,返回用户信息:{}", iscUserInfo);
return iscUserInfo;
} catch (Exception e) {
log.error("调用ISC系统验证Ticket失败", e);
return null;
}
}
}
5.模拟数据库操作逻辑 一般如果票据返回过来 验证中 有这个用户 而你系统没有就要新增用户
package com.example.iscdemo.demos.service;
import com.example.iscdemo.demos.entity.IscUserInfo;
import com.example.iscdemo.demos.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
/**
* 模拟本地数据库的用户操作服务
*/
@Slf4j
@Service
public class LocalUserService {
/**
* 模拟数据库表:key=userName,value=SysUser
*/
private static final Map<String, SysUser> USER_MAP = new HashMap<>();
/**
* 模拟自增ID
*/
private static final AtomicLong ID_GENERATOR = new AtomicLong(1);
/**
* 根据用户名查询本地用户
*/
public SysUser getUserByUserName(String userName) {
SysUser sysUser = USER_MAP.get(userName);
if (sysUser != null) {
log.info("本地数据库查询到用户:{}", userName);
} else {
log.info("本地数据库未查询到用户:{}", userName);
}
return sysUser;
}
/**
* 新增用户到本地数据库
*/
public boolean addUser(IscUserInfo iscUserInfo) {
try {
SysUser sysUser = new SysUser();
sysUser.setUserId(ID_GENERATOR.getAndIncrement()); // 模拟自增ID
sysUser.setUserName(iscUserInfo.getUserName());
sysUser.setNickName(iscUserInfo.getNickName());
sysUser.setDeptId(iscUserInfo.getDeptId());
USER_MAP.put(iscUserInfo.getUserName(), sysUser);
log.info("本地数据库新增用户成功:{}", iscUserInfo.getUserName());
return true;
} catch (Exception e) {
log.error("本地数据库新增用户失败", e);
return false;
}
}
}
6.调用ISC校验接口 → 拿到用户信息-加工返回token
package com.example.iscdemo.demos.controller;
import com.example.iscdemo.demos.entity.IscUserInfo;
import com.example.iscdemo.demos.entity.SysUser;
import com.example.iscdemo.demos.service.IscAuthService;
import com.example.iscdemo.demos.service.LocalUserService;
import com.example.iscdemo.demos.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Ticket验证 + Token生成控制器
*/
@Slf4j
@RestController
public class TicketAuthController {
@Autowired
private IscAuthService iscAuthService;
@Autowired
private LocalUserService localUserService;
@Autowired
private JwtUtil jwtUtil;
/**
* 验证Ticket并生成Token
* @param ticket 前端传入的Ticket(示例:ISC-TICKET-uu8xtx26xk8-1766546331543)
* @return Token与用户信息
*/
@GetMapping("/auth/validate-ticket")
public ResponseEntity<Map<String, Object>> validateTicket(@RequestParam String ticket) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 调用ISC系统验证Ticket
IscUserInfo iscUserInfo = iscAuthService.validateTicket(ticket);
if (iscUserInfo == null) {
result.put("code", 401);
result.put("msg", "Ticket验证失败,请检查Ticket是否有效");
return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED);
}
// 2. 根据ISC返回的用户名查询本地数据库
SysUser localUser = localUserService.getUserByUserName(iscUserInfo.getUserName());
if (localUser == null) {
// 3. 本地数据库无该用户,执行新增
boolean addSuccess = localUserService.addUser(iscUserInfo);
if (!addSuccess) {
result.put("code", 500);
result.put("msg", "本地用户新增失败");
return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
}
// 新增后重新查询
localUser = localUserService.getUserByUserName(iscUserInfo.getUserName());
}
// 4. 生成JWT Token
String token = jwtUtil.generateToken(iscUserInfo.getUserName());
// 5. 构造返回结果
result.put("code", 200);
result.put("msg", "操作成功");
result.put("data", new HashMap<String, Object>() {{
put("token", token);
put("localUser", localUser); // 本地用户信息
put("iscUserInfo", iscUserInfo); // ISC返回的用户信息
}});
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (Exception e) {
log.error("Ticket验证与Token生成异常", e);
result.put("code", 500);
result.put("msg", "服务器内部错误");
return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
测试接口
GET http://localhost:8080/auth/validate-ticket?ticket=ISC-TICKET-uu8xtx26xk8-1766546331543
结果返回
、
{
"code": 200,
"msg": "操作成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImlzY185ZDRhNzU5NyIsInN1YiI6ImlzY185ZDRhNzU5NyIsImlhdCI6MTcxNjU1MDQyMCwiZXhwIjoxNzE2NTUzMDIwfQ.xxxx",
"localUser": {
"userId": 1,
"userName": "isc_9d4a7597",
"nickName": "ISC用户_isc_9d4a7597",
"deptId": "1001",
"status": "0",
"delFlag": "0"
},
"iscUserInfo": {
"userName": "isc_9d4a7597",
"nickName": "ISC用户_isc_9d4a7597",
"deptId": "1001",
"phone": "13800138000"
}
}
}
以上就是通俗易懂的 sso单点登入教程 不会的可以私聊 麻烦关注。谢谢
1154

被折叠的 条评论
为什么被折叠?



