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对象就被清空了呢?

原因如下:

java 复制代码
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()方法:

java 复制代码
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)

java 复制代码
@Transactional
    public 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方法:

java 复制代码
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都是一样的:

java 复制代码
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)

在进行查询:

java 复制代码
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:

java 复制代码
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:

java 复制代码
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的登陆原理,希望对大家有所帮助。

相关推荐
fanged3 小时前
构建系统maven
java·maven
沙滩小岛小木屋3 小时前
maven编译时跳过test过程
java·maven
江沉晚呤时4 小时前
SQL Server 事务详解:概念、特性、隔离级别与实践
java·数据库·oracle·c#·.netcore
还是鼠鼠5 小时前
单元测试-概述&入门
java·开发语言·后端·单元测试·log4j·maven
MyikJ7 小时前
Java求职面试:从Spring到微服务的技术挑战
java·数据库·spring boot·spring cloud·微服务·orm·面试技巧
MyikJ7 小时前
Java 面试实录:从Spring到微服务的技术探讨
java·spring boot·微服务·kafka·spring security·grafana·prometheus
ShiinaMashirol7 小时前
代码随想录打卡|Day50 图论(拓扑排序精讲 、dijkstra(朴素版)精讲 )
java·图论
cui_hao_nan8 小时前
Nacos实战——动态 IP 黑名单过滤
java
惜.己8 小时前
MySql(十一)
java·javascript·数据库
10000hours8 小时前
【存储基础】NUMA架构
java·开发语言·架构