refactor: 调整 solon-sso 示例代码

This commit is contained in:
click33
2025-05-09 03:53:03 +08:00
parent d6187e8c34
commit cad172819a
35 changed files with 631 additions and 428 deletions

View File

@@ -55,14 +55,13 @@
<artifactId>solon.view.thymeleaf</artifactId>
<version>${solon.version}</version>
</dependency>
<!-- Http请求工具在模式三的单点注销功能下用到如不需要可以注释掉 -->
<dependency>
<groupId>org.noear</groupId>
<artifactId>forest-solon-plugin</artifactId>
<version>${solon.version}</version>
</dependency>
<!-- Sa-Token 插件:整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-forest</artifactId>
<version>${sa-token.version}</version>
</dependency>
</dependencies>

View File

@@ -0,0 +1,23 @@
package com.pj;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.dao.SaTokenDaoForRedisx;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
/**
* @author noear 2023/3/13 created
*/
@Configuration
public class SaConfig {
/**
* 构建建 SaToken redis dao如果不需要 redis可以注释掉
* */
@Bean
public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx saTokenDao) {
return saTokenDao;
}
}

View File

@@ -2,9 +2,14 @@ package com.pj;
import cn.dev33.satoken.sso.SaSsoManager;
import cn.dev33.satoken.sso.model.SaSsoClientInfo;
import cn.dev33.satoken.stp.StpUtil;
import org.noear.solon.Solon;
import org.noear.solon.annotation.SolonMain;
import java.util.ArrayList;
import java.util.List;
@SolonMain
public class SaSsoServerApp {
@@ -12,11 +17,25 @@ public class SaSsoServerApp {
Solon.start(SaSsoServerApp.class, args);
System.out.println();
System.out.println("---------------------- Solon Sa-Token SSO 统一认证中心启动成功 ----------------------");
System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------");
System.out.println("配置信息:" + SaSsoManager.getServerConfig());
System.out.println("统一认证登录地址http://sa-sso-server.com:9000/sso/auth");
System.out.println("测试前需要根据官网文档修改hosts文件测试账号密码sa / 123456");
System.out.println("测试前需要根据官网文档修改 hosts 文件测试账号密码sa / 123456");
System.out.println();
// SaSsoClientInfo sci = new SaSsoClientInfo();
// sci.setClient("client1");
//
// List<SaSsoClientInfo> list = new ArrayList<>();
// list.add(sci);
//
// StpUtil.getSessionByLoginId(10001).set("list", list);
//
// List<SaSsoClientInfo> list2 = (List)StpUtil.getSessionByLoginId(10001).get("list");
// for (SaSsoClientInfo info : list2) {
// System.out.println(info);
// }
}
}

View File

@@ -1,39 +0,0 @@
package com.pj.h5;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;
/**
* 跨域过滤器
* @author click33
*/
@Component(index = -200)
public class CorsFilter implements Filter {
static final String OPTIONS = "OPTIONS";
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
// 允许指定域访问跨域资源
ctx.headerSet("Access-Control-Allow-Origin", "*");
// 允许所有请求方式
ctx.headerSet("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
// 有效时间
ctx.headerSet("Access-Control-Max-Age", "3600");
// 允许的header参数
ctx.headerSet("Access-Control-Allow-Headers", "x-requested-with,satoken");
// 如果是预检请求,直接返回
if (OPTIONS.equals(ctx.method())) {
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
ctx.output("");
return;
}
// System.out.println("*********************************过滤器被使用**************************");
chain.doFilter(ctx);
}
}

View File

