中文网站建设翻译成英文是什么意思在线识别图片来源
文章目录
- 概要
- 功能结构图
- 11.OAuth2.0认证
- 12.分布式会话
- 13.参数签名
概要
Sa-Token核心功能解剖,功能列表:
- OAuth2.0认证 —— 轻松搭建 OAuth2.0 服务,支持openid模式 。
- 分布式会话 —— 提供共享数据中心分布式会话方案。
- 参数签名 —— 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。
功能结构图
11.OAuth2.0认证
轻松搭建 OAuth2.0 服务,支持openid模式 。
4. OAuth2.0 四种模式
基于不同的使用场景,OAuth2.0设计了四种模式:
- 授权码(Authorization Code):OAuth2.0 标准授权步骤,Server 端向 Client 端下放 Code 码,Client 端再用 Code 码换取授权 Access-Token。
- 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server 端使用 URL 重定向方式直接将 Access-Token 下放到 Client 端页面。
- 密码式(Password):Client 端直接拿着用户的账号密码换取授权 Access-Token。
- 客户端凭证(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: 8000sa-token:token-name: satokenis-log: truejwt-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.1port: 6379password: ''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}}
- 统一认证登录(接收:客户端访问登录、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();}............}
}
- 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 定制化配置@Autowiredpublic 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、构建请求 ModelRequestAuthModel 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、构建请求 ModelRequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);// 7、判断授权类型,构建不同的重定向地址// 如果是 授权码式,则:开始重定向授权,下放codeif(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*/@Overridepublic CodeModel generateCode(RequestAuthModel ra) {SaOAuth2Dao dao = SaOAuth2Manager.getDao();// 删除旧Codedao.deleteCode(dao.getCodeValue(ra.clientId, ra.loginId));// 生成新CodeString 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());// 保存新Codedao.saveCode(cm);dao.saveCodeIndex(cm);// 保存code-noncedao.saveCodeNonceIndex(cm);// 返回return cm;}
public interface SaOAuth2Dao {//save 数据:持久化:Code-Modeldefault 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 {@Overridepublic String getHandlerGrantType() {return GrantType.authorization_code;}@Overridepublic 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*/@Overridepublic AccessTokenModel generateAccessToken(String code) {SaOAuth2Dao dao = SaOAuth2Manager.getDao();SaOAuth2DataConverter dataConverter = SaOAuth2Manager.getDataConverter();.............// 3、生成tokenAccessTokenModel at = dataConverter.convertCodeToAccessToken(cm);SaOAuth2Strategy.instance.workAccessTokenByScope.accept(at);RefreshTokenModel rt = dataConverter.convertAccessTokenToRefreshToken(at);at.refreshToken = rt.refreshToken;at.refreshExpiresTime = rt.expiresTime;// 4、保存tokendao.saveAccessToken(at);dao.saveAccessTokenIndex(at);dao.saveRefreshToken(rt);dao.saveRefreshTokenIndex(rt);// 5、删除此Codedao.deleteCode(code);dao.deleteCodeIndex(cm.clientId, cm.loginId);// 6、返回 Access-Tokenreturn 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
登录成功后,开始授权。
- 选择 模式一:授权码(Authorization Code) 方式 认证
- 点我开始授权登录(静默授权)----> 访问 认证中心(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会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点, 这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。 - 解决方案
要怎么解决这个问题呢?目前的主流方案有四种:
- Session同步:只要一个节点的数据发生了改变,就强制同步到其它所有节点
- Session粘滞:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务
- 建立会话中心:将Session存储在缓存中间件,使每个节点都变成无状态服务,例如Redis
- 颁发无状态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 + "×tamp =" + timestamp + "&key=" + secretKey);
// 将 sign 拼接在请求地址后面
String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&nonce=" + nonce + "×tamp =" + 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 + "×tamp =" + timestamp + "&key=" + secretKey);if( ! sign2.equals(sign)) {return SaResult.error("无效 sign,无法响应请求");}// 2、业务代码 // ...// 3、返回return SaResult.ok();
}