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

SAP Commerce(Hybris)开发实战(二):登陆生成token问题

 问题简述

最近处理Hybris框架标准的登陆功能,遇到一个问题:用两个不同的浏览器,同时登陆一个账号,会同时生成两个不同的token和refreshToken。

问题原因

 解决了其实非常简单,就是Hybris的Employee表中,有一个禁用登陆的字段logindisabled,被设置为禁止登陆了,正常情况应该是设置为false。

Hybris登陆原理

 在这里可以详细的说一下问题以及造成的原因,Hybris的标准登陆功能是,同一个人的账号,登陆以后会设置access_token,refresh_token,expired,分别为登陆token,刷新token和超时时间,这个三个字段都存在同一张表里,只要在登陆时间内,无论怎么登陆,都应该返回的是同一个token对象,也就是上面三个字段的值应该保持一致。

但是这次遇到的问题就是,用两个不同浏览器,登陆同一个账号,会产生不同的token。

这里再补充一下,问题背景,这里的登陆,是在完成了saml2.0的单点登陆的第二步:后端通过url跳转,携带code直接从前端进入的Hybris后台标准登陆方法。

之前一直在单纯的排查登陆问题,实际登陆没有任何问题,因为用了标准的源码,问题出在单点登陆的第一步,也就是通过saml登陆请求到后端以后,在这里改变了Employee表的logindisabled字段状态,从而导致存token对象的表中的指定数据被清空,从而导致第二步的标准登陆方法执行没有获取到用户的token,而是直接生成了一个新的token。

那问题的重点就在,改变了Employee表的一个字段状态,存储在表中token对象就被清空了呢?

原因如下:

public class UserAuthenticationTokensRemovePrepareInterceptor implements PrepareInterceptor<UserModel> {private final TimeService timeService;public UserAuthenticationTokensRemovePrepareInterceptor(TimeService timeService) {this.timeService = timeService;}public void onPrepare(UserModel userModel, InterceptorContext ctx) throws InterceptorException {if (userModel.isLoginDisabled() || this.isUserDeactivated(userModel)) {Collection<OAuthAccessTokenModel> tokensToRemove = userModel.getTokens();if (tokensToRemove != null) {tokensToRemove.forEach((token) -> ctx.registerElementFor(token, PersistenceOperation.DELETE));}userModel.setTokens(Collections.emptyList());}}
......

UserMoldel是Employee的父类 

isLoginDisabled()方法就是判断字段loginDisabled的值。如果为true,就把user里面的token对象设置为空。

也就是后来登陆查询不到用户token对象的原因。

获取token对象

那Hybris是怎么获取当前已经登陆过的用户的token对象的呢?

主要是通过DefaultHybrisOpenIDTokenServices的createAccessToken()方法:

public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) {try {OAuth2AccessToken accessToken = super.createAccessToken(authentication);Set<String> scopes = OAuth2Utils.parseParameterList((String)authentication.getOAuth2Request().getRequestParameters().get("scope"));if (scopes.contains("openid")) {OAuthClientDetailsModel clientDetailsModel = this.getClientDetailsDao().findClientById(authentication.getOAuth2Request().getClientId());if (!(clientDetailsModel instanceof OpenIDClientDetailsModel)) {Logger var10000 = LOG;String var10001 = clientDetailsModel.getClientId();var10000.warn("OAuth2 error, wrong configuration - Client with ID " + var10001 + " is not instance of " + OpenIDClientDetailsModel.class.getName());throw new InvalidRequestException("Server error. Can't generate id_token.");} else {OpenIDClientDetailsModel openIDClientDetailsModel = (OpenIDClientDetailsModel)clientDetailsModel;List<String> externalScopes = null;if (openIDClientDetailsModel.getExternalScopeClaimName() != null) {externalScopes = this.externalScopesStrategy.getExternalScopes(clientDetailsModel, (String)authentication.getUserAuthentication().getPrincipal());LOG.debug("externalScopes: " + externalScopes);}IDTokenParameterData idtokenparam = this.initializeIdTokenParameters(openIDClientDetailsModel.getClientId());DefaultOAuth2AccessToken accessTokenIdToken = new DefaultOAuth2AccessToken(accessToken);String requestedScopes = (String)authentication.getOAuth2Request().getRequestParameters().get("scope");if (!StringUtils.isEmpty(requestedScopes) && requestedScopes.contains("openid")) {IdTokenHelper idTokenHelper = this.createIdTokenHelper(authentication, openIDClientDetailsModel, externalScopes, idtokenparam);Jwt jwt = idTokenHelper.encodeAndSign(this.getSigner(idtokenparam));Map<String, Object> map = new HashMap();map.put("id_token", jwt.getEncoded());accessTokenIdToken.setAdditionalInformation(map);return accessTokenIdToken;} else {LOG.warn("Missing openid scope");throw new InvalidRequestException("Missing openid scope");}}} else {return accessToken;}} catch (ModelSavingException e) {LOG.debug("HybrisOAuthTokenServices->createAccessToken : ModelSavingException", e);return super.createAccessToken(authentication);} catch (ModelRemovalException e) {LOG.debug("HybrisOAuthTokenServices->createAccessToken : ModelRemovalException", e);return super.createAccessToken(authentication);}}