@@ -1,15 +1,13 @@
package com.pj.h5;
import cn.dev33.satoken.sso.util.SaSsoConsts;
import cn.dev33.satoken.sso.template.SaSsoUtil;
import cn.dev33.satoken.sso.util.SaSsoConsts;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Render;
/**
* 前后台分离架构下集成SSO所需的代码 SSO-Server端
@@ -18,40 +16,42 @@ import org.noear.solon.core.handle.Render;
*
*/
@Controller
public class H5Controller implements Render {
public class H5Controller {
/**
* 获取 redirectUrl
*/
@Mapping("/sso/getRedirectUrl")
private Object getRedirectUrl(String redirect, String mode, String client) {
// 未登录情况下,返回 code=401
if (StpUtil.isLogin() == false) {
public SaResult getRedirectUrl(String client, String redirect, String mode) {
// 未登录情况下,返回 code=401
if(StpUtil.isLogin() == false) {
return SaResult.code(401);
}
// 已登录情况下,构建 redirectUrl
if (SaSsoConsts.MODE_SIMPLE.equals(mode)) {
// 模式一
SaSsoUtil.checkRedirectUrl(SaFoxUtil.decoderUrl(redirect));
// 已登录情况下,构建 redirectUrl
redirect = SaFoxUtil.decoderUrl(redirect);
if(SaSsoConsts.MODE_SIMPLE.equals(mode)) {
// 模式一
SaSsoUtil.checkRedirectUrl(client, redirect);
return SaResult.data(redirect);
} else {
// 模式二或模式三
String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), client, redirect);
// 模式二或模式三
String redirectUrl = SaSsoUtil.buildRedirectUrl(client, redirect, StpUtil.getLoginId(), StpUtil.getLoginDeviceId());
return SaResult.data(redirectUrl);
}
}
/**
* 控制当前类的异常
*/
@Override
public void render(Object data, Context ctx) throws Throwable {
if (data instanceof Throwable) {
Throwable e = (Throwable) data;
e.printStackTrace();
ctx.render(SaResult.error(e.getMessage()));
} else {
ctx.render(data);
}
}
// /**
// * 控制当前类的异常
// */
// @Override
// public void render(Object data, Context ctx) throws Throwable {
// if (data instanceof Throwable) {
// Throwable e = (Throwable) data;
// e.printStackTrace();
// ctx.render(SaResult.error(e.getMessage()));
// } else {
// ctx.render(data);
// }
// }
}

View File

@@ -0,0 +1,40 @@
package com.pj.h5;
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
/**
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
*
* @author click33
*/
@Configuration
public class SaTokenConfigure {
/**
* CORS 跨域处理策略
*/
@Bean
public SaCorsHandleFunction corsHandle() {
return (req, res, sto) -> {
res.
// 允许指定域访问跨域资源
setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求不做处理"))
.back();
};
}
}

View File

@@ -21,8 +21,8 @@ public class GlobalExceptionFilter implements Filter {
chain.doFilter(ctx);
} catch (Exception e) {
e.printStackTrace();
ctx.render(SaResult.error(e.getMessage()));
}
}
}

View File

@@ -0,0 +1,36 @@
package com.pj.sso;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.stp.StpUtil;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
/**
* SSO 平台中心模式示例,跳连接进入子系统
*/
@Controller
public class HomeController {
// 平台化首页
@Mapping("/home")
public Object index() {
// 如果未登录,则先去登录
if(!StpUtil.isLogin()) {
return SaHolder.getResponse().redirect("/sso/auth");
}
// 拼接各个子系统的地址,格式形如:/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页}
String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/";
String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/";
String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/";
// 组织网页结构返回到前端
String title = "<h2>SSO 平台首页 (平台中心模式)</h2>";
String client1 = "<p><a href='" + link1 + "' target='_blank'> 进入Client1系统 </a></p>";
String client2 = "<p><a href='" + link2 + "' target='_blank'> 进入Client2系统 </a></p>";
String client3 = "<p><a href='" + link3 + "' target='_blank'> 进入Client3系统 </a></p>";
return title + client1 + client2 + client3;
}
}

View File

@@ -1,60 +0,0 @@
package com.pj.sso;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.dao.SaTokenDaoForRedisx;
import cn.dev33.satoken.sso.config.SaSsoServerConfig;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.dtflys.forest.Forest;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.handle.ModelAndView;
/**
* @author noear 2023/1/3 created
*/
@Configuration
public class SsoConfig {
/**
* 构建建 SaToken redis dao如果不需要 redis可以注释掉
* */
@Bean
public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx saTokenDao) {
return saTokenDao;
}
// 配置SSO相关参数
@Bean
public void configSso(SaSsoServerConfig ssoServer) { //SaSsoConfig 已自动构建
// 配置未登录时返回的View
ssoServer.notLoginView = () -> {
return new ModelAndView("sa-login.html");
};
// 配置:登录处理函数
ssoServer.doLoginHandle = (name, pwd) -> {
// 此处仅做模拟登录,真实环境应该查询数据进行登录
if("sa".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("登录失败!");
};
// 配置 Http 请求处理器
ssoServer.sendHttp = url -> {
try {
System.out.println("------ 发起请求:" + url);
String resStr = Forest.get(url).executeAsString();
System.out.println("------ 请求结果:" + resStr);
return resStr;
} catch (Exception e) {
e.printStackTrace();
return null;
}
};
}
}

