当前位置: 首页 > news >正文

Sa-Token核心功能解剖三(OAuth2.0认证、分布式会话、参数签名 )

文章目录

    • 概要
    • 功能结构图
      • 11.OAuth2.0认证
      • 12.分布式会话
      • 13.参数签名

概要

Sa-Token核心功能解剖,功能列表:

  1. OAuth2.0认证 —— 轻松搭建 OAuth2.0 服务,支持openid模式 。
  2. 分布式会话 —— 提供共享数据中心分布式会话方案。
  3. 参数签名 —— 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。

功能结构图

在这里插入图片描述

11.OAuth2.0认证

轻松搭建 OAuth2.0 服务,支持openid模式 。
4. OAuth2.0 四种模式
基于不同的使用场景,OAuth2.0设计了四种模式:

  1. 授权码(Authorization Code):OAuth2.0 标准授权步骤,Server 端向 Client 端下放 Code 码,Client 端再用 Code 码换取授权 Access-Token。
  2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server 端使用 URL 重定向方式直接将 Access-Token 下放到 Client 端页面。
  3. 密码式(Password):Client 端直接拿着用户的账号密码换取授权 Access-Token。
  4. 客户端凭证(Client Credentials):Server 端针对 Client 级别的 Token,代表应用自身的资源授权。
    在这里插入图片描述
    前提:修改hosts文件(C:\windows\system32\drivers\etc\hosts),添加以下IP映射,方便我们进行测试
127.0.0.1 sa-oauth-server.com
127.0.0.1 sa-oauth-client.com

OAuth2-Server端:sa-token-demo-oauth2-server 源码链接
OAuth2-Client端: sa-token-demo-oauth2-client 源码链接

一、Server 认证端(sa-token-demo-oauth2-server端)、认证中心
1.application.yml

server:
  port: 8000

sa-token:
  token-name: satoken
  is-log: true
  jwt-secret-key: saxsaxsaxsax
  # OAuth2.0 配置
  oauth2-server:
    enable-authorization-code: true
    # 是否全局开启 Implicit 模式
    enable-implicit: true
    # 是否全局开启密码模式
    enable-password: true
    # 是否全局开启客户端模式
    enable-client-credentials: true
    # 定义哪些 scope 是高级权限,多个用逗号隔开
    # higher-scope: openid,userid
    # 定义哪些 scope 是低级权限,多个用逗号隔开
    # lower-scope: userinfo
spring:
  #根据部署调整
  redis:
    ip: 127.0.0.1
    port: 6379
    password: ''
    database: 1

2、启动日志

2025-03-31 15:49:01.001  INFO 20716 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8000 (http)

Sa-Token-OAuth2 Server端启动成功,配置如下:
SaOAuth2ServerConfig{enableAuthorizationCode=true, enableImplicit=true, enablePassword=true, enableClientCredentials=true, isNewRefresh=false, codeTimeout=300, accessTokenTimeout=7200, refreshTokenTimeout=2592000, clientTokenTimeout=7200, lowerClientTokenTimeout=-1, openidDigestPrefix='openid_default_digest_prefix, unionidDigestPrefix='unionid_default_digest_prefix, higherScope='null, lowerScope='null, mode4ReturnAccessToken='false, hideStatusField='false, oidc='SaOAuth2OidcConfig{iss='null', idTokenTimeout=600}}
  1. 统一认证登录(接收:客户端访问登录、OAuth模式访问 等请求)
    I) 登录认证请求:
  • http://sa-oauth-server.com:8000/oauth2/doLogin?name=sa&pwd=123456

SaOAuth2ServerController: Sa-Token-OAuth2 Server 认证端 Controller

@RestController
public class SaOAuth2ServerController {
	// OAuth2-Server 端:处理所有 OAuth2 相关请求
	@RequestMapping("/oauth2/*")
	public Object request() {
		System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
		return SaOAuth2ServerProcessor.instance.dister();
	}
	............
	}
}
  1. Sa-Token OAuth2 请求处理器:处理 Server 端请求, 路由分发
