?問題簡述
最近處理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的登陸原理,希望對大家有所幫助。
?