Json web token (JWT), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準,詳情可以參考什麼是 JWT -- JSON WEB TOKEN。其特點如下:
基於oauth2協定認證過程中,以密碼型別認證方式為例,包括認證和授權兩個步驟。分別如下:
一般在資源服務驗證token時,需要通過token向授權伺服器呼叫認證服務,並且,需要通過token向授權伺服器獲取使用者資訊。在服務的相互呼叫過程中,會頻繁地呼叫授權伺服器,如果使用JWT有如下幾個優勢:
通過向授權服務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的設定,原始碼如下:
@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();
}
}
資源伺服器認證token流程如下
資源伺服器通過OAuth2AuthenticationProcessingFilter,對資源伺服器的url進行攔截授權。大致過程步驟如下:
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 。