擴充套件jwt解決oauth2 效能瓶頸

冷冷gg發表於2019-03-18

oauth2 效能瓶頸

資源伺服器的請求都會被攔截 到認證伺服器校驗合法性 (如下圖)

  • 使用者攜帶token 請求資源伺服器
  • 資源伺服器攔截器 攜帶token 去認證伺服器 呼叫tokenstore 對token 合法性校驗
  • 資源伺服器拿到token,預設只會含有使用者名稱資訊
  • 通過使用者名稱呼叫userdetailsservice.loadbyusername 查詢使用者全部資訊

如上步驟在實際使用,會造成認證中心的負載壓力過大,成為造成整個系統瓶頸的關鍵點。

擴充套件jwt解決oauth2 效能瓶頸

check-token 過程中涉及的原始碼

擴充套件jwt 生成攜帶使用者詳細資訊

  • 為什麼使用jwt 替代預設的UUID token ?
    通過jwt 訪問資源伺服器後,不再使用check-token 過程,通過對jwt 的解析即可實現身份驗證,登入資訊的傳遞。減少網路開銷,提高整體微服務叢集的效能
  • spring security oauth 預設的jwttoken 只含有username,通過擴充套件TokenEnhancer,實現關鍵欄位的注入到 JWT 中,方便資源伺服器使用
	@Bean
	public TokenEnhancer tokenEnhancer() {
		return (accessToken, authentication) -> {
			if (SecurityConstants.CLIENT_CREDENTIALS
					.equals(authentication.getOAuth2Request().getGrantType())) {
				return accessToken;
			}

			final Map<String, Object> additionalInfo = new HashMap<>(8);
			PigxUser pigxUser = (PigxUser) authentication.getUserAuthentication().getPrincipal();
			additionalInfo.put("user_id", pigxUser.getId());
			additionalInfo.put("username", pigxUser.getUsername());
			additionalInfo.put("dept_id", pigxUser.getDeptId());
			additionalInfo.put("tenant_id", pigxUser.getTenantId());
			additionalInfo.put("license", SecurityConstants.PIGX_LICENSE);
			((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
			return accessToken;
		};
	}
複製程式碼
  • 生成的token 如下,含有關鍵的欄位
    擴充套件jwt解決oauth2 效能瓶頸

重寫預設的資源伺服器處理行為

  • 不再使用RemoteTokenServices 去掉用認證中心 CheckToken,自定義客戶端TokenService
@Slf4j
public class PigxCustomTokenServices implements ResourceServerTokenServices {
	@Setter
	private TokenStore tokenStore;

	@Setter
	private DefaultAccessTokenConverter defaultAccessTokenConverter;

	@Setter
	private JwtAccessTokenConverter jwtAccessTokenConverter;
	
	@Override
	public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
		OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
		UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter();
		defaultAccessTokenConverter.setUserTokenConverter(userTokenConverter);
		Map<String, ?> map = jwtAccessTokenConverter.convertAccessToken(readAccessToken(accessToken), oAuth2Authentication);
		return defaultAccessTokenConverter.extractAuthentication(map);
	}


	@Override
	public OAuth2AccessToken readAccessToken(String accessToken) {
		return tokenStore.readAccessToken(accessToken);
	}
}
複製程式碼
  • 解析jwt 組裝成Authentication
/**
 * @author lengleng
 * @date 2019-03-17
 * <p>
 * jwt 轉化使用者資訊
 */
public class PigxUserAuthenticationConverter implements UserAuthenticationConverter {
	private static final String USER_ID = "user_id";
	private static final String DEPT_ID = "dept_id";
	private static final String TENANT_ID = "tenant_id";
	private static final String N_A = "N/A";
	
	@Override
	public Authentication extractAuthentication(Map<String, ?> map) {
		if (map.containsKey(USERNAME)) {
			Collection<? extends GrantedAuthority> authorities = getAuthorities(map);

			String username = (String) map.get(USERNAME);
			Integer id = (Integer) map.get(USER_ID);
			Integer deptId = (Integer) map.get(DEPT_ID);
			Integer tenantId = (Integer) map.get(TENANT_ID);
			PigxUser user = new PigxUser(id, deptId, tenantId, username, N_A, true
					, true, true, true, authorities);
			return new UsernamePasswordAuthenticationToken(user, N_A, authorities);
		}
		return null;
	}

	private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
		Object authorities = map.get(AUTHORITIES);
		if (authorities instanceof String) {
			return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
		}
		if (authorities instanceof Collection) {
			return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
					.collectionToCommaDelimitedString((Collection<?>) authorities));
		}
		throw new IllegalArgumentException("Authorities must be either a String or a Collection");
	}
}

複製程式碼
  • 資源伺服器配置中注入以上配置即可
@Slf4j
public class PigxResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
	@Override
	public void configure(ResourceServerSecurityConfigurer resources) {
		DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
		UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter();
		accessTokenConverter.setUserTokenConverter(userTokenConverter);

		PigxCustomTokenServices tokenServices = new PigxCustomTokenServices();
		
		// 這裡的簽名key 保持和認證中心一致
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setSigningKey("123");
		converter.setVerifier(new MacSigner("123"));
		JwtTokenStore jwtTokenStore = new JwtTokenStore(converter);
		tokenServices.setTokenStore(jwtTokenStore);
		tokenServices.setJwtAccessTokenConverter(converter);
		tokenServices.setDefaultAccessTokenConverter(accessTokenConverter);

		resources
				.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
				.tokenServices(tokenServices);
	}
}
複製程式碼

使用JWT 擴充套件後帶來的問題

  • JWT 的最大缺點是,由於伺服器不儲存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的許可權。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非伺服器部署額外的邏輯。

  • 去認證伺服器校驗的過程就是 通過tokenstore 來控制jwt 安全性的一個方法,去掉Check-token 意味著 jwt token 安全性不可保證

  • JWT 本身包含了認證資訊,一旦洩露,任何人都可以獲得該令牌的所有許可權。為了減少盜用,JWT 的有效期應該設定得比較短。對於一些比較重要的許可權,使用時應該再次對使用者進行認證。

  • 為了減少盜用,JWT 不應該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。

關注我

相關文章