View File

@@ -1,9 +1,18 @@
package com.pj.sso;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.sso.processor.SaSsoServerProcessor;
import cn.dev33.satoken.sso.template.SaSsoServerTemplate;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.core.handle.ModelAndView;
/**
* Sa-Token-SSO Server端 Controller
@@ -11,17 +20,52 @@ import org.noear.solon.annotation.Mapping;
*
*/
@Controller
@Configuration
public class SsoServerController {
/*
* SSO-Server端处理所有SSO相关请求
* http://{host}:{port}/sso/auth -- 单点登录授权地址接受参数redirect=授权重定向地址
* http://{host}:{port}/sso/doLogin -- 账号密码登录接口接受参数name、pwd
* http://{host}:{port}/sso/checkTicket -- Ticket校验接口isHttp=true时打开接受参数ticket=ticket码、ssoLogoutCall=单点注销回调地址 [可选]
* http://{host}:{port}/sso/signout -- 单点注销地址isSlo=true时打开接受参数loginId=账号id、secretkey=接口调用秘钥
/**
* SSO-Server端处理所有SSO相关请求
* http://{host}:{port}/sso/auth -- 单点登录授权地址
* http://{host}:{port}/sso/doLogin -- 账号密码登录接口接受参数name、pwd
* http://{host}:{port}/sso/signout -- 单点注销地址isSlo=true时打开
*/
@Mapping("/sso/*")
public Object ssoRequest() {
return SaSsoServerProcessor.instance.dister();
}
// 配置SSO相关参数
@Bean
private void configSso(SaSsoServerTemplate ssoServerTemplate) {
// 配置未登录时返回的View
ssoServerTemplate.strategy.notLoginView = () -> {
return new ModelAndView("sa-login.html");
};
// 配置:登录处理函数
ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {
// 此处仅做模拟登录,真实环境应该查询数据库进行登录
if("sa".equals(name) && "123456".equals(pwd)) {
String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32));
StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId));
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("登录失败!");
};
// 添加消息处理器userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口)
ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> {
System.out.println("收到消息:" + message);
// 自定义返回结果(模拟)
return SaResult.ok()
.set("id", message.get("loginId"))
.set("name", "LinXiaoYu")
.set("sex", "")
.set("age", 18);
});
}
}

View File

@@ -3,28 +3,50 @@ server:
port: 9000
# Sa-Token 配置
sa-token:
# ------- SSO-模式一相关配置 (非模式一不需要配置)
# cookie:
# 配置 Cookie 作用域
# domain: stp.com
# ------- SSO-模式二相关配置
sa-token:
# 打印操作日志
is-log: true
# SSO 模式一配置 (非模式一不需要配置)
# cookie:
# # 配置 Cookie 作用域
# domain: stp.com
# SSO-Server 配置
sso-server:
# Ticket有效期 (单位: 秒),默认五分钟
# Ticket有效期 (单位: 秒),默认五分钟
ticket-timeout: 300
# 所有允许的授权回调地址
# 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址
home-route: /home
# 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数)
allow-anon-client: true
# 所有允许的授权回调地址 (匿名 client 使用)
allow-url: "*"
# 是否打开单点注销功能
isSlo: true
# ------- SSO-模式三相关配置 下面的配置在SSO模式三并且 is-slo=true 时打开)
# 是否打开模式三
isHttp: true
# 接口调用秘钥用于SSO模式三的单点注销功能
sign:
# API 接口调用秘钥 (全局默认 + 匿名 client 使用)
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器文档有步骤说明
# 应用列表:配置接入的应用信息
clients:
# 应用 sso-client1采用模式一对接 (同域、同Redis)
sso-client1:
client: sso-client1
allowUrl: "*"
# 应用 sso-client2采用模式二对接 (跨域、同Redis)
sso-client2:
client: sso-client2
allowUrl: "*"
secretKey: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# 应用 sso-client3采用模式三对接 (跨域、跨Redis)
sso-client3:
# 应用名称
client: sso-client3
# 允许授权地址
allowUrl: "*"
# 是否接收消息推送
isPush: true
# 消息推送地址
pushUrl: http://sa-sso-client1.com:9003/sso/pushC
# 接口调用秘钥 (如果不配置则使用全局默认秘钥)
secretKey: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
sa-token.dao: #名字可以随意取
redis:
@@ -32,11 +54,7 @@ sa-token.dao: #名字可以随意取
# password: 123456
db: 1
maxTotal: 200
forest:
# 关闭 forest 请求日志打印
log-enabled: false