可以看到其主要就是通过调用父类的 createAccessToken(authentication)来实现的。

通过调用链:

HybrisOAuthTokenServices.createAccessToken(authentication)————>>>

DefaultTokenServices.createAccessToken(authentication)

@Transactionalpublic OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);OAuth2RefreshToken refreshToken = null;if (existingAccessToken != null) {if (!existingAccessToken.isExpired()) {this.tokenStore.storeAccessToken(existingAccessToken, authentication);return existingAccessToken;}if (existingAccessToken.getRefreshToken() != null) {refreshToken = existingAccessToken.getRefreshToken();this.tokenStore.removeRefreshToken(refreshToken);}this.tokenStore.removeAccessToken(existingAccessToken);}if (refreshToken == null) {refreshToken = this.createRefreshToken(authentication);} else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {refreshToken = this.createRefreshToken(authentication);}}OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);this.tokenStore.storeAccessToken(accessToken, authentication);refreshToken = accessToken.getRefreshToken();if (refreshToken != null) {this.tokenStore.storeRefreshToken(refreshToken, authentication);}return accessToken;}

可以看到重点在这一句

OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);

 通过authentication来获取token,也就是HybrisOAuthTokenStore的getAccessToken方法:

public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {OAuth2AccessToken accessToken = null;OAuthAccessTokenModel accessTokenModel = null;String authenticationId = this.authenticationKeyGenerator.extractKey(authentication);try {accessTokenModel = this.oauthTokenService.getAccessTokenForAuthentication(authenticationId);accessToken = this.deserializeAccessToken((byte[])accessTokenModel.getToken());} catch (ClassCastException | IllegalArgumentException var7) {LOG.warn("Could not extract access token for authentication " + authentication);this.oauthTokenService.removeAccessTokenForAuthentication(authenticationId);} catch (UnknownIdentifierException var8) {if (LOG.isInfoEnabled()) {LOG.debug("Failed to find access token for authentication " + authentication);}}try {if (accessToken != null && accessTokenModel != null && !StringUtils.equals(authenticationId, this.authenticationKeyGenerator.extractKey(this.deserializeAuthentication((byte[])accessTokenModel.getAuthentication())))) {this.replaceToken(authentication, accessToken);}} catch (ClassCastException | IllegalArgumentException var6) {this.replaceToken(authentication, accessToken);}return accessToken;}

这一段的重点是通过

String authenticationId = this.authenticationKeyGenerator.extractKey(authentication); 

获取authenticationId,再去对应的表里,根据authenticationId查询出对应用户的token信息:

accessTokenModel = this.oauthTokenService.getAccessTokenForAuthentication(authenticationId);

这里还有一点需要重点了解的,就是 authenticationId是如何生成的,怎么保证每个用户的authenticationId都是一样的:

public class DefaultAuthenticationKeyGenerator implements AuthenticationKeyGenerator {private static final String CLIENT_ID = "client_id";private static final String SCOPE = "scope";private static final String USERNAME = "username";public String extractKey(OAuth2Authentication authentication) {Map<String, String> values = new LinkedHashMap();OAuth2Request authorizationRequest = authentication.getOAuth2Request();if (!authentication.isClientOnly()) {values.put("username", authentication.getName());}values.put("client_id", authorizationRequest.getClientId());if (authorizationRequest.getScope() != null) {values.put("scope", OAuth2Utils.formatParameterList(new TreeSet(authorizationRequest.getScope())));}return this.generateKey(values);}protected String generateKey(Map<String, String> values) {try {MessageDigest digest = MessageDigest.getInstance("MD5");byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));return String.format("%032x", new BigInteger(1, bytes));} catch (NoSuchAlgorithmException nsae) {throw new IllegalStateException("MD5 algorithm not available.  Fatal (should be in the JDK).", nsae);} catch (UnsupportedEncodingException uee) {throw new IllegalStateException("UTF-8 encoding not available.  Fatal (should be in the JDK).", uee);}}
}

