当前位置: 首页 > 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的登陆原理,希望对大家有所帮助。

 

相关文章:

  • C#实现MCP Client 与 LLM 连接,抓取网页内容功能!
  • Windows系统部署Redis
  • 【医学影像 AI】TorchIO医学影像数据增强:从入门到精通的全面指南
  • Java 接口
  • 06-Web后端基础(java操作数据库)
  • Linux基础IO---缓冲区----文件系统----软硬链接
  • Python打卡训练营学习记录Day36
  • Spring AI 与 Python:AI 开发的新老势力对决与协作
  • git提交通用规范
  • 阿里云国际版香港轻量云服务器:CN2 GIA加持,征服海外网络的“速度与激情”!
  • 掌握聚合函数:COUNT,MAX,MIN,SUM,AVG,GROUP BY和HAVING子句的用法,Where和HAVING的区别
  • 成功解决ImportError: cannot import name ‘DTensor‘ from ‘torch.distributed.tensor‘
  • 题目 3327: 蓝桥杯2025年第十六届省赛真题-倒水
  • loss的范围
  • 读《Go语言圣经》记录(一)
  • uni-app学习笔记十三-vue3中slot插槽的使用
  • QML与C++交互2
  • 【电子通识】连接器的绝缘胶座和接触端子基础知识
  • 数据结构 栈的详细解析
  • Java——优先级队列(堆)
  • 山东网站制作/产品市场推广方案范文
  • 重庆官方网站建设/百度网盘在线观看资源
  • 网站开发与设计教程/网络市场的四大特点
  • 制作营销型网站公司/青岛seo外包公司
  • php网站怎么做静态化/宁德市人民医院
  • 公司首页模板/网站如何seo推广