View File

@@ -21,11 +21,6 @@
<dependencies>
<!-- Solon 依赖 -->
<!--<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-api</artifactId>
</dependency>-->
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>

View File

@@ -18,12 +18,12 @@ public class SaSso1ClientApp {
System.out.println("\nSa-Token SSO模式一 Client端启动成功");
System.out.println();
System.out.println("---------------------- Solon Sa-Token SSO 模式一 Client 端启动成功 ----------------------");
System.out.println("---------------------- Sa-Token SSO 模式一 Client 端启动成功 ----------------------");
System.out.println("配置信息:" + SaSsoManager.getClientConfig());
System.out.println("测试访问应用端一: http://s1.stp.com:9001");
System.out.println("测试访问应用端二: http://s2.stp.com:9001");
System.out.println("测试访问应用端三: http://s3.stp.com:9001");
System.out.println("测试前需要根据官网文档修改hosts文件测试账号密码sa / 123456");
System.out.println("测试前需要根据官网文档修改 hosts 文件测试账号密码sa / 123456");
System.out.println();
}

View File

@@ -1,7 +1,10 @@
package com.pj.sso;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.sso.SaSsoManager;
import cn.dev33.satoken.sso.config.SaSsoClientConfig;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
@@ -21,12 +24,16 @@ public class SsoClientController implements Render {
@Produces(MimeType.TEXT_HTML_VALUE)
@Mapping("/")
public String index() {
String authUrl = SaSsoManager.getClientConfig().splicingAuthUrl();
String solUrl = SaSsoManager.getClientConfig().splicingSignoutUrl();
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='" + authUrl + "?mode=simple&redirect=' + encodeURIComponent(location.href);\">登录</a> " +
"<a href=\"javascript:location.href='" + solUrl + "?back=' + encodeURIComponent(location.href);\">注销</a> </p>";
String url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), Context.current().queryString()) );
SaSsoClientConfig cfg = SaSsoManager.getClientConfig();
String str = "<h2>Sa-Token SSO-Client 应用端 (模式一)</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
"<p>" +
"<a href='" + cfg.splicingAuthUrl() + "?mode=simple&client=" + cfg.getClient() + "&redirect=" + url + "'>登录</a> - " +
"<a href='" + cfg.splicingSignoutUrl() + "?singleDeviceIdLogout=true&back=" + url + "'>单浏览器注销</a> - " +
"<a href='" + cfg.splicingSignoutUrl() + "?back=" + url + "'>全端注销</a> " +
"</p>";
return str;
}
@@ -36,7 +43,6 @@ public class SsoClientController implements Render {
if(data instanceof Exception){
data = SaResult.error(((Exception)data).getMessage());
}
ctx.render(data);
}

View File

@@ -3,9 +3,14 @@ server:
port: 9001
# Sa-Token 配置
sa-token:
sa-token:
# 打印操作日志
is-log: true
# SSO-相关配置
sso-client:
# client 标识
client: sso-client1
# SSO-Server端 - 主机地址
server-url: http://sso.stp.com:9000

View File

@@ -53,12 +53,14 @@
<version>${sa-token.version}</version>
</dependency>
<!-- Http 请求工具 -->
<!-- Sa-Token 插件:整合 Forest 请求工具 -->
<dependency>
<groupId>org.noear</groupId>
<artifactId>forest-solon-plugin</artifactId>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-forest</artifactId>
<version>${sa-token.version}</version>
</dependency>
</dependencies>

View File