可以看到,就是通过提取authentication对象的一系列属性,做MD5的Hash算法算出来的,由于用户都是一个,authentication提取的属性能保持都是一致的,所以可以为每个用户生成一个唯一的authenticationId。

然后在

this.oauthTokenService.getAccessTokenForAuthentication(authenticationId)

在进行查询:

public OAuthAccessTokenModel getAccessTokenForAuthentication(final String authenticationId) {ServicesUtil.validateParameterNotNull(authenticationId, "Parameter 'authenticationId' must not be null!");return (OAuthAccessTokenModel)this.getSessionService().executeInLocalView(new SessionExecutionBody() {public Object execute() {DefaultOAuthTokenService.this.searchRestrictionService.disableSearchRestrictions();try {return DefaultOAuthTokenService.this.oauthTokenDao.findAccessTokenByAuthenticationId(authenticationId);} catch (ModelNotFoundException e) {throw new UnknownIdentifierException(e);}}});}

这里可以直接看到查询sql: 

public OAuthAccessTokenModel findAccessTokenByAuthenticationId(String authenticationId) {Map<String, Object> params = new HashMap();params.put("id", authenticationId);return (OAuthAccessTokenModel)this.searchUnique(new FlexibleSearchQuery("SELECT {pk} FROM {OAuthAccessToken} WHERE {authenticationId} = ?id ", params));}

 也就是从表OAuthAccessToken进行查询。

这也就是Hybris标准的登陆功能的实现原理。

按理来说只要是同一个用户登陆,在过期时间之类,都能查询到对应的token。

而笔者遇到的问题,没能查询到,就是在登陆之前修改了表Employee的字段loginDisabled的状态

导致OAuthAccessToken表对应的用户数据被清空,从而需要刷新token。

那么刷新token 的逻辑是怎样呢?

前面也有:

在DefaultTokenServices的createAccessToken方法中,当查询不到token时,会重新生成refresh_token和access_token:

if (refreshToken == null) {refreshToken = this.createRefreshToken(authentication);} else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {refreshToken = this.createRefreshToken(authentication);}}OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);this.tokenStore.storeAccessToken(accessToken, authentication);refreshToken = accessToken.getRefreshToken();if (refreshToken != null) {this.tokenStore.storeRefreshToken(refreshToken, authentication);}return accessToken;

然后返回token对象。

结论

在解决问题的同时,通过源码详细的讲解了Hybris的登陆原理,希望对大家有所帮助。

 

http://www.xdnf.cn/news/643609.html

相关文章:

  • 企业级智能体 —— 企业 AI 发展的下一个风口?
  • 【公式】批量添加MathType公式编号
  • [Linux]磁盘分区及swap交换空间
  • 第38节:PyTorch模型训练流程详解
  • Baklib知识中台构建实战
  • [DS]使用 Python 库中自带的数据集来实现上述 50 个数据分析和数据可视化程序的示例代码
  • 【LangChain全栈开发指南】从LLM应用到企业级AI助手构建
  • LLM多平台统一调用系统-LiteLLM概述
  • MYSQL备份恢复知识:第五章:备份原理
  • 渗透测试流程-下篇
  • 定时任务调度平台XXL-JOB
  • 基于Python实现JSON点云数据的3D可视化与过滤
  • 美团2025年校招笔试真题手撕教程(三)
  • Spring 源码阅读(循环依赖、Bean 生命周期、AOP、IOC) - 5.2.15.RELEASE
  • 电路笔记(通信):RS-485总线 物理层规范 接口及其组成部分 瑞萨电子RS-485总线笔记
  • vue3中computed计算属性和watch监听的异同点
  • Qt实战教程:设计并实现一个结构清晰、功能完整的桌面应用
  • 机械师安装ubantu双系统:一、制作系统盘
  • React从基础入门到高级实战:React 核心技术 - 组件通信与 Props 深入
  • Pandas数据规整
  • 香橙派3B学习笔记3:宝塔 Linux 面板的安装
  • 2025年- H46-Lc154 --543. 二叉树的直径(深度、递归、深搜)--Java版
  • 华为OD机试真题—— 矩阵匹配(2025B卷:200分)Java/python/JavaScript/C/C++/GO最佳实现
  • MySQL的查询进阶
  • 学习STC51单片机15(芯片为STC89C52RCRC)
  • (九)PMSM驱动控制学习---高阶滑膜观测器
  • 网络 :序列和反序列化
  • <uniapp><threejs>在uniapp中,怎么使用threejs来显示3D图形?
  • 电子电路:电压就是电场么?二者有什么关系?
  • python打卡day36