1. 程式人生 > >專案使用InMemoryTokenStore時,token有效期設定與強制清除某客戶端持有的token

專案使用InMemoryTokenStore時,token有效期設定與強制清除某客戶端持有的token

1. 設定token有效期

    在使用InMemoryTokenStore(token儲存在記憶體)token生成策略時,系統預設的token的有效時間是12小時。

從oauth原始碼的預設token生成方法中,可以看出

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
		ConsumerTokenServices, InitializingBean {

	private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.

	private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
.............................
        private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
             DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
             int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
             if (validitySeconds > 0) {
                  token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
             }
             token.setRefreshToken(refreshToken);
             token.setScope(authentication.getOAuth2Request().getScope());

             return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
        }
      
       /**
        * The access token validity period in seconds
        * @param clientAuth the current authorization request
        * @return the access token validity period in seconds
        */
       protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) {
         if (clientDetailsService != null) {
             ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
             Integer validity = client.getAccessTokenValiditySeconds();
             if (validity != null) {
                 return validity;
             }
         }
         return accessTokenValiditySeconds;
      }
 ......................

}
從上面官方原始碼我們可以瞭解到2個事情

1. 在token物件裡面包含了token的過期時間

2. ClientDetails 中的AccessTokenValiditySeconds欄位可以指定token的有效期

目前我已經有一個自定義的客戶端驗證服務類。那麼可以在驗證客戶端物件時,直接呼叫AccessTokenValiditySeconds的set方法,設定token有效期,這個時間的單位是秒

我的程式碼:

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;

import bap.core.config.util.spring.SpringContextHolder;
import bap.core.dao.BaseDao;
import bap.pp.core.config.item.domain.ConfigItem;
import bap.pp.strongbox.client.domain.ClientDetail;

/**
 * 客戶端身份驗證
 * @author Amanda.Z
 *
 */
public class ClientDetailConfig implements ClientDetailsService{

	private BaseDao baseDao;
	
	public ClientDetailConfig() {
		baseDao=SpringContextHolder.getBean(BaseDao.class);
	}
	
	/* 
	 * 根據客戶端clientid檢查客戶端有效性,如果有效,則封裝為oauth2客戶端物件
	 * @see org.springframework.security.oauth2.provider.ClientDetailsService#loadClientByClientId(java.lang.String)
	 */
	@Override
	public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
		BaseClientDetails details=new BaseClientDetails();
		ClientDetail client=(ClientDetail) this.baseDao.getUniqueResultByHql("from ClientDetail where clientName=? and deleted=0",clientId);
		if(client!=null){
			//設定客戶端id
			details.setClientId(client.getClientName());
			//客戶端私鑰
			details.setClientSecret(client.getClientSecret());
			//受保護資源id
			List<String> resourceList=new ArrayList<>();
			resourceList.add(client.getResource().getResourceKey());
			details.setResourceIds(resourceList);
			//oauth2保護模式,本專案預設全部是客戶端認證模式
			List<String> authorizedGrantTypes=new ArrayList<>();
			authorizedGrantTypes.add("client_credentials");
			details.setAuthorizedGrantTypes(authorizedGrantTypes);
			//客戶端傳入的引數
			List<String> scopList=new ArrayList<>();
			scopList.add("view");
			scopList.add(client.getScope());
			details.setScope(scopList);
			//設定此客戶端持有的使用者組
			Collection<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
			if(bap.util.StringUtil.isNotEmpty(client.getAuthoritites())){
				String [] authArray=client.getAuthoritites().split(",");
				for (String auth : authArray) {
					auths.add(new SimpleGrantedAuthority(auth));
				}
			}
			details.setAuthorities(auths);
			//設定token有效時間,單位是秒,(如果不設定,框架內部預設是12小時,本平臺設定預設2小時)
			details.setAccessTokenValiditySeconds(Integer.parseInt(ConfigItem.TokenValiditySeconds.getVal()));
		}
		return details;
	}

}

注意:這裡通過ClientDail設定進去的token有效時間,只要有效時間沒有過,不管這個時候,客戶端是否被刪除,都不會影響這個token的訪問。

      這個時候 ,產生了一個明顯的問題,認證端管理員建立了某個客戶端賬戶,並分配了許可權,這個客戶端獲取了一個token,如果這個時候管理員希望把這個客戶端刪除,理論上應該連帶這個客戶端產生的token也刪除。

      接下來,介紹如何解決上面的問題,在自己的服務類中,刪除某個客戶端賬戶產生的token資料

      首先還是先看原始碼,我使用的是InMemoryTokenStore,其中已經具備了刪除token的方法:

public class InMemoryTokenStore implements TokenStore {

	private static final int DEFAULT_FLUSH_INTERVAL = 1000;

	private final ConcurrentHashMap<String, OAuth2AccessToken> accessTokenStore = new ConcurrentHashMap<String, OAuth2AccessToken>();

	private final ConcurrentHashMap<String, OAuth2AccessToken> authenticationToAccessTokenStore = new ConcurrentHashMap<String, OAuth2AccessToken>();

	private final ConcurrentHashMap<String, Collection<OAuth2AccessToken>> userNameToAccessTokenStore = new ConcurrentHashMap<String, Collection<OAuth2AccessToken>>();

	private final ConcurrentHashMap<String, Collection<OAuth2AccessToken>> clientIdToAccessTokenStore = new ConcurrentHashMap<String, Collection<OAuth2AccessToken>>();

	private final ConcurrentHashMap<String, OAuth2RefreshToken> refreshTokenStore = new ConcurrentHashMap<String, OAuth2RefreshToken>();

	private final ConcurrentHashMap<String, String> accessTokenToRefreshTokenStore = new ConcurrentHashMap<String, String>();

	private final ConcurrentHashMap<String, OAuth2Authentication> authenticationStore = new ConcurrentHashMap<String, OAuth2Authentication>();

	private final ConcurrentHashMap<String, OAuth2Authentication> refreshTokenAuthenticationStore = new ConcurrentHashMap<String, OAuth2Authentication>();

	private final ConcurrentHashMap<String, String> refreshTokenToAccessTokenStore = new ConcurrentHashMap<String, String>();

	private final DelayQueue<TokenExpiry> expiryQueue = new DelayQueue<TokenExpiry>();

	private final ConcurrentHashMap<String, TokenExpiry> expiryMap = new ConcurrentHashMap<String, TokenExpiry>();

	private int flushInterval = DEFAULT_FLUSH_INTERVAL;

	private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();

	private AtomicInteger flushCounter = new AtomicInteger(0);
        .............................
        //從記憶體中清除token記錄
        public void removeAccessToken(OAuth2AccessToken accessToken) {
		removeAccessToken(accessToken.getValue());
	}

        //根據clientid獲取其所有token
	public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
               Collection<OAuth2AccessToken> result = clientIdToAccessTokenStore.get(clientId);
                return result != null ? Collections.<OAuth2AccessToken> unmodifiableCollection(result) : Collections
                .<OAuth2AccessToken> emptySet();
        }
        //根據token值,在記憶體中移除這條token的記錄
	public void removeAccessToken(String tokenValue) {
		OAuth2AccessToken removed = this.accessTokenStore.remove(tokenValue);
		this.accessTokenToRefreshTokenStore.remove(tokenValue);
		// Don't remove the refresh token - it's up to the caller to do that
		OAuth2Authentication authentication = this.authenticationStore.remove(tokenValue);
		if (authentication != null) {
			this.authenticationToAccessTokenStore.remove(authenticationKeyGenerator.extractKey(authentication));
			Collection<OAuth2AccessToken> tokens;
			String clientId = authentication.getOAuth2Request().getClientId();
			tokens = this.userNameToAccessTokenStore.get(getApprovalKey(clientId, authentication.getName()));
			if (tokens != null) {
				tokens.remove(removed);
			}
			tokens = this.clientIdToAccessTokenStore.get(clientId);
			if (tokens != null) {
				tokens.remove(removed);
			}
			this.authenticationToAccessTokenStore.remove(authenticationKeyGenerator.extractKey(authentication));
		}
	}
        ...............
}

      我們看到這些remove方法,入參要求傳入token的,顯然我並不知道這個客戶端的token的值,我只有這個客戶端的物件本身,那麼,再看另外的方法findTokensByClientId通過這個方法,可以由clientid獲取到這個客戶端所有token值,也就是說,接下來,只需要呼叫這個方法,遍歷結果集,逐個呼叫remove方法移除即可。

     事實上,真正坑爹的是,你得不到InMemoryTokensStrore這個類的例項物件,從Spring容器中,竟然獲取不了,我試著用@autowride獲取它,直接異常。這是,求助官網文件,但是連個P都沒說。所以繼續扣原始碼!!

     持續向上查詢所有呼叫InMemoryTokenStore這個類中方法,並且能夠提供對tokenStore的get方法的類,還要求能夠從spring中獲取得到這個類物件,終於!終於!在public final class AuthorizationServerEndpointsConfigurer 這個類裡面我找到了tokenStore的get方法。

public final class AuthorizationServerEndpointsConfigurer {

	private AuthorizationServerTokenServices tokenServices;

	private ConsumerTokenServices consumerTokenServices;

	private AuthorizationCodeServices authorizationCodeServices;

	private ResourceServerTokenServices resourceTokenServices;

	private TokenStore tokenStore;

	private TokenEnhancer tokenEnhancer;

	private AccessTokenConverter accessTokenConverter;

	private ApprovalStore approvalStore;

	private TokenGranter tokenGranter;

	private OAuth2RequestFactory requestFactory;

	private OAuth2RequestValidator requestValidator;

	private UserApprovalHandler userApprovalHandler;

	private AuthenticationManager authenticationManager;

	private ClientDetailsService clientDetailsService;

	private String prefix;

	private Map<String, String> patternMap = new HashMap<String, String>();

	private Set<HttpMethod> allowedTokenEndpointRequestMethods = new HashSet<HttpMethod>();

	private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping;

	private boolean approvalStoreDisabled;

	private List<Object> interceptors = new ArrayList<Object>();

	private DefaultTokenServices defaultTokenServices;

	private UserDetailsService userDetailsService;

	private boolean tokenServicesOverride = false;

	private boolean userDetailsServiceOverride = false;

	private boolean reuseRefreshToken = true;

	private WebResponseExceptionTranslator exceptionTranslator;

        ......................
        //記住這裡先
        public boolean isUserDetailsServiceOverride() {
		return userDetailsServiceOverride;
	}
        .............
        public AuthorizationServerEndpointsConfigurer userDetailsService(UserDetailsService userDetailsService) {
		if (userDetailsService != null) {
			this.userDetailsService = userDetailsService;
			this.userDetailsServiceOverride = true;
		}
		return this;
	}
        .................

       //tokenStore是InMemoryTokenStore的父類,這裡是關鍵,我們親愛的get方法
        public TokenStore getTokenStore() {
		return tokenStore();
	}


那麼如何獲取AuthorizationServerEndpointsConfigurer,我嘗試用@autowride注入我的服務類,然並卵。那麼繼續挖,在oauth2驗證端的原始配置類中,找到了如下語句:

@Configuration
@Order(0)
@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class })
public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Autowired
	private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();

	@Autowired
	private ClientDetailsService clientDetailsService;

	@Autowired
	private AuthorizationServerEndpointsConfiguration endpoints;

	@Autowired
	public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception {
		for (AuthorizationServerConfigurer configurer : configurers) {
			configurer.configure(clientDetails);
		}
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
		FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
		http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
		configure(configurer);
		http.apply(configurer);
		String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
		String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
		String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
		if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
			UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
                        //注意看這行程式碼
                        endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
		}

      endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);看到這一行,你會明白,用這個endpoints.getEndpointsConfigurer(),你可以獲取到AuthorizationServerEndpointsConfigurrer物件。那麼這個endpoints這貨,是怎麼拿到的,是@autowride來的,他在spring容器!妥了這回!

      我的客戶端全部都是自定義的,有自己的服務類,所以,現在我可以在我自己的服務類中的刪除客戶端方法中這樣寫:

@Autowired
private AuthorizationServerEndpointsConfiguration endpoints;

@Transactional
	public boolean delete(String[] ck_ids) {
		//執行邏輯刪除操作
		for (String id : ck_ids) {
			ClientDetail detail=this.baseDao.get(ClientDetail.class, id);
			//移除這個客戶端建立的所有token
			InMemoryTokenStore tokenStore=(InMemoryTokenStore) endpoints.getEndpointsConfigurer().getTokenStore();
			Collection<OAuth2AccessToken> tokens=tokenStore.findTokensByClientId(detail.getClientName());
			if(tokens!=null&&tokens.size()>0){
				for(OAuth2AccessToken accessToken:tokens){
					tokenStore.removeAccessToken(accessToken);
				}
			}
			//對客戶端進行邏輯刪除
			detail.setDeleted(1);	
			this.baseDao.update(detail);
		}
		return true;
	}


打完收工