Sa-Token SSO 前后端分离

1.简介

单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是比较流行的。单点登录——便是我们搭建统一认证中心的关键。

2.注意点

  1. sa-token-sso 虽然是个单独的插件,但其本质仍是对 Sa-Token 本身各个功能的组合使用,所以先熟练掌握 Sa-Token可有效降低 SSO 章节的学习压力。
  2. 相比单体系统的会话管理,SSO 登录与注销的整体链路较长,出现 bug 时调试步骤也更复杂,因此建议先通过 学习Sa-Token 打通各个技术细节,再正式集成到项目中。

3.Sa-Token-SSO 特性

  1. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝Ticket劫持、Token窃取等常见攻击手
  2. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:http://a.com?id=1&name=2,登录成功之后就变成了:http://a.com?id=1,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面。
  3. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决权限认证 + 单点登录问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合……
  4. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发。

4.搭建认证中心(sso-server)

1.首先创建一个SpringBoot项目(如果还不清楚怎么创建项目的可以参考这篇文章),创建好sso-server(认证中心)后,在项目的pom.xml文件中添加如下依赖

<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.35.0.RC</version>
</dependency>

<!-- Sa-Token 插件:整合SSO -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-sso</artifactId>
    <version>1.35.0.RC</version>
</dependency>
        
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-jackson</artifactId>
    <version>1.35.0.RC</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
可以对pom依赖进行酌情删减或者增加相关依赖,这里这是作为演示。

2.开放认证接口

  • 新建 SsoServerController,用于对外开放接口:
 - /**
 - Sa-Token-SSO Server端 Controller 
 */
@RestController
public class SsoServerController {

 /*
     * SSO-Server端:处理所有SSO相关请求 (下面的章节我们会详细列出开放的接口)
     */
    @RequestMapping("/sso/*")
    public Object ssoRequest() {
        return SaSsoProcessor.instance.serverDister();
    }

    /**
     * 配置SSO相关参数
     */
    @Autowired
    private void configSso(SaSsoConfig sso) {
        // 配置:登录处理函数 
        sso.setDoLoginHandle((name, pwd) -> {
            // 此处仅做模拟登录,真实环境应该查询数据进行登录 
            if ("sa".equals(name) && "123456".equals(pwd)) {
                StpUtil.login(10001);
                return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
            }
            return SaResult.error("登录失败!");
        });
    }
}

注意:

  • 在setDoLoginHandle函数里如果要获取name, pwd以外的参数,可通过SaHolder.getRequest().getParam("xxx")来获取,如:前端传了verifyCode字段,后端可以用SaHolder.getRequest().getParam("verifyCode")获取值。

3.授权重定向

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.dev33.satoken.sso.SaSsoConsts;
import cn.dev33.satoken.sso.SaSsoUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;

@RestController
public class H5Controller {
	
	/**
	 * 获取 redirectUrl 
	 */
	@RequestMapping("/sso/getRedirectUrl")
	private Object getRedirectUrl(String redirect, String mode, String client) {
		// 未登录情况下,返回 code=401 
		if(StpUtil.isLogin() == false) {
			return SaResult.code(401);
		}
		// 已登录情况下,构建 redirectUrl 
		if(SaSsoConsts.MODE_SIMPLE.equals(mode)) {
			// 模式一 
			SaSsoUtil.checkRedirectUrl(SaFoxUtil.decoderUrl(redirect));
			return SaResult.data(redirect);
		} else {
			// 模式二或模式三 
			String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), client, redirect);
			return SaResult.data(redirectUrl);
		}
	}

	// 全局异常拦截 
	@ExceptionHandler
	public SaResult handlerException(Exception e) {
		e.printStackTrace(); 
		return SaResult.error(e.getMessage());
	}
	
}

4.跨域过滤器配置

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 跨域过滤器 
 * @author click33 
 */
@Component
@Order(-200)
public class CorsFilter implements Filter {

	static final String OPTIONS = "OPTIONS";

	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		
		// 允许指定域访问跨域资源
		response.setHeader("Access-Control-Allow-Origin", "*");
		// 允许所有请求方式
		response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
		// 有效时间
		response.setHeader("Access-Control-Max-Age", "3600");
		// 允许的header参数
		response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");

		// 如果是预检请求,直接返回
		if (OPTIONS.equals(request.getMethod())) {
			System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
			response.getWriter().print("");
			return;
		}

