什么是JWT?
Json web token (JWT), 是為了在網路應用環境間傳遞宣告而執行的一種基于JSON的開放標準,詳情可以參考什么是 JWT -- JSON WEB TOKEN,其特點如下:
- 具體時效性,包含失效時間,
- 具有安全性,基于密鑰機制的簽發和驗證機制,
為什么選擇JWT?
基于oauth2協議認證程序中,以密碼型別認證方式為例,包括認證和授權兩個步驟,分別如下:
- 客戶端通過客戶端用戶名和密碼,密碼授權方式,以及用戶名和密碼,向授權服務器認證,如果有效則回傳token(訪問令牌),
- 客戶端攜帶token(訪問令牌)訪問資源服務器,資源服務器驗證token,如果有效則回傳受保護的資源,
一般在資源服務驗證token時,需要通過token向授權服務器呼叫認證服務,并且,需要通過token向授權服務器獲取用戶資訊,在服務的相互呼叫程序中,會頻繁地呼叫授權服務器,如果使用JWT有如下幾個優勢:
- 由于JWT具有時效性,如果token失效則直接校驗失敗
- 在資源服務可以基于密鑰對token進行校驗,而無需呼叫授權服務器的認證服務,避免頻繁呼叫認證服務器
- 在JWT中攜帶非敏感的認證資訊,同樣避免了頻繁呼叫授權服務器獲取用戶相關資訊,方便了在服務之間傳遞
如何在Spring Cloud Security集成使用?
授權服務器生成JWT
- 授權服務器生成token的流程分析如下:
通過向授權服務TokenEndpoint的/oauth/token POST 請求生成訪問令牌,TokenEndpoint部分原始碼如下:
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
// 通過客戶端id獲取客戶端資訊
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
// 省略授權模式校驗
... ...
// 生成token訪問授權碼
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
根據原始碼可知,security通過TokenGranter生成OAuth2AccessToken,在一般的實作中會使用CompositeTokenGranter組合生成token,分析TokenGranter的公共類AbstractTokenGranter可知,TokenGranter通過呼叫AuthorizationServerTokenServices的createAccessToken方法生成token,AbstractTokenGranter原始碼如下:
// TokenGranter 公共實作類
public abstract class AbstractTokenGranter implements TokenGranter {
// 生成OAuth2AccessToken
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
if (logger.isDebugEnabled()) {
logger.debug("Getting access token for: " + clientId);
}
return getAccessToken(client, tokenRequest);
}
// 通過AuthorizationServerTokenServices 生成token
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
}
在security中默認的AuthorizationServerTokenServices實作類為DefaultTokenServices,在呼叫createAccessToken生成token時,會呼叫TokenEnhancer的enhance對OAuth2AccessToken進行包裝,在實作JWT的TokenEnhancer實作類JwtAccessTokenConverter時,會按照JWT的規范生成OAuth2AccessToken,并且可以自定義TokenEnhancer在OAuth2AccessToken的additionalInformation擴展欄位中追加自定義屬性,
- 基于jwt的授權服務器搭建的相關配置如下:
定義jwt的配置,原始碼如下:
@Configuration
public class JwtTokenConfig {
// 定義密鑰的key
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mm.jks"),
"AJKcNwxDry".toCharArray());
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("tttttt");
return keyPair;
}
// 使用JwtAccessTokenConverter
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
// 使用JwtTokenStore
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
授權服務器配置原始碼如下,AuthorizationServerConfig 在配置生效需要在JwtTokenConfig之后,配置時JWT的相關配置已經生效,其中自定義的認證方式方式實作可以參考玩轉Spring Cloud Security OAuth2身份認證擴展——電話號碼+驗證碼認證,在tokenEnhancer的配置中,使用TokenEnhancerChain,利用TokenEnhancer串列組合多種token增強方式,其中自定義JwtTokenUserEnhancer,在在OAuth2AccessToken的additionalInformation擴展欄位中追加用戶屬性,AuthorizationServerConfig部分原始碼如下:
@Configuration
@EnableAuthorizationServer
@AutoConfigureAfter(JwtTokenConfig.class)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 自定義用戶資訊增強
*/
@Autowired
private TokenEnhancer jwtTokenEnhancer;
/**
* jwt token 轉換器
*/
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenStore jwtTokenStore;
/**
* 配置授權服務
* @param endpoints 授權服務端點配置
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
AuthenticationManager authenticationManager = authenticationProviderManager();
// tokenStore
endpoints.tokenStore(jwtTokenStore)
.userDetailsService(userManager)
.authenticationManager(authenticationManager)
// jwtAccessTokenConverter
.accessTokenConverter(jwtAccessTokenConverter)
// token增強
.tokenEnhancer(tokenEnhancerChain(jwtAccessTokenConverter))
// 自定義認證方式
.tokenGranter(compositeTokenGranter(endpoints, authenticationManager));
}
/**
* token增加鏈表
*/
private TokenEnhancerChain tokenEnhancerChain(JwtAccessTokenConverter jwtAccessTokenConverter) {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = Lists.newArrayList(jwtTokenEnhancer, jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);
return enhancerChain;
}
}
JwtTokenUserEnhancer,用于在OAuth2AccessToken的additionalInformation擴展欄位中追加用戶屬性,部分原始碼如下:
@Component
public class JwtTokenUserEnhancer implements TokenEnhancer {
/**
* 增強 AccessToken
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
// 擴展屬性集合
Map<String, Object> additionalInformation = new LinkedHashMap<>(oAuth2AccessToken.getAdditionalInformation());
Authentication authentication;
// 認證物件
Object principal;
if (Objects.nonNull(authentication = oAuth2Authentication.getUserAuthentication())
&& Objects.nonNull(principal = authentication.getPrincipal())
&& (principal instanceof UserWrapper)) {
UserWrapper userWrapper = (UserWrapper) principal;
// 設定附加資訊
Map<String, Object> info;
if (MapUtils.isNotEmpty(info = this.getAdditionalInformationByUser(userWrapper))) {
additionalInformation.putAll(info);
}
}
}
private Map<String, Object> getAdditionalInformationByUser(UserWrapper userWrapper) {
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
// 用戶權限串列
Collection<? extends GrantedAuthority> grantedAuthorities;
// 省略用戶資訊的轉換邏輯
......
return builder.build();
}
}
在資源服務驗證JWT
-
資源服務器認證token流程如下
資源服務器通過OAuth2AuthenticationProcessingFilter,對資源服務器的url進行攔截授權,大致程序步驟如下:
- 從引數中獲取token
- 對token進行認證
- 認證通過把認證物件保存在安全背景關系SecurityContextHolder中
OAuth2AuthenticationProcessingFilter原始碼如下:
// 實作Filter 對資源服務器的url訪問請求進行攔截
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
try {
// 獲取token
Authentication authentication = tokenExtractor.extract(request);
if (authentication == null) {
if (stateless && isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
// 認證token
Authentication authResult = authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
eventPublisher.publishAuthenticationSuccess(authResult);
// 在安全背景關系設定認證物件
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
catch (OAuth2Exception failed) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + failed);
}
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));
return;
}
chain.doFilter(request, response);
}
}
security通過AuthenticationManager的實作類Auth2AuthenticationManager對token進行認證授權,在認證程序中,通過ResourceServerTokenServices的loadAuthentication驗證token,并且驗證客戶端資訊,Auth2AuthenticationManager部分原始碼如下:
public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
// 獲取OAuth2Authentication認證物件
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
// 驗證客戶端
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
// 認證通過回傳認證物件
auth.setAuthenticated(true);
return auth;
}
}
ResourceServerTokenServices的默認實作類DefaultTokenServices呼叫loadAuthentication獲取認證物件,在loadAuthentication中,通過TokenStore通過readAccessToken獲取token;通過readAuthentication獲取認證資訊,DefaultTokenServices的部分原始碼如下:
public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
ConsumerTokenServices, InitializingBean {
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
InvalidTokenException {
// 獲取token
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
else if (accessToken.isExpired()) {
tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
}
// 根據token獲取認證物件
OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
if (result == null) {
// in case of race condition
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
if (clientDetailsService != null) {
String clientId = result.getOAuth2Request().getClientId();
try {
clientDetailsService.loadClientByClientId(clientId);
}
catch (ClientRegistrationException e) {
throw new InvalidTokenException("Client not valid: " + clientId, e);
}
}
return result;
}
}
呼叫JwtTokenStore的readAuthentication方法獲取認證物件時,會呼叫JwtAccessTokenConverter的extractAuthentication方法獲取認證物件,最終,JwtAccessTokenConverter通過AccessTokenConverter的extractAuthentication獲取認證資訊;認證通過后通過SecurityContextHolder背景關系獲取認證物件,
- 資源服務器的配置如下:
在業務開發中,自定義了JwtAccessTokenConverter的AccessTokenConverter實作類UserAuthenticationConverter,用于從OAuth2AccessToken的additionalInformation擴展欄位中獲取用戶的部分資訊,ResourceServerConfiguration的配置生效在JwtTokenConfig之后,所以,JWT的相關配置已經注入,ResourceServerConfiguration的部分配置如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@AutoConfigureAfter(JwtTokenConfig.class)
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
/**
* 資源屬性配置
*/
@Autowired
private ResourceServerProperties resource;
/**
* jwtTokenStore
*/
@Autowired(required = false)
private TokenStore jwtTokenStore;
/**
* jwt AccessToken轉換器
*/
@Autowired(required = false)
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* 資源服務器自定義配置
*
* @param resources
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
// 設定資源id,
// 客戶端配置表oauth_client_details的 resource_ids 值包含該resourceId,授權才會通過
resources.resourceId(resource.getResourceId());
// 設定自定義tokenStore
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
// 設定自定義的UserAuthenticationConverter
accessTokenConverter.setUserTokenConverter(new UserAuthenticationConverter());
jwtAccessTokenConverter.setAccessTokenConverter(accessTokenConverter);
resources.tokenStore(jwtTokenStore);
}
}
UserAuthenticationConverter用于從OAuth2AccessToken的additionalInformation擴展欄位中獲取用戶的部分資訊,其原始碼如下:
public class UserAuthenticationConverter extends DefaultUserAuthenticationConverter {
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
User user = new User();
// 設定擴展屬性傳遞的值
user.extractAuthentication(map);
// 設定權限值
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
// 設定定義認證物件
return new ExtUsernamePasswordAuthenticationToken(user, user.getUsername(), org.apache.commons.lang3.StringUtils.EMPTY, authorities);
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
Object authorities = map.get(AUTHORITIES);
if (Objects.isNull(authorities)) {
authorities = Collections.EMPTY_LIST;
}
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");
}
}
public class ExtUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
private User userExt;
public ExtUsernamePasswordAuthenticationToken(User user, Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
this.userExt = user;
}
public User getUserExt() {
return userExt;
}
}
資源服務器認證通過后,就可以從安全背景關系SecurityContextHolder獲取認證物件ExtUsernamePasswordAuthenticationToken ,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/148955.html
標籤:其他