public class SaOAuth2ServerProcessor {
	public static SaOAuth2ServerProcessor instance = new SaOAuth2ServerProcessor();
	/**
	 * 处理 Server 端请求, 路由分发
	 * @return 处理结果
	 */
	public Object dister() {
		// 获取变量
		SaRequest req = SaHolder.getRequest();
		// ------------------ 路由分发 ------------------
		// doLogin 登录接口
		if(req.isPath(Api.doLogin)) {
			return doLogin();
		}
	}
-------------->
	/**
	 * doLogin 登录接口
	 * @return 处理结果
	 */
	public Object doLogin() {
		// 获取变量
		SaRequest req = SaHolder.getRequest();
		SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();
		return cfg.doLoginHandle.apply(req.getParam(Param.name), req.getParam(Param.pwd));
	}

SaOAuth2ServerController类 完成登录

@RestController
public class SaOAuth2ServerController {
	// Sa-Token OAuth2 定制化配置
	@Autowired
	public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {
		// 登录处理函数
		oauth2Server.doLoginHandle = (name, pwd) -> {
			if("sa".equals(name) && "123456".equals(pwd)) {
				StpUtil.login(10001);
				return SaResult.ok().set("satoken", StpUtil.getTokenValue());
			}
			return SaResult.error("账号名或密码错误");
		};
}

登录完成,callback到 客户端主页:http://sa-oauth-client.com:8002

其它 过程:
II)授权[http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/]

     public Object dister() {
		// 获取变量
		SaRequest req = SaHolder.getRequest();
		// ------------------ 路由分发 ------------------
		// 模式一:Code授权码 || 模式二:隐藏式
		if(req.isPath(Api.authorize)) {
			return authorize();
		}
------------>
/**
	 * 模式一:Code授权码 / 模式二:隐藏式
	 * @return 处理结果
	 */
	public Object authorize() {
		// 获取变量
		SaRequest req = SaHolder.getRequest();
		SaResponse res = SaHolder.getResponse();
		SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();
		SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();
		SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();
		String responseType = req.getParamNotNull(Param.response_type);

		// 3、构建请求 Model
		RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, SaOAuth2Manager.getStpLogic().getLoginId());
		// 6、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面
		boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);
		if(isNeedCarefulConfirm) {
			return cfg.confirmView.apply(ra.clientId, ra.scopes);
		}
		
	}

第一次该用户尚未授权,则转到授权页面。

III)确认授权:用户在WEB页 同意授权 [/oauth2/doConfirm]

     public Object dister() {
		// 获取变量
		SaRequest req = SaHolder.getRequest(); 
		// doConfirm 确认授权接口
		if(req.isPath(Api.doConfirm)) {
			return doConfirm();
		}
------------>
    /**
	 * doConfirm 确认授权接口
	 * @return 处理结果
	 */
	public Object doConfirm() {
		// 获取变量
		SaRequest req = SaHolder.getRequest();
		String clientId = req.getParamNotNull(Param.client_id);
		Object loginId = SaOAuth2Manager.getStpLogic().getLoginId();
		String scope = req.getParamNotNull(Param.scope);
		List<String> scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(scope);
		SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();
		SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();
		// 确认授权
		oauth2Template.saveGrantScope(clientId, loginId, scopes);
		.......
		// -------- 情况2:需要返回最终的 redirect_uri 地址
		// s3、构建请求 Model
		RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);
		// 7、判断授权类型,构建不同的重定向地址
		// 		如果是 授权码式,则:开始重定向授权,下放code
		if(ResponseType.code.equals(ra.responseType)) {
			CodeModel codeModel = dataGenerate.generateCode(ra);
			String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);
			return SaResult.ok().set(Param.redirect_uri, redirectUri);
		}............
	}
