SpringBoot+Spring security 集成sso单点登录 详细教程

2025博客之星年度评选已开启 10w+人浏览 3.2k人参与

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版本的

  1. 搭建环境 
前端传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单点登入教程  不会的可以私聊 麻烦关注。谢谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值