		// System.out.println("*********************************过滤器被使用**************************");
		chain.doFilter(req, res);
	}

	@Override
	public void init(FilterConfig filterConfig) {
	}

	@Override
	public void destroy() {
	}

}

5.application.yml配置

# 端口
server:
    port: 9000

# Sa-Token 配置
sa-token: 
    # ------- SSO-模式一相关配置  (非模式一不需要配置) 
    # cookie: 
        # 配置 Cookie 作用域 
        # domain: stp.com 
        
    # ------- SSO-模式二相关配置 
    sso: 
        # Ticket有效期 (单位: 秒),默认五分钟 
        ticket-timeout: 300
        # 所有允许的授权回调地址
        allow-url: "*"
        
        # ------- SSO-模式三相关配置 (下面的配置在使用SSO模式三时打开)
        # 是否打开模式三 
        is-http: true
        # ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明) 
        
spring: 
    # Redis配置 (SSO模式一和模式二使用Redis来同步会话)
    redis:
        # Redis数据库索引(默认为0)
        database: 1
        # Redis服务器地址
        host: 127.0.0.1
        # Redis服务器连接端口
        port: 6379
        # Redis服务器连接密码(默认为空)
        password: 
        
forest: 
    # 关闭 forest 请求日志打印
    log-enabled: false

注意:

  • 如果你自己配置的redis有密码,在application.yml文件中password处修改为自己的密码。
  • 将 allow-url 改为你的回调地址,如:http://localhost:8080/#/sso-login,*代表允许所有回调地址,这里为了方便调试,设置为*。

5.搭建客户端(sso-client)

1、准备工作
首先修改hosts文件(C:\windows\system32\drivers\etc\hosts),添加以下IP映射,方便我们进行测试

127.0.0.1 sa-sso-server.com
127.0.0.1 sa-sso-client.com

2.创建客户端,同4.1。在pom.xml文件中添加如下依赖:

<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.35.0.RC</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-sso</artifactId>
    <version>1.35.0.RC</version>
</dependency>

<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-jackson</artifactId>
    <version>1.35.0.RC</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-alone-redis</artifactId>
    <version>1.35.0.RC</version>
</dependency>

3.yml配置

# 端口
server:
    port: 9001

# sa-token配置 
sa-token: 
    # SSO-相关配置
    sso:
        # SSO-Server端 统一认证地址 
        auth-url: http://127.0.0.1:5500/sso-auth.html
        # 是否打开单点注销接口
        is-slo: true
    
    # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
    alone-redis: 
        # Redis数据库索引
        database: 1
        # Redis服务器地址
        host: 127.0.0.1
        # Redis服务器连接端口
        port: 6379
        # Redis服务器连接密码(默认为空)
        password: 123456
        # 连接超时时间
        timeout: 10s
        lettuce:
            pool:
                # 连接池最大连接数
                max-active: 200
                # 连接池最大阻塞等待时间(使用负值表示没有限制)
                max-wait: -1ms
                # 连接池中的最大空闲连接
                max-idle: 10
                # 连接池中的最小空闲连接
                min-idle: 0

注意:

  • auth-url后填写你们的登录页面地址

4.创建 SSO-Client 端认证接口

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.dev33.satoken.sso.SaSsoProcessor;
import cn.dev33.satoken.sso.SaSsoUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;

@RestController
public class H5Controller {

	// 当前是否登录 
	@RequestMapping("/sso/isLogin")
	public Object isLogin() {
		return SaResult.data(StpUtil.isLogin());
	}
	
	// 返回SSO认证中心登录地址 
	@RequestMapping("/sso/getSsoAuthUrl")
	public SaResult getSsoAuthUrl(String clientLoginUrl) {
		String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, "");
		return SaResult.data(serverAuthUrl);
	}
	
	// 根据ticket进行登录 
	@RequestMapping("/sso/doLoginByTicket")
	public SaResult doLoginByTicket(String ticket) {
		System.out.println("触发ticket");
		Object loginId = SaSsoProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket");
		if(loginId != null) {
			StpUtil.login(loginId);
			return SaResult.data(StpUtil.getTokenValue());
		}
		return SaResult.error("无效ticket:" + ticket); 
	}

	// 全局异常拦截 
	@ExceptionHandler
	public SaResult handlerException(Exception e) {
		e.printStackTrace(); 
		return SaResult.error(e.getMessage());
	}
	
}

5.跨域过滤器配置

同4.4

搭建vue2

1.客户端