@@ -2,8 +2,6 @@ package com.pj;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.dao.SaTokenDaoForRedisx;
import cn.dev33.satoken.sso.config.SaSsoClientConfig;
import com.dtflys.forest.Forest;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
@@ -22,15 +20,4 @@ public class SaConfig {
return saTokenDao;
}
@Bean
public void configSso(SaSsoClientConfig ssoClient) {
// 配置Http请求处理器
ssoClient.sendHttp = url -> {
System.out.println("------ 发起请求:" + url);
String resStr = Forest.get(url).executeAsString();
System.out.println("------ 请求结果:" + resStr);
return resStr;
};
}
}

View File

@@ -1,6 +1,5 @@
package com.pj;
import cn.dev33.satoken.sso.SaSsoManager;
import org.noear.solon.Solon;
import org.noear.solon.annotation.SolonMain;

View File

@@ -1,38 +0,0 @@
package com.pj.h5;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;
/**
* 跨域过滤器
* @author click33
*/
@Component(index = -200)
public class CorsFilter implements Filter {
static final String OPTIONS = "OPTIONS";
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
// 允许指定域访问跨域资源
ctx.headerSet("Access-Control-Allow-Origin", "*");
// 允许所有请求方式
ctx.headerSet("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
// 有效时间
ctx.headerSet("Access-Control-Max-Age", "3600");
// 允许的header参数
ctx.headerSet("Access-Control-Allow-Headers", "x-requested-with,satoken");
// 如果是预检请求,直接返回
if (OPTIONS.equals(ctx.method())) {
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
ctx.output("");
return;
}
// System.out.println("*********************************过滤器被使用**************************");
chain.doFilter(ctx);
}
}

View File

@@ -1,13 +1,13 @@
package com.pj.h5;
import cn.dev33.satoken.sso.model.SaCheckTicketResult;
import cn.dev33.satoken.sso.processor.SaSsoClientProcessor;
import cn.dev33.satoken.sso.template.SaSsoUtil;
import cn.dev33.satoken.sso.template.SaSsoClientUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Render;
/**
* 前后台分离架构下集成SSO所需的代码 SSO-Client端
@@ -16,40 +16,30 @@ import org.noear.solon.core.handle.Render;
*
*/
@Controller
public class H5Controller implements Render {
public class H5Controller {
// 当前是否登录
// 判断当前是否登录
@Mapping("/sso/isLogin")
public Object isLogin() {
return SaResult.data(StpUtil.isLogin());
return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull());
}
// 返回SSO认证中心登录地址
// 返回SSO认证中心登录地址
@Mapping("/sso/getSsoAuthUrl")
public SaResult getSsoAuthUrl(String clientLoginUrl) {
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, "");
String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, "");
return SaResult.data(serverAuthUrl);
}
// 根据ticket进行登录
// 根据 ticket 进行登录
@Mapping("/sso/doLoginByTicket")
public SaResult doLoginByTicket(String ticket) {
Object loginId = SaSsoClientProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket");
if(loginId != null) {
StpUtil.login(loginId);
return SaResult.data(StpUtil.getTokenValue());
}
return SaResult.error("无效ticket" + ticket);
SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket);
StpUtil.login(ctr.loginId, new SaLoginParameter()
.setTimeout(ctr.remainTokenTimeout)
.setDeviceId(ctr.deviceId)
);
return SaResult.data(StpUtil.getTokenValue());
}
// 全局异常拦截并转换
@Override
public void render(Object data, Context ctx) throws Throwable {
if(data instanceof Exception){
data = SaResult.error(((Exception)data).getMessage());
}
ctx.render(data);
}
}

View File

@@ -0,0 +1,40 @@
package com.pj.h5;
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
/**
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
*
* @author click33
*/
@Configuration
public class SaTokenConfigure {
/**
* CORS 跨域处理策略
*/
@Bean
public SaCorsHandleFunction corsHandle() {
return (req, res, sto) -> {
res.
// 允许指定域访问跨域资源
setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求不做处理"))
.back();
};
}
}

View File

@@ -0,0 +1,28 @@
package com.pj.sso;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;
/**
* 全局异常处理
* @author click33
*
*/
@Component
public class GlobalExceptionFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} catch (Exception e) {
e.printStackTrace();
ctx.render(SaResult.error(e.getMessage()));
}
}
}

View File

@@ -1,52 +1,83 @@
package com.pj.sso;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.sso.message.SaSsoMessage;
import cn.dev33.satoken.sso.processor.SaSsoClientProcessor;
import cn.dev33.satoken.sso.template.SaSsoClientTemplate;
import cn.dev33.satoken.sso.template.SaSsoClientUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.annotation.Produces;
import org.noear.solon.annotation.*;
import org.noear.solon.boot.web.MimeType;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Render;
/**
* Sa-Token-SSO Client端 Controller
* @author click33
*/
@Controller
public class SsoClientController implements Render {
@Configuration
public class SsoClientController {
// 首页
@Produces(MimeType.TEXT_HTML_VALUE)
@Mapping("/")
public String index() {
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a> " +
"<a href='/sso/logout?back=self'>注销</a></p>";
String str = "<h2>Sa-Token SSO-Client 应用端 (模式二)</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
"<p> " +
"<a href='/sso/login?back=/'>登录</a> - " +
"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - " +
"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - " +
"<a href='/sso/logout?back=self'>全端注销</a> - " +
"<a href='/sso/myInfo' target='_blank'>账号资料</a>" +
"</p>";
return str;
}
/*
* SSO-Client端处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client端登录地址接受参数back=登录后的跳转地址
* http://{host}:{port}/sso/logout -- Client端单点注销地址isSlo=true时打开接受参数back=注销后的跳转地址
* http://{host}:{port}/sso/logoutCall -- Client端单点注销回调地址isSlo=true时打开此接口为框架回调开发者无需关心
* SSO-Client端处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client 端登录地址
* http://{host}:{port}/sso/logout -- Client 端注销地址isSlo=true时打开
* http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址
*/
@Mapping("/sso/*")
public Object ssoRequest() {
return SaSsoClientProcessor.instance.dister();
}
// 全局异常拦截并转换
@Override
public void render(Object data, Context ctx) throws Throwable {
if(data instanceof Exception){
data = SaResult.error(((Exception)data).getMessage());
// 配置SSO相关参数
@Bean
private void configSso(SaSsoClientTemplate ssoClientTemplate) {
}
// 当前应用独自注销 (不退出其它应用)
@Mapping("/sso/logoutByAlone")
public Object logoutByAlone() {
StpUtil.logout();
return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());
}
// 查询我的账号信息sso-client 前端 -> sso-center 后端 -> sso-server 后端
@Mapping("/sso/myInfo")
public Object myInfo() {
// 如果尚未登录
if( ! StpUtil.isLogin()) {
return "尚未登录,无法获取";
}
ctx.render(data);
// 获取本地 loginId
Object loginId = StpUtil.getLoginId();
// 推送消息
SaSsoMessage message = new SaSsoMessage();
message.setType("userinfo");
message.set("loginId", loginId);
SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);
// 返回给前端
return result;
}
}

View File

@@ -2,18 +2,21 @@
server:
port: 9002
# sa-token配置
sa-token:
# sa-token配置
sa-token:
# 打印操作日志
is-log: true
# SSO-相关配置
sso-client:
# SSO-Server端 主机地址
# 应用标识
client: sso-client2
# SSO-Server 端主机地址
server-url: http://sa-sso-server.com:9000
# 在 sso-server 端前后端分离时需要单独配置 auth-url 参数上面的不要注释auth-url 配置项和 server-url 要同时存在)
# auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html
# 是否打开单点注销接口
is-slo: true
sign:
# API 接口调用秘钥
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# API 接口调用秘钥 (单点注销时会用到)
secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# 配置 Sa-Token 单独使用的Redis连接 此处需要和SSO-Server端连接同一个Redis
sa-token.dao: #名字可以随意取

View File

@@ -21,23 +21,12 @@
<dependencies>
<!-- Solon 依赖 -->
<!-- <dependency>-->
<!-- <groupId>org.noear</groupId>-->
<!-- <artifactId>solon-api</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
<version>${solon.version}</version>
</dependency>
<!-- Http 请求工具 -->
<dependency>
<groupId>org.noear</groupId>
<artifactId>forest-solon-plugin</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档https://sa-token.cc/ -->
<dependency>
<groupId>cn.dev33</groupId>
@@ -59,6 +48,13 @@
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 插件:整合 Forest 请求工具 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-forest</artifactId>
<version>${sa-token.version}</version>
</dependency>
</dependencies>

View File

@@ -0,0 +1,45 @@
package com.pj.h5;
import cn.dev33.satoken.sso.model.SaCheckTicketResult;
import cn.dev33.satoken.sso.processor.SaSsoClientProcessor;
import cn.dev33.satoken.sso.template.SaSsoClientUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
/**
* 前后台分离架构下集成SSO所需的代码 SSO-Client端
* <p>如果不需要前后端分离架构下集成SSO可删除此包下所有代码</p>
* @author click33
*
*/
@Controller
public class H5Controller {
// 判断当前是否登录
@Mapping("/sso/isLogin")
public Object isLogin() {
return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull());
}
// 返回SSO认证中心登录地址
@Mapping("/sso/getSsoAuthUrl")
public SaResult getSsoAuthUrl(String clientLoginUrl) {
String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, "");
return SaResult.data(serverAuthUrl);
}
// 根据 ticket 进行登录
@Mapping("/sso/doLoginByTicket")
public SaResult doLoginByTicket(String ticket) {
SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket);
StpUtil.login(ctr.loginId, new SaLoginParameter()
.setTimeout(ctr.remainTokenTimeout)
.setDeviceId(ctr.deviceId)
);
return SaResult.data(StpUtil.getTokenValue());
}
}

View File

@@ -0,0 +1,40 @@
package com.pj.h5;
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
/**
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
*
* @author click33
*/
@Configuration
public class SaTokenConfigure {
/**
* CORS 跨域处理策略
*/
@Bean
public SaCorsHandleFunction corsHandle() {
return (req, res, sto) -> {
res.
// 允许指定域访问跨域资源
setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求不做处理"))
.back();
};
}
}

View File

@@ -0,0 +1,28 @@
package com.pj.sso;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;
/**
* 全局异常处理
* @author click33
*
*/
@Component
public class GlobalExceptionFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} catch (Exception e) {
e.printStackTrace();
ctx.render(SaResult.error(e.getMessage()));
}
}
}

View File

@@ -1,69 +1,82 @@
package com.pj.sso;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.sso.message.SaSsoMessage;
import cn.dev33.satoken.sso.processor.SaSsoClientProcessor;
import cn.dev33.satoken.sso.template.SaSsoUtil;
import cn.dev33.satoken.sso.template.SaSsoClientTemplate;
import cn.dev33.satoken.sso.template.SaSsoClientUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.annotation.Produces;
import org.noear.solon.annotation.*;
import org.noear.solon.boot.web.MimeType;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Render;
import java.util.HashMap;
import java.util.Map;
/**
* Sa-Token-SSO Client端 Controller
* @author click33
*/
@Controller
public class SsoClientController implements Render {
@Configuration
public class SsoClientController {
// SSO-Client端首页
@Produces(MimeType.TEXT_HTML_VALUE)
@Mapping("/")
public String index() {
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a>" +
" <a href='/sso/logout?back=self'>注销</a></p>";
String str = "<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
"<p> " +
"<a href='/sso/login?back=/'>登录</a> - " +
"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - " +
"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - " +
"<a href='/sso/logout?back=self'>全端注销</a> - " +
"<a href='/sso/myInfo' target='_blank'>账号资料</a>" +
"</p>";
return str;
}
/*
* SSO-Client端处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client端登录地址接受参数back=登录后的跳转地址
* http://{host}:{port}/sso/logout -- Client端单点注销地址isSlo=true时打开接受参数back=注销后的跳转地址
* http://{host}:{port}/sso/logoutCall -- Client端单点注销回调地址isSlo=true时打开此接口为框架回调开发者无需关心
* SSO-Client端处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client 端登录地址
* http://{host}:{port}/sso/logout -- Client 端注销地址isSlo=true时打开
* http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址
*/
@Mapping("/sso/*")
public Object ssoRequest() {
return SaSsoClientProcessor.instance.dister();
}
// 查询我的账号信息
// 配置SSO相关参数
@Bean
private void configSso(SaSsoClientTemplate ssoClientTemplate) {
}
// 当前应用独自注销 (不退出其它应用)
@Mapping("/sso/logoutByAlone")
public Object logoutByAlone() {
StpUtil.logout();
return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());
}
// 查询我的账号信息sso-client 前端 -> sso-center 后端 -> sso-server 后端
@Mapping("/sso/myInfo")
public Object myInfo() {
// 组织请求参数
Map<String, Object> map = new HashMap<>();
map.put("apiType", "userinfo");
map.put("loginId", StpUtil.getLoginId());
// 发起请求
Object resData = SaSsoUtil.getData(map);
System.out.println("sso-server 返回的信息:" + resData);
return resData;
}
// 全局异常拦截并转换
@Override
public void render(Object data, Context ctx) throws Throwable {
if(data instanceof Exception){
data = SaResult.error(((Exception)data).getMessage());
// 如果尚未登录
if( ! StpUtil.isLogin()) {
return "尚未登录,无法获取";
}
ctx.render(data);
// 获取本地 loginId
Object loginId = StpUtil.getLoginId();
// 推送消息
SaSsoMessage message = new SaSsoMessage();
message.setType("userinfo");
message.set("loginId", loginId);
SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);
// 返回给前端
return result;
}
}

View File

@@ -1,24 +0,0 @@
package com.pj.sso;
import cn.dev33.satoken.sso.config.SaSsoClientConfig;
import com.dtflys.forest.Forest;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
/**
* @author noear 2023/3/13 created
*/
@Configuration
public class SsoConfig {
// 配置SSO相关参数
@Bean
private void configSso(SaSsoClientConfig ssoClient) {
// 配置Http请求处理器
ssoClient.sendHttp = url -> {
System.out.println("------ 发起请求:" + url);
String resStr = Forest.get(url).executeAsString();
System.out.println("------ 请求结果:" + resStr);
return resStr;
};
}
}

View File

@@ -3,31 +3,29 @@ server:
port: 9003
# sa-token配置
sa-token:
# SSO-相关配置
sso-client:
# SSO-Server端 主机地址
server-url: http://sa-sso-server.com:9000
# 使用Http请求校验ticket
is-http: true
# 打开单点注销功能
is-slo: true
sign:
# 接口调用秘钥
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
sa-token:
# 打印操作日志
is-log: true
# sso-client 相关配置
sso-client:
# 应用标识
client: sso-client3
# sso-server 端主机地址
server-url: http://sa-sso-server.com:9000
# 在 sso-server 端前后端分离时需要单独配置 auth-url 参数上面的不要注释auth-url 配置项和 server-url 要同时存在)
# auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html
# 使用 Http 请求校验 ticket (模式三)
is-http: true
# API 接口调用秘钥
secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# 配置 Sa-Token Dao此处与SSO-Server端连接不同的Redis
sa-token.dao: #名字可以随意取
redis:
server: "localhost:6379"
# password: 123456
db: 2
db: 4
maxTotal: 200
forest:
# 关闭 forest 请求日志打印
log-enabled: false

View File

@@ -1,13 +1,10 @@
package com.pj.h5;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
@@ -15,50 +12,29 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
* @author click33
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
public class SaTokenConfigure {
/**
* 注册 [Sa-Token 全局过滤器]
* CORS 跨域处理策略
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
public SaCorsHandleFunction corsHandle() {
return (req, res, sto) -> {
res.
// 允许指定域访问跨域资源
setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 指定 [拦截路由] 与 [放行路由]
.addInclude("/**").addExclude("/favicon.ico")
// 认证函数: 每次请求执行
.setAuth(obj -> {
// SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
// ...
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
return SaResult.error(e.getMessage());
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
SaHolder.getResponse()
// ---------- 设置跨域响应头 ----------
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "*")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
;
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求不做处理"))
.back();
})
;
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求不做处理"))
.back();
};
}
}

View File

@@ -3,7 +3,10 @@ server:
port: 9001
# Sa-Token 配置
sa-token:
sa-token:
# 打印操作日志
is-log: true
# SSO-相关配置
sso-client:
# client 标识

View File

@@ -70,7 +70,7 @@ public class CustomSaSsoServerTemplate extends SaSsoServerTemplate {
```
### 2、在 sso-server 端开启匿名 client 接入
### 2、在 sso-client 端不要配置 client 字段
然后在对应的应用端不要配置 client 字段,例如:

View File

@@ -42,7 +42,7 @@ public class SaSsoClientModel implements Serializable {
/**
* 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket )
*/
public String allowUrl = "*";
public String allowUrl = "";
/**
* 是否接收推送消息