SSO单点登录
本文是用jjwt 来做token,然后实现简单的单点登录的流程,主要用于了解单点登录流程,快速 理解单点登录
业务场景
现在假如我是公司老板,我手下有两个部门,一个是A部门,一个是B部门,AB两个部门功能不一,现在我想看登录A部门看A部门的业务情况,我得有账号密码,我登陆B部门看B部门的情况,我还得有账号密码,这样太麻烦了,我作为老板居然还要记着你们每个部门系统的账号密码,所以我现在要做一个系统, 一个母系统,要求我只要在这个母系统上登录之后,我在母系统里面点AB两个系统能跳转到AB两个系统里面,而且不用在额外登录,但是同时,也保留AB这两个系统的单独的登录情况
单点登录
那么我可以针对上面的业务情况我这样做,
老板->>母系统: 用老板账号登录母系统->>SSO认证中心: 验证身份SSO认证中心-->>母系统: 签发全局Token老板->>母系统: 点击"进入A系统"母系统->>子系统A: 携带加密Token跳转子系统A->>SSO认证中心: 验证Token有效性SSO认证中心-->>子系统A: 返回老板权限子系统A-->>老板: 自动进入A系统(无需登录)同理完成B系统访问
其实说白了sso 就扮演一个中间人的角色 ,所有的权限认证token办法都需要经过sso
在我们的demo中,来做一个小验证,我们的流程如下,模拟一个sso认证中心
老板》》登录母系统》》登陆通过返回一个sso——token 前端存到cookie中》》在母系统中访问A系统 》》自动验证SSOtoken 》》验证通过可以登录 ------------------- 如果单独访问A系统》》有单独的登录接口, 正常也应该是弄一个认证中心,这里我们也把token存到cookie中去
后端代码
jwt
package com.example.ssodemo.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;public class JwtUtil {// 使用安全的密钥生成方式private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);private static final long EXPIRATION_TIME = 3600000; // 1小时public static String generateToken(String username) {return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)).signWith(SECRET_KEY).compact();}public static Claims parseToken(String token) {return Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token).getBody();}public static boolean validateToken(String token) {try {Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token);return true;} catch (Exception e) {return false;}}
}
ssocontroller层
package com.example.ssodemo.controller;/*** @author wyz* @date 2025-04-27 21:24*/
import com.example.ssodemo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@CrossOrigin(origins = "*")
@RequestMapping("/sso")
public class SsoController {@PostMapping("/login")public String login(@RequestParam String username,@RequestParam String password) {log.info("sdengli");// 简单模拟登录验证if ("admin".equals(username) && "123456".equals(password)) {log.info("sdengli11");return JwtUtil.generateToken(username);}throw new RuntimeException("用户名或密码错误");}@GetMapping("/validate")public boolean validate(@RequestParam String token) {return JwtUtil.validateToken(token);}@GetMapping("/userinfo")public String getUserInfo(@RequestParam String token) {Claims claims = JwtUtil.parseToken(token);return claims.getSubject();}
}
子系统A
package com.example.ssodemo.controller;/*** @author wyz* @date 2025-04-27 21:26*/
import com.example.ssodemo.util.JwtUtil;
import org.springframework.web.bind.annotation.*;@RestController
@CrossOrigin(origins = "*")
@RequestMapping("/subsystemA")
public class SubsystemAController {// 子系统自己的登录接口@PostMapping("/login")public String subsystemLogin(@RequestParam String username,@RequestParam String password) {// 子系统自己的认证逻辑if ("user".equals(username) && "sub123".equals(password)) {return JwtUtil.generateToken(username);}throw new RuntimeException("子系统登录失败");}// 通过SSO令牌访问的接口@GetMapping("/sso-access")public String ssoAccess(@RequestParam String token) {if (JwtUtil.validateToken(token)) {String username = JwtUtil.parseToken(token).getSubject();return "欢迎 " + username + " 通过SSO访问子系统";}throw new RuntimeException("无效的SSO令牌");}
}
子系统B
package com.example.ssodemo.controller;/*** @author wyz* @date 2025-04-27 21:55*/import com.example.ssodemo.util.JwtUtil;
import org.springframework.web.bind.annotation.*;@RestController
@CrossOrigin(origins = "*")
@RequestMapping("/subsystemB")
public class SubsystemBController {// 子系统自己的登录接口@PostMapping("/login")public String subsystemLogin(@RequestParam String username,@RequestParam String password) {// 子系统自己的认证逻辑if ("user".equals(username) && "123".equals(password)) {return JwtUtil.generateToken(username);}throw new RuntimeException("子系统登录失败");}// 通过SSO令牌访问的接口@GetMapping("/sso-access")public String ssoAccess(@RequestParam String token) {if (JwtUtil.validateToken(token)) {String username = JwtUtil.parseToken(token).getSubject();return "欢迎 " + username + " 通过SSO访问子系统";}throw new RuntimeException("无效的SSO令牌");}
}
拦截器
package com.example.ssodemo.config;import com.example.ssodemo.util.JwtUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @author wyz* @date 2025-04-27 21:35*/
@Component
public class TokenHandler implements HandlerInterceptor {// 免认证路径private static final String[] EXCLUDE_PATHS = {"/subsystemA/login","/sso/login","/subsystemA/sso-access","/subsystemB/login","/subsystemB/sso-access"};
//前置处理@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {for (String excludePath : EXCLUDE_PATHS) {if (request.getRequestURI().contains(excludePath)) {return true;}}//不包含匹配的路径String token = request.getParameter("token");if (token != null && JwtUtil.validateToken(token)) {return true;}response.sendRedirect("/subsystem/login.html");return false;}}
前端代码
母系统
<!DOCTYPE html>
<html><head><title>母系统登录</title>
</head><body><h1>母系统登录</h1><input type="text" id="username" placeholder="用户名" value="admin"><input type="password" id="password" placeholder="密码" value="123456"><button onclick="login()">登录</button><p id="message"></p><div id="subsystems" style="display:none;"><h3>子系统链接</h3><a href="subsystemA.html">子系统A</a><a href="subsystemB.html">子系统B</a></div><script>function login() {const username = document.getElementById('username').value;const password = document.getElementById('password').value;fetch('http://localhost:8080/sso/login?username=' + username + '&password=' + password, {method: 'POST'}).then(response => {console.log("111")if (!response.ok) throw new Error('登录失败');return response.text();}).then(token => {localStorage.setItem('sso_token', token);document.getElementById('message').textContent = '登录成功!';document.getElementById('subsystems').style.display = 'block';}).catch(() => {document.getElementById('message').textContent = '登录失败';});}// 检查是否已有tokenif (localStorage.getItem('sso_token')) {document.getElementById('subsystems').style.display = 'block';document.getElementById('message').textContent = '您已登录';}</script>
</body></html>
子系统A
<!DOCTYPE html>
<html><head><title>子系统A</title>
</head><body><div id="login-form" style="display:none;"><h1>子系统A登录</h1><input type="text" id="username" placeholder="用户名" value="user"><input type="password" id="password" placeholder="密码" value="sub123"><button onclick="localLogin()">本地登录</button><p id="message"></p></div><div id="system-content" style="display:none;"><h1>欢迎访问子系统A</h1><p id="welcome-message"></p><button onclick="accessResource()">访问资源</button><p id="resource-message"></p></div><script>// 页面加载时检查SSO令牌window.onload = function () {const ssoToken = localStorage.getItem('sso_token');if (ssoToken) {// 验证SSO令牌fetch('http://localhost:8080/sso/validate?token=' + ssoToken).then(response => response.json()).then(valid => {if (valid) {// 令牌有效,直接进入系统showSystemContent('SSO');} else {// 令牌无效,清除并显示本地登录localStorage.removeItem('sso_token');showLoginForm();}});} else {// 无SSO令牌,显示本地登录showLoginForm();}};function showLoginForm() {document.getElementById('login-form').style.display = 'block';}function showSystemContent(loginType) {document.getElementById('system-content').style.display = 'block';document.getElementById('welcome-message').textContent =`您已通过${loginType}方式登录`;}function localLogin() {const username = document.getElementById('username').value;const password = document.getElementById('password').value;fetch('http://localhost:8080/subsystemA/login?username=' + username + '&password=' + password, {method: 'POST'}).then(response => {if (!response.ok) throw new Error('登录失败');return response.text();}).then(token => {localStorage.setItem('subsystemA_token', token);showSystemContent('本地');}).catch(() => {document.getElementById('message').textContent = '登录失败';});}function accessResource() {const ssoToken = localStorage.getItem('sso_token');const localToken = localStorage.getItem('subsystemA_token');const token = ssoToken || localToken;fetch('http://localhost:8080/subsystemA/sso-access?token=' + token).then(response => response.text()).then(result => {document.getElementById('resource-message').textContent = result;}).catch(() => {document.getElementById('resource-message').textContent = '访问资源失败';});}</script>
</body></html>
子系统B
<!DOCTYPE html>
<html><head><title>子系统A</title>
</head><body><div id="login-form" style="display:none;"><h1>子系统A登录</h1><input type="text" id="username" placeholder="用户名" value="user"><input type="password" id="password" placeholder="密码" value="sub123"><button onclick="localLogin()">本地登录</button><p id="message"></p></div><div id="system-content" style="display:none;"><h1>欢迎访问子系统A</h1><p id="welcome-message"></p><button onclick="accessResource()">访问资源</button><p id="resource-message"></p></div><script>// 页面加载时检查SSO令牌window.onload = function () {const ssoToken = localStorage.getItem('sso_token');if (ssoToken) {// 验证SSO令牌fetch('http://localhost:8080/sso/validate?token=' + ssoToken).then(response => response.json()).then(valid => {if (valid) {// 令牌有效,直接进入系统showSystemContent('SSO');} else {// 令牌无效,清除并显示本地登录localStorage.removeItem('sso_token');showLoginForm();}});} else {// 无SSO令牌,显示本地登录showLoginForm();}};function showLoginForm() {document.getElementById('login-form').style.display = 'block';}function showSystemContent(loginType) {document.getElementById('system-content').style.display = 'block';document.getElementById('welcome-message').textContent =`您已通过${loginType}方式登录`;}function localLogin() {const username = document.getElementById('username').value;const password = document.getElementById('password').value;fetch('http://localhost:8080/subsystemA/login?username=' + username + '&password=' + password, {method: 'POST'}).then(response => {if (!response.ok) throw new Error('登录失败');return response.text();}).then(token => {localStorage.setItem('subsystemA_token', token);showSystemContent('本地');}).catch(() => {document.getElementById('message').textContent = '登录失败';});}function accessResource() {const ssoToken = localStorage.getItem('sso_token');const localToken = localStorage.getItem('subsystemA_token');const token = ssoToken || localToken;fetch('http://localhost:8080/subsystemA/sso-access?token=' + token).then(response => response.text()).then(result => {document.getElementById('resource-message').textContent = result;}).catch(() => {document.getElementById('resource-message').textContent = '访问资源失败';});}</script>
</body></html>