------------>
/**
	 * 持久化:用户授权记录
	 * @param clientId 应用id
	 * @param loginId 账号id
	 * @param scopes 权限列表
	 */
	default void saveGrantScope(String clientId, Object loginId, List<String> scopes) {
		if( ! SaFoxUtil.isEmpty(scopes)) {
			long ttl = checkClientModel(clientId).getAccessTokenTimeout();
			String value = SaOAuth2Manager.getDataConverter().convertScopeListToString(scopes);
			getSaTokenDao().set(splicingGrantScopeKey(clientId, loginId), value, ttl);
		}
	}
------------>
	/**
	 * 拼接key:用户授权记录
	 * @param clientId 应用id
	 * @param loginId 账号id
	 * @return key
	 */
	default String splicingGrantScopeKey(String clientId, Object loginId) {
		return getSaTokenConfig().getTokenName() + ":oauth2:grant-scope:" + clientId + ":" + loginId;
	}
	` Redis 存储 用户授权 [satoken:oauth2:grant-scope:clientId:loginId, scopes集合]`
	
------------>
/**
     * 构建Model:Code授权码
     * @param ra 请求参数Model
     * @return 授权码Model
     */
    @Override
    public CodeModel generateCode(RequestAuthModel ra) {
        SaOAuth2Dao dao = SaOAuth2Manager.getDao();
        // 删除旧Code
        dao.deleteCode(dao.getCodeValue(ra.clientId, ra.loginId));
        // 生成新Code
        String codeValue = SaOAuth2Strategy.instance.createCodeValue.execute(ra.clientId, ra.loginId, ra.scopes);
        CodeModel cm = new CodeModel(codeValue, ra.clientId, ra.scopes, ra.loginId, ra.redirectUri, ra.getNonce());
        // 保存新Code
        dao.saveCode(cm);
        dao.saveCodeIndex(cm);
        // 保存code-nonce
        dao.saveCodeNonceIndex(cm);
        // 返回
        return cm;
    }

public interface SaOAuth2Dao {
	//save 数据:持久化:Code-Model
	default void saveCode(CodeModel c) {
		if(c == null) {
			return;
		}
		getSaTokenDao().setObject(splicingCodeSaveKey(c.code), c, SaOAuth2Manager.getServerConfig().getCodeTimeout());
	}
} 
-----------
	/**
	 * 拼接key:Code持久化
	 * @param code 授权码
	 * @return key
	 */
	default String splicingCodeSaveKey(String code) {
		return getSaTokenConfig().getTokenName() + ":oauth2:code:" + code;
	}
 `存储 code 授权码 [satoken:oauth2:code:code值(60位), CodeModel对象]`

执行完成后,String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);
重定向: http://sa-oauth-client.com:8002/code=60位串

V)Code码获取 Access-Token

	public Object dister() {
	    ...........
		// Code 换 Access-Token || 模式三:密码式
		if(req.isPath(Api.token)) {
			return token();
		}
	---------------
	public Object token() {
		AccessTokenModel accessTokenModel = SaOAuth2Strategy.instance.grantTypeAuth.apply(SaHolder.getRequest());
		return SaOAuth2Manager.getDataResolver().buildAccessTokenReturnValue(accessTokenModel);
	}
	--------------
	/**
	 * 根据 scope 信息对一个 AccessTokenModel 进行加工处理
	 */
	public SaOAuth2GrantTypeAuthFunction grantTypeAuth = (req) -> {
		String grantType = req.getParamNotNull(SaOAuth2Consts.Param.grant_type);
		SaOAuth2GrantTypeHandlerInterface grantTypeHandler = grantTypeHandlerMap.get(grantType);
		...................
		// 看看全局是否开启了此 grantType
		........
		// 调用 处理器
		return grantTypeHandler.getAccessToken(req, clientIdAndSecretModel.getClientId(), scopes);
	};

authorization_code grant_type 处理器

public class AuthorizationCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface {

    @Override
    public String getHandlerGrantType() {
        return GrantType.authorization_code;
    }

    @Override
    public AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes) {
       .............
        // 构建 Access-Token、返回
        AccessTokenModel accessTokenModel = SaOAuth2Manager.getDataGenerate().generateAccessToken(code);
        return accessTokenModel;
    }

}

