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 postAccessToken(Principal principal, @RequestParam
Map 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 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 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 info;
if (MapUtils.isNotEmpty(info = this.getAdditionalInformationByUser(userWrapper))) {
additionalInformation.putAll(info);
}
}
}
private Map getAdditionalInformationByUser(UserWrapper userWrapper) {
ImmutableMap.Builder 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 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 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 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 。