建议去gitee直接扒代码点这里

2.登录页(这里用html作为演示,你也可以用vue写登录页。)
任意文件夹新建前端项目:在根目录添加测试文件:index.html

<!DOCTYPE html>
<html lang="zh">
	<head>
		<title>Sa-SSO-Server 认证中心-登录</title>
		<meta charset="utf-8">
		<base th:href="@{/}" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
		<link rel="stylesheet" href="./login.css">
	</head>
	<body>
		<div class="view-box">
			<div class="bg-1"></div>
			<div class="content-box">
				<div class="login-box">
					<div class="from-box">
						<h2 class="from-title">Sa-SSO-Server 认证中心(前后端分离版)</h2>
						<div class="from-item">
							<input class="s-input" name="name" placeholder="请输入账号" />
						</div>
						<div class="from-item">
							<input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
						</div>
						<div class="from-item">
							<button class="s-input s-btn login-btn">登录</button>
						</div>
						<div class="from-item reset-box">
							<a href="javascript: location.reload();" >刷新</a>
						</div>
					</div>
				</div>
			</div>
			<!-- 底部 版权 -->
			<div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
				This page is provided by Sa-Token-SSO 
			</div>
		</div>

		<!-- scripts -->
		<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
		<script src="./login.js"></script>
		
	</body>
</html>

login.js内容如下:

// 服务端地址 
var baseUrl = "http://sa-sso-server.com:9000";

// sa 
var sa = {};

// 打开loading
sa.loading = function(msg) {
	layer.closeAll();	// 开始前先把所有弹窗关了
	return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});
};

// 隐藏loading
sa.hideLoading = function() {
	layer.closeAll();
};

// 封装一下Ajax
sa.ajax = function(url, data, successFn) {
	$.ajax({
		url: baseUrl + url,
		type: "post", 
		data: data,
		dataType: 'json',
		headers: {
			'X-Requested-With': 'XMLHttpRequest',
			'satoken': localStorage.getItem('satoken')
		},
		success: function(res){
			console.log('返回数据:', res);
			successFn(res);
		},
		error: function(xhr, type, errorThrown){
			if(xhr.status == 0){
				return alert('无法连接到服务器,请检查网络');
			}
			return alert("异常:" + JSON.stringify(xhr));
		}
	});
}

// ----------------------------------- 相关事件 -----------------------------------

// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码 
var pData = {
	client: getParam('client', ''), 
	redirect: "http://localhost:8080/#/sso-login", 
	mode: getParam('mode', '')
};
sa.ajax("/sso/getRedirectUrl", pData, function(res) {
	if(res.code == 200) {
		// 已登录,并且redirect地址有效,开始跳转  
		location.href = decodeURIComponent(res.data);
	} else if(res.code == 401) {
		console.log('未登录');
	} else {
		layer.alert(res.msg); 
	}
})

// 登录
$('.login-btn').click(function(){
	sa.loading("正在登录...");
	// 开始登录
	var data = {
		name: $('[name=name]').val(),
		pwd: $('[name=pwd]').val()
	};
	sa.ajax("/sso/doLogin", data, function(res) {
		sa.hideLoading();
		if(res.code == 200) {
			localStorage.setItem('satoken', res.data);
			layer.msg('登录成功', {anim: 0, icon: 6 }); 
			setTimeout(function() {
				location.reload();
			}, 800);
		} else {
			layer.msg(res.msg, {anim: 6, icon: 2 }); 
		}
	})
});


// 绑定回车事件
$('[name=name],[name=pwd]').bind('keypress', function(event){
	if(event.keyCode == "13") {
		$('.login-btn').click();
	}
});

// 输入框获取焦点
$("[name=name]").focus();

// 从url中查询到指定名称的参数值 
function getParam(name, defaultValue){
	var query = window.location.search.substring(1);
	var vars = query.split("&");
	for (var i=0;i<vars.length;i++) {
		var pair = vars[i].split("=");
		if(pair[0] == name){return pair[1] + (pair[2] ? '=' + pair[2] : '');}
	}
	return(defaultValue == undefined ? null : defaultValue);
}

// 打印信息 
var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/";
console.log(str);

测试项目

先启动Server服务端与Client服务端,再随便找个能预览html的工具打开前端项目(比如HBuilderX)然后启动vue2项目(npm run serve),点击vue2的访问地址。

已上传git,需要的可以点击这里

Logo

前往低代码交流专区

更多推荐