Sa-Token OAuth2 数据构建器,默认实现类SaOAuth2DataGenerateDefaultImpl :

public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate {
      ............
    /**
     * 构建Model:Access-Token
     * @param code 授权码
     * @return AccessToken Model
     */
    @Override
    public AccessTokenModel generateAccessToken(String code) {
        SaOAuth2Dao dao = SaOAuth2Manager.getDao();
        SaOAuth2DataConverter dataConverter = SaOAuth2Manager.getDataConverter();
        .............
        // 3、生成token
        AccessTokenModel at = dataConverter.convertCodeToAccessToken(cm);
        SaOAuth2Strategy.instance.workAccessTokenByScope.accept(at);
        RefreshTokenModel rt = dataConverter.convertAccessTokenToRefreshToken(at);
        at.refreshToken = rt.refreshToken;
        at.refreshExpiresTime = rt.expiresTime;

        // 4、保存token
        dao.saveAccessToken(at);
        dao.saveAccessTokenIndex(at);
        dao.saveRefreshToken(rt);
        dao.saveRefreshTokenIndex(rt);

        // 5、删除此Code
        dao.deleteCode(code);
        dao.deleteCodeIndex(cm.clientId, cm.loginId);

        // 6、返回 Access-Token
        return at;
    }

二、客户端(sa-token-demo-oauth2-client端)
1、application.yml

server:
  port: 8002
sa-token.token-name: satoken-client

2、启动日志

2025-03-31 15:59:54.366  INFO 21036 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8002 (http) with context 
Sa-Token-OAuth Client端启动成功
-------------------- Sa-Token-OAuth2 示例 --------------------
首先在host文件 (C:\windows\system32\drivers\etc\hosts) 添加以下内容: 
	127.0.0.1 sa-oauth-server.com 
	127.0.0.1 sa-oauth-client.com 
再从浏览器访问:
	http://sa-oauth-client.com:8002

3.浏览器访问:http://sa-oauth-client.com:8002
未登录 —> 先登录(被 服务端拦截,进入 服务端【4. 统一认证登录】
http://sa-oauth-server.com:8000/oauth2/doLogin?name=sa&pwd=123456
登录成功后,开始授权。

  1. 选择 模式一:授权码(Authorization Code) 方式 认证
    1. 点我开始授权登录(静默授权)----> 访问 认证中心(Server 认证端):
http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code
&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/
  • response_type:code 即模式一:授权码
  • redirect_uri: 认证成功后的callback 主页(一般为子系统home主页)
    2) 认证中心【 II)确认授权】:用户 同意授权后 ,可进行Token和用户信息等获取
    3)code码获取Token和用户信息
http://sa-oauth-server.com:8000/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code}

进入 认证中心【V)Code码获取 Access-Token】

12.分布式会话

提供共享数据中心分布式会话方案。

  • 需求场景
    微服务架构下的第一个难题便是数据同步,单机版的Session在分布式环境下一般不能正常工作,为此我们需要对框架做一些特定的处理。
    首先我们要明白,分布式环境下为什么Session会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点, 这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。
  • 解决方案
    要怎么解决这个问题呢?目前的主流方案有四种:
  1. Session同步:只要一个节点的数据发生了改变,就强制同步到其它所有节点
  2. Session粘滞:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务
  3. 建立会话中心:将Session存储在缓存中间件,使每个节点都变成无状态服务,例如Redis
  4. 颁发无状态token:放弃Session机制,将用户数据写入令牌,使会话数据做到令牌自解释,例如jwt
  • 方案选择
    方案一:性能消耗太大,不太考虑
    方案二:需要从网关处动手,与框架无关
    方案三:Sa-Token 整合Redis非常简单,详见章节:集成 Redis
    方案四:详见官方仓库中 Sa-Token 整合jwt的示例

13.参数签名

提供跨系统API调用签名校验模块,防参数篡改,防请求重放。

涉及跨系统接口调用时,我们容易碰到以下安全问题:
请求身份被伪造。
请求参数被篡改。
请求被抓包,然后重放攻击。

解决思路:
1.使用摘要算法和密钥(secretKey:由双方制定32位字符串)生成参数签名
2.签名算法 追加 nonce 随机字符串,存储在缓存-redis保存15分钟 (防止被抓包,多次重放攻击)
3.签名算法 追加timestamp 时间戳,将请求的有效性限定在一个有限时间范围内,例如 15分钟。

客户端请求签名,传递参数以及签名sign值过程:

// 声明变量---根据业务接口 自定义
long userId = 10001;
long money = 1000;
String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串
long timestamp = System.currentTimeMillis(); // 系统当前时间戳 
String secretKey = "xxxxxxxxxxxxxxxxxxxx";
// 计算 sign 参数
String sign = md5("money=" + money + "&userId=" + userId + "&nonce=" + nonce + "&timestamp =" + timestamp + "&key=" + secretKey);
// 将 sign 拼接在请求地址后面
String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&nonce=" + nonce + "&timestamp =" + timestamp + "&money=" + money + "&sign=" + sign);

服务验证签名:解析参数,匹配签名sign值

@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money, String nonce, long timestamp, String sign) {
    // 1、检查timestamp是否超出允许的范围[此处需要取绝对值,防止系统A与系统B服务器时钟不一致]
    long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp);
    if(timestampDisparity > 1000 * 60 * 15) {
        return SaResult.error("timestamp 时间差超出允许的范围,请求无效");
    }
   
     // 2、检查此 nonce 是否已被使用过,防止被抓包,多次重放攻击
    if(CacheUtil.get("nonce_" + nonce) != null) {
        return SaResult.error("此 nonce 已被使用过了,请求无效");
    }
  
    //在 B 系统,使用同样的算法、同样的密钥,计算出 sign2,与传入的 sign 进行比对
    String sign2 = md5("money=" + money + "&userId=" + userId + "&nonce=" + nonce + "&timestamp =" + timestamp + "&key=" + secretKey);
    if( ! sign2.equals(sign)) {
        return SaResult.error("无效 sign,无法响应请求");
    }
    // 2、业务代码 
    // ...
    // 3、返回
    return SaResult.ok();
}

相关文章:

  • Transformers without Normalization paper笔记
  • Android OpenGLES 360全景图片渲染(球体内部)
  • wsl2的centos7安装jdk17、maven
  • 欧拉公式和sin cos
  • 3.31Python有关文件操作
  • 【java】Java核心知识点与相应面试技巧(九)——异常
  • PHP回调后门
  • Ubuntu22.04系统离线部署Maxkb【教程】
  • 再见VS Code!Google IDE 正颠覆传统开发体验
  • 探秘中医五色五味:开启饮食养生新智慧
  • Element ui input组件类型为 textarea 时没有 清空按钮
  • [网络_1] 因特网 | 三种交换 | 拥塞 | 差错 | 流量控制
  • Nordic 新一代无线 SoC nRF54L系列介绍
  • Tiny Lexer 一个极简的C语言词法分析器
  • 回溯(子集型):分割回文串
  • 如何在 Windows 上安装与配置 Tomcat
  • 基于PX4和Ardupilot固件下自定义MAVLink消息测试(QGroundControl和Mission Planner)
  • 76. pinctrl和gpio子系统试验
  • 【Easylive】HikariCP 介绍
  • 14:00开始面试,14:08就出来了,问的问题有点变态。。。
  • 用ssh做的网站/郑州网站推广公司咨询
  • 做网站的公司有哪些岗位/广告推广方式
  • 哪里找做网站客户/百度公司的发展历程
  • 东莞定制网站建设/什么是seo如何进行seo
  • 一家公司做两个网站/企业营销型网站有哪些
  • 冀州网站优化/注册城乡规划师教材