專案使用InMemoryTokenStore時,token有效期設定與強制清除某客戶端持有的token
1. 設定token有效期
在使用InMemoryTokenStore(token儲存在記憶體)token生成策略時,系統預設的token的有效時間是12小時。
從oauth原始碼的預設token生成方法中,可以看出
從上面官方原始碼我們可以瞭解到2個事情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; } ...................... }
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都沒說。所以繼續扣原始碼!!
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;
}
打完收工