1. @EnableResourceServer 注解
- @EnableResourceServer 配置接口为ResourceServerConfigurer,接口内容如下
public interface ResourceServerConfigurer {
/**
* Add resource-server specific properties (like a resource id). The defaults should work for many applications, but
* you might want to change at least the resource id.
*
* @param resources configurer for the resource server
* @throws Exception if there is a problem
*/
void configure(ResourceServerSecurityConfigurer resources) throws Exception;
/**
* Use this to configure the access rules for secure resources. By default all resources <i>not</i> in "/oauth/**"
* are protected (but no specific rules about scopes are given, for instance). You also get an
* {@link OAuth2WebSecurityExpressionHandler} by default.
*
* @param http the current http filter configuration
* @throws Exception if there is a problem
*/
void configure(HttpSecurity http) throws Exception;
}
其默认实现为OAuth2ResourceServerConfiguration
@Configuration
@Conditional({OAuth2ResourceServerConfiguration.ResourceServerCondition.class})
@ConditionalOnClass({EnableResourceServer.class, SecurityProperties.class})
@ConditionalOnWebApplication
@ConditionalOnBean({ResourceServerConfiguration.class})
@Import({ResourceServerTokenServicesConfiguration.class})
public class OAuth2ResourceServerConfiguration {
private final ResourceServerProperties resource;
public OAuth2ResourceServerConfiguration(ResourceServerProperties resource) {
this.resource = resource;
}
@Bean
@ConditionalOnMissingBean({ResourceServerConfigurer.class})
public ResourceServerConfigurer resourceServer() {
return new OAuth2ResourceServerConfiguration.ResourceSecurityConfigurer(this.resource);
}
@ConditionalOnBean({AuthorizationServerEndpointsConfiguration.class})
private static class AuthorizationServerEndpointsConfigurationBeanCondition {
private AuthorizationServerEndpointsConfigurationBeanCondition() {
}
public static boolean matches(ConditionContext context) {
Class<OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition> type = OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition.class;
Conditional conditional = (Conditional)AnnotationUtils.findAnnotation(type, Conditional.class);
StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(type);
Class[] var4 = conditional.value();
int var5 = var4.length;
for(int var6 = 0; var6 < var5; ++var6) {
Class<? extends Condition> conditionType = var4[var6];
Condition condition = (Condition)BeanUtils.instantiateClass(conditionType);
if (condition.matches(context, metadata)) {
return true;
}
}
return false;
}
}
protected static class ResourceServerCondition extends SpringBootCondition implements ConfigurationCondition {
private static final Bindable<Map<String, Object>> STRING_OBJECT_MAP = Bindable.mapOf(String.class, Object.class);
private static final String AUTHORIZATION_ANNOTATION = "org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration";
protected ResourceServerCondition() {
}
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
Builder message = ConditionMessage.forCondition("OAuth ResourceServer Condition", new Object[0]);
Environment environment = context.getEnvironment();
if (!(environment instanceof ConfigurableEnvironment)) {
return ConditionOutcome.noMatch(message.didNotFind("A ConfigurableEnvironment").atAll());
} else if (this.hasOAuthClientId(environment)) {
return ConditionOutcome.match(message.foundExactly("client-id property"));
} else {
Binder binder = Binder.get(environment);
String prefix = "security.oauth2.resource.";
if (binder.bind(prefix + "jwt", STRING_OBJECT_MAP).isBound()) {
return ConditionOutcome.match(message.foundExactly("JWT resource configuration"));
} else if (binder.bind(prefix + "jwk", STRING_OBJECT_MAP).isBound()) {
return ConditionOutcome.match(message.foundExactly("JWK resource configuration"));
} else if (StringUtils.hasText(environment.getProperty(prefix + "user-info-uri"))) {
return ConditionOutcome.match(message.foundExactly("user-info-uri property"));
} else if (StringUtils.hasText(environment.getProperty(prefix + "token-info-uri"))) {
return ConditionOutcome.match(message.foundExactly("token-info-uri property"));
} else {
return ClassUtils.isPresent("org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration", (ClassLoader)null) && OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition.matches(context) ? ConditionOutcome.match(message.found("class").items(new Object[]{"org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration"})) : ConditionOutcome.noMatch(message.didNotFind("client ID, JWT resource or authorization server").atAll());
}
}
}
private boolean hasOAuthClientId(Environment environment) {
return StringUtils.hasLength(environment.getProperty("security.oauth2.client.client-id"));
}
}
protected static class ResourceSecurityConfigurer extends ResourceServerConfigurerAdapter {
private ResourceServerProperties resource;
public ResourceSecurityConfigurer(ResourceServerProperties resource) {
this.resource = resource;
}
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(this.resource.getResourceId());
}
public void configure(HttpSecurity http) throws Exception {
((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
}
}
}
我们可以提供自己的配置,如下所示
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
@Autowired
protected ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
@Autowired
protected PlatformAccessDeniedHandler platformAccessDeniedHandler;
@Autowired
protected RemoteTokenServices remoteTokenServices;
@Autowired
protected UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/actuator/**"
, "/v2/api-docs").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
userTokenConverter.setUserDetailsService(userDetailsService);
accessTokenConverter.setUserTokenConverter(userTokenConverter);
remoteTokenServices.setRestTemplate(lbRestTemplate());
remoteTokenServices.setAccessTokenConverter(accessTokenConverter);
resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
.accessDeniedHandler(platformAccessDeniedHandler)
.tokenServices(remoteTokenServices);
}
@Bean
@LoadBalanced
public RestTemplate lbRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
//设置自定义异常处理
restTemplate.setErrorHandler(new PlatformResponseErrorHandler());
return restTemplate;
}
}
ResourceServerSecurityConfigurer 在方法configure(HttpSecurity http) 配置了OAuth2AuthenticationProcessingFilter过滤器,代码如下
@Override
public void configure(HttpSecurity http) throws Exception {
AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
if (eventPublisher != null) {
resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
}
if (tokenExtractor != null) {
resourcesServerFilter.setTokenExtractor(tokenExtractor);
}
resourcesServerFilter = postProcess(resourcesServerFilter);
resourcesServerFilter.setStateless(stateless);
// @formatter:off
http
.authorizeRequests().expressionHandler(expressionHandler)
.and()
.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
// @formatter:on
}
2.资源认证的核心 OAuth2AuthenticationProcessingFilter过滤器。
来看一下它的源码
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);
private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
private AuthenticationManager authenticationManager;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private boolean stateless = true;
/**
* Flag to say that this filter guards stateless resources (default true). Set this to true if the only way the
* resource can be accessed is with a token. If false then an incoming cookie can populate the security context and
* allow access to a caller that isn't an OAuth2 client.
*
* @param stateless the flag to set (default true)
*/
public void setStateless(boolean stateless) {
this.stateless = stateless;
}
/**
* @param authenticationEntryPoint the authentication entry point to set
*/
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
/**
* @param authenticationManager the authentication manager to set (mandatory with no default)
*/
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/**
* @param tokenExtractor the tokenExtractor to set
*/
public void setTokenExtractor(TokenExtractor tokenExtractor) {
this.tokenExtractor = tokenExtractor;
}
/**
* @param eventPublisher the event publisher to set
*/
public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
/**
* @param authenticationDetailsSource The AuthenticationDetailsSource to use
*/
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void afterPropertiesSet() {
Assert.state(authenticationManager != null, "AuthenticationManager is required");
}
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 {
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));
}
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);
}
private boolean isAuthenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
return false;
}
return true;
}
public void init(FilterConfig filterConfig) throws ServletException {
}
public void destroy() {
}
private static final class NullEventPublisher implements AuthenticationEventPublisher {
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
public void publishAuthenticationSuccess(Authentication authentication) {
}
}
}
当access_token 不为空时,认证管理器authenticationManager 即 OAuth2AuthenticationManager进行身份认证。身份认证代码如下:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
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;
}
tokenServices(实现类为RemoteTokenServices) 调用loadAuthentication(token) 方法进行身份认证
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
if (map.containsKey("error")) {
if (logger.isDebugEnabled()) {
logger.debug("check_token returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
// gh-838
if (!Boolean.TRUE.equals(map.get("active"))) {
logger.debug("check_token returned active attribute: " + map.get("active"));
throw new InvalidTokenException(accessToken);
}
return tokenConverter.extractAuthentication(map);
}
... 省略其它方法 ...
private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
if (headers.getContentType() == null) {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
}
@SuppressWarnings("rawtypes")
Map map = restTemplate.exchange(path, HttpMethod.POST,
new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
@SuppressWarnings("unchecked")
Map<String, Object> result = map;
return result;
}
跟踪代码,我们可以看到最终是使用 restTemplate 到 checkTokenEndpointUrl 进行认证,checkTokenEndpointUrl 的值是我们在配置文件中配置的 token-info-uri
接下来便进入 CheckTokenEndpoint 完成校验
@FrameworkEndpoint
public class CheckTokenEndpoint {
private ResourceServerTokenServices resourceServerTokenServices;
private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
protected final Log logger = LogFactory.getLog(this.getClass());
private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();
public CheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
this.resourceServerTokenServices = resourceServerTokenServices;
}
public void setExceptionTranslator(WebResponseExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
}
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.accessTokenConverter = accessTokenConverter;
}
@RequestMapping({"/oauth/check_token"})
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
OAuth2AccessToken token = this.resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
} else if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
} else {
OAuth2Authentication authentication = this.resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, Object> response = this.accessTokenConverter.convertAccessToken(token, authentication);
response.put("active", true);
return response;
}
}
@ExceptionHandler({InvalidTokenException.class})
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
public int getHttpErrorCode() {
return 400;
}
};
return this.exceptionTranslator.translate(e400);
}
}
至此,资源服务器主要内容全部完成。
PS:
本人发现 CheckTokenEndpoint 认证失败时会抛出异常,restTemplate 调用 check_token 时,如果使用默认的异常处理类DefaultResponseErrorHandler异常处理逻辑如下图所示,会继续抛出异常,导致调用端出现500错误。
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
switch (statusCode.series()) {
case CLIENT_ERROR:
throw new HttpClientErrorException(statusCode, response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
case SERVER_ERROR:
throw new HttpServerErrorException(statusCode, response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
default:
throw new UnknownHttpStatusCodeException(statusCode.value(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
}
}
可以通过 restTemplate.setErrorHandler(new PlatformResponseErrorHandler()); 设置自定义异常处理类来进行异常处理。
@Slf4j
public class PlatformResponseErrorHandler extends DefaultResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException{
return super.hasError(response);
}
@Override
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
log.error("RestTemplate 异常信息 statusCode={}, response={}",statusCode, response.toString());
// super.handleError(response, statusCode);
}
}
以上仅仅是个人的一些理解及查看的源码,如果有错误或不足,欢迎指正!
网友评论