【Spring Security】 Oauth2 开放平台 资源服务器
【Spring Security】 Oauth2 开放平台 资源服务器
Metadata
title: 【Spring Security】 Oauth2 开放平台 资源服务器
date: 2023-02-02 22:18
tags:
- 行动阶段/完成
- 主题场景/组件
- 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
- 细化主题/Module/SpringSecurity
categories:
- SpringSecurity
keywords:
- SpringSecurity
description: 【Spring Security】 Oauth2 开放平台 资源服务器
需求
搭建了授权服务器并能获取到令牌后,接下来就是如何使用令牌访问资源了。
资源服务器搭建
- 创建资源服务器模块,之前我们已经创建了并引入了相关依赖。
- 创建一个资源模拟接口,直接访问,会发现直接跳转到了登录页
@RequestMapping
@RestController
public class ResourceController {
@GetMapping("/resource")
@PreAuthorize("hasRole('USER')")
public String resource(){
return "访问到了resource 资源 ";
}
}
- 授权服务器放行 /oauth/check_token 端点,此端点默认是受保护的。现在我们的 Oauth2 认证信息是在内存中,当携带令牌访问 Oauth 时,需要去授权服务器校验令牌及获取当前认证信息,所以需要放开此端点。
// 将 check_token 暴露出去,否则资源服务器访问时报错
web.ignoring().antMatchers("/oauth/check_token");
- 配置 ResourceServerConfigurerAdapter,这里基本没做配置,只是开启了 @EnableResourceServer 。
@Configuration
@EnableResourceServer //标识为资源服务
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
public MyResourceServerConfig() {
super();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
// 密码解析器
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- yml 添加授权服务器相关信息
security:
oauth2:
client:
client-id: client
client-secret: secret
access-token-uri: http://localhost:20000/oauth/token
user-authorization-uri: http://localhost:20000/oauth/authorize
resource:
token-info-uri: http://localhost:20000/oauth/check_token
- 配置完成,启动项目。
测试
- 在授权服务中,使用密码模式获取令牌。
- 直接访问,会返回一个未认证错误,而不是像 security 那样跳转到登录页。
- 携带令牌访问,令牌需要添加在请求头,key 为 Authorization,值为 Bearer xxtoken 格式,再次访问,发现获取到了资源。
也可以接口携带令牌直接访问:
http://localhost:9001/resource?access_token=4c0544e6-c558-4e2b-8f56-28f7c741bcb1
SecurityContextHolder 也能获取到认证信息,此时的 Authentication 为 OAuth2Authentication。
OAuth2Authentication [Principal=user, Credentials=[PROTECTED], Authenticated=true, Details=remoteAddress=0:0:0:0:0:0:0:1, tokenType=BearertokenValue=<TOKEN>, Granted Authorities=[ROLE_USER]]
源码分析
资源服务器只需要一个 token,就完成了认证流程,具体是怎么实现的呢,接下来我们追踪下源码。
1. 进入 OAuth2AuthenticationProcessingFilter
OAuth2AuthenticationProcessingFilter 是 OAuth2 受保护资源的身份验证过滤器。从传入的请求中提取 OAuth2 令牌,并使用它来填充带有 {@link OAuth2Authentication} 的 Spring Security context 。
doFilter 方法源码:
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 {
// 1. 从标头中提取OAuth令牌,
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 {
// 2. request设置属性 OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE:4c0544e6-c558-4e2b-8f56-28f7c741bcb1
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
// 3. 转为AbstractAuthenticationToken并设置Details
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
// 4. 认证管理器进行认证
Authentication authResult = authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
// 5. 发布认证成功
eventPublisher.publishAuthenticationSuccess(authResult);
// 6. 设置SecurityContext
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
// 7. 异常处理
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);
}
2. 构造预处理的 Authentication
过滤器的第一步会调用 BearerTokenExtractor 的 extract 方法,从标头中提取 OAuth 令牌。
首先调用 BearerTokenExtractor.extractHeaderToken 方法获取令牌。
protected String extractHeaderToken(HttpServletRequest request) {
// 1.获取Authorization消息头内容 Bearer 4c0544e6-c558-4e2b-8f56-28f7c741bcb1
Enumeration<String> headers = request.getHeaders("Authorization");
// 2. 循环消息头内容
while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
String value = headers.nextElement();
// 3. 如果已Bearer 开头
if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
// 4. 截取令牌 =》 4c0544e6-c558-4e2b-8f56-28f7c741bcb1
String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
// Add this here for the auth details later. Would be better to change the signature of this method.
// 5. request对象添加=》 OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE:Bearer
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
int commaIndex = authHeaderValue.indexOf(',');
if (commaIndex > 0) {
// 6.如果存在逗号,则重新截取
authHeaderValue = authHeaderValue.substring(0, commaIndex);
}
// 7. 返回token =》4c0544e6-c558-4e2b-8f56-28f7c741bcb1
return authHeaderValue;
}
}
return null;
}
然后进入 BearerTokenExtractor.extractToken 方法:
protected String extractToken(HttpServletRequest request) {
// first check the header...
// 1. 检查消息头中的令牌
String token = extractHeaderToken(request);
// 2. 消息头没有检查请求参数中是否有access_token
// bearer type allows a request parameter as well
if (token == null) {
logger.debug("Token not found in headers. Trying request parameters.");
token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
if (token == null) {
logger.debug("Token not found in request parameters. Not an OAuth2 request.");
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
}
}
// 3.返回token
return token;
}
最后调用 BearerTokenExtractor.extract 返回 PreAuthenticatedAuthenticationToken 对象。这是一个预认证对象,他的 principal 属性包含了当前令牌
public Authentication extract(HttpServletRequest request) {
String tokenValue = extractToken(request);
if (tokenValue != null) {
// 创建PreAuthenticatedAuthenticationToken
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
return authentication;
}
return null;
}
3. 调用认证管理器进行认证
这里的认证管理器为 OAuth2AuthenticationManager 的 authenticate 方法。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 1. Authentication预认证对象为null,抛出Invalid token (token not found)异常。
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
// 2. 获取token 4c0544e6-c558-4e2b-8f56-28f7c741bcb1
String token = (String) authentication.getPrincipal();
// 3. 调用RemoteTokenServices,远程获取认证信息并封装为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 + ")");
}
// 4. 检查ClientDetails
checkClientDetails(auth);
// 5. 填充认证信息并返回Authention
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;
}
4. 调用 ResourceServerTokenServices
调用 ResourceServerTokenServices 进行根据令牌获取认证信息,这里的实际对象 RemoteTokenServices,会远程调用授权服务器的 / check_token 端点以获取访问令牌的内容。如果端点返回 400 响应,则表示令牌无效。
首先进入 RemoteTokenServices.loadAuthentication 方法:
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
// 1. 创建请求,添加accessToken、配置文件中的clientId、clientSecret
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
// 2. Post请求配置的http://localhost:20000/oauth/check_token
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
// 3. 返回error则抛出InvalidTokenException异常
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);
}
// 4. 提取认证
return tokenConverter.extractAuthentication(map);
}
5. 提取认证
访问令牌获取到了认证信息后,会进行提取认证,返回 OAuth2Authentication 对象。
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
Map<String, String> parameters = new HashMap<String, String>();
Set<String> scope = extractScope(map);
Authentication user = userTokenConverter.extractAuthentication(map);
String clientId = (String) map.get(clientIdAttribute);
parameters.put(clientIdAttribute, clientId);
if (includeGrantType && map.containsKey(GRANT_TYPE)) {
parameters.put(GRANT_TYPE, (String) map.get(GRANT_TYPE));
}
Set<String> resourceIds = new LinkedHashSet<String>(map.containsKey(AUD) ? getAudience(map)
: Collections.<String>emptySet());
Collection<? extends GrantedAuthority> authorities = null;
if (user==null && map.containsKey(AUTHORITIES)) {
@SuppressWarnings("unchecked")
String[] roles = ((Collection<String>)map.get(AUTHORITIES)).toArray(new String[0]);
authorities = AuthorityUtils.createAuthorityList(roles);
}
OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null,
null);
return new OAuth2Authentication(request, user);
}
6. 检查 ClientDetails
获取到认证信息后,重新回到 OAuth2AuthenticationManager,之后进入 checkClientDetails 方法。
private void checkClientDetails(OAuth2Authentication auth) {
// 1. Oauth客户端如果为null ,不检查,配置了ClientDetailsService的实现类,会进行检查
if (clientDetailsService != null) {
ClientDetails client;
try {
// 2. 获取客户端
client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
}
catch (ClientRegistrationException e) {
throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
}
// 3. 获取配置的客户端授权范围
Set<String> allowed = client.getScope();
for (String scope : auth.getOAuth2Request().getScope()) {
// 4. 如果没有当前授权,则抛出异常OAuth2AccessDeniedException
if (!allowed.contains(scope)) {
throw new OAuth2AccessDeniedException(
"Invalid token contains disallowed scope (" + scope + ") for this client");
}
}
}
}
7. 返回认证结果处理
认证管理器认证成功后,返回认证结果,发布认证成功,并设置 SecurityContext。
8. 异常处理 OAuth2Exception
当认证发生异常,会抛出 OAuth2Exception,在过滤器中会进行拦击处理,最终调用 OAuth2AuthenticationEntryPoint 直接返回错误信息。
catch (OAuth2Exception failed) {
// 清理SecurityContext
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + failed);
}
// 发布认证失败
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
// 调用OAuth2AuthenticationEntryPoint返回ResponseEntity
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));
return;
}