首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Spring Security OAuth2是如何校验token的

Spring Security OAuth2是如何校验token的

作者头像
烟雨平生
发布2023-03-07 13:56:37
发布2023-03-07 13:56:37
5.4K0
举报
文章被收录于专栏:数字化之路数字化之路

你是谁,从哪来?能到哪去?

  • Spring Security概览
  • OAuth2概览
  • 校验token

Spring Security概览

安全框架有两个重要的概念,即认证(Authentication)和授权(Authorization)。认证即确认主体可以访问当前系统的过程; 授权即确定主体通过认证后,检查在当前系统下所拥有的功能权限的过程。 这里的主体既可以是登录系统的用户,也可以是接入的设备或其它系统。

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security是一个功能强大且高度可定制的身份认证和访问控制框架,是保护基于spring的应用程序的事实上的标准。 Spring Security会根据请求的URI的路径来确定该请求的过滤器链(Filter)以及最终的具体Servlet控制器(Controller)。

从上图可以看到,Spring Security以一个单Filte的(FilterChainProxy)存在于整个过滤链路中。这个FilterChainProxy代理着众多的Spring Security Filter。

OAuth2概览

OAuth2是一个基于令牌的安全验证和授权框架。 它将安全性分解为以下4个部分:

  1. 受保护的资源
  2. 资源拥有者
  3. 应用程序
  4. 受保护的资源OAuth2验证服务器 OAuth2服务器对用户进行验证并确认提供给它的令牌。 即承担校验token的职责

校验token

下面的代码涉及到的spring-security-oauth2的版本:

代码语言:javascript
复制
    <dependency>
      <groupId>org.springframework.security.oauth</groupId>
      <artifactId>spring-security-oauth2</artifactId>
      <version>2.3.4.RELEASE</version>
    </dependency>
  1. 两个关键类
代码语言:javascript
复制
// 校验token
org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter
//从请求中提取token
org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor#extractHeaderToken
一定要记得:Spring Security是基于Web容器的Filter来实现身份认证的。来看下解析token的整体流程:
  1. 校验流程

2.1 从请求中获取token 支持以下三种携带token的方式: 2.1.1 在Header中携带【优先级最高,如果找到,则查找结束】

代码语言:javascript
复制
http://localhost:8080/accounts/me
Request Header:
Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135
代码语言:javascript
复制
  //支持此特性的代码实现
  request.getHeaders("Authorization")
  startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase())

2.1.2 拼接在url中作为requestParam

代码语言:javascript
复制
http://localhost:8080/accounts/me?access_token=f732723d-af7f-41bb-bd06-2636ab2be135
//   //支持此特性的代码实现,key为access_token的参数
request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
2.1.3 在form表单中携带
代码语言:javascript
复制
http://localhost:8080/accounts/me
form param:
access_token=f732723d-af7f-41bb-bd06-2636ab2be135
//   //支持此特性的代码实现,key为access_token的参数
request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);

extractToken的完整代码:

代码语言:javascript
复制
//org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor#extractToken

  protected String extractToken(HttpServletRequest request) {
    // first check the header...
    String token = extractHeaderToken(request);

    // 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);
      }
    }

    return token;
  }

  /**
   * Extract the OAuth bearer token from a header.
   * 
   * @param request The request.
   * @return The token, or null if no OAuth authorization header was supplied.
   */
  protected String extractHeaderToken(HttpServletRequest request) {
    Enumeration<String> headers = request.getHeaders("Authorization");
    while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
      String value = headers.nextElement();
      //token前的bearer type是不区分大小写的
      if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
      //在请求头中传token时 bearer type和access_token之前的空格不限制:0个、多个都可以
        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.
        request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
            value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
        int commaIndex = authHeaderValue.indexOf(',');
        if (commaIndex > 0) {
          authHeaderValue = authHeaderValue.substring(0, commaIndex);
        }
        return authHeaderValue;
      }
    }
    returnnull;
  }
代码语言:javascript
复制

2.2 校验token是否合法

2.2.1 校验通过 将token中解析出来的数据放入SecurityContextHolder.getContext(),然后执行Filter链的下一个

代码语言:javascript
复制
// org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter#doFilter
        Authentication authResult = authenticationManager.authenticate(authentication);

        if (debug) {
          logger.debug("Authentication success: " + authResult);
        }

        eventPublisher.publishAuthenticationSuccess(authResult);
        SecurityContextHolder.getContext().setAuthentication(authResult);

2.2.2 校验不通过 请求执行结束,返回invalid token

代码语言:javascript
复制
{
  "error": "unauthorized",
  "error_description": "Full authentication is required to access this resource"
}

OAuth2AuthenticationProcessingFilter摘要:

代码语言:javascript
复制
   // org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter#doFilter
    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"));
// 将failed.getMessage()的封闭为返回的JSON
      authenticationEntryPoint.commence(request, response,
          new InsufficientAuthenticationException(failed.getMessage(), failed));

      return;
    }
代码语言:javascript
复制

代码语言:javascript
复制
// org.springframework.security.oauth2.provider.error.DefaultOAuth2ExceptionRenderer#writeWithMessageConverters 将org.springframework.security.oauth2.common.exceptions.InvalidTokenException convert为上述格式

// org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Serializer#serialize
@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2ExceptionJackson1Serializer.class)
@org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2ExceptionJackson1Deserializer.class)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2ExceptionJackson2Serializer.class)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2ExceptionJackson2Deserializer.class)
public class OAuth2Exception extends RuntimeException {
  ... 
  //
  @Override
  public String toString() {
    return getSummary();
  }

  /**
   * @return a comma-delimited list of details (key=value pairs)
   */
  public String getSummary() {
    
    StringBuilder builder = new StringBuilder();

    String delim = "";

    String error = this.getOAuth2ErrorCode();
    if (error != null) {
      builder.append(delim).append("error=\"").append(error).append("\"");
      delim = ", ";
    }

    String errorMessage = this.getMessage();
    if (errorMessage != null) {
      builder.append(delim).append("error_description=\"").append(errorMessage).append("\"");
      delim = ", ";
    }

    Map<String, String> additionalParams = this.getAdditionalInformation();
    if (additionalParams != null) {
      for (Map.Entry<String, String> param : additionalParams.entrySet()) {
        builder.append(delim).append(param.getKey()).append("=\"").append(param.getValue()).append("\"");
        delim = ", ";
      }
    }
    
    return builder.toString();

  }
代码语言:javascript
复制

代码语言:javascript
复制
// org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Serializer
  @Override
  public void serialize(OAuth2Exception value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
      JsonProcessingException {
        jgen.writeStartObject();
    jgen.writeStringField("error", value.getOAuth2ErrorCode());
    String errorMessage = value.getMessage();
    if (errorMessage != null) {
      errorMessage = HtmlUtils.htmlEscape(errorMessage);
    }
    jgen.writeStringField("error_description", errorMessage);
    if (value.getAdditionalInformation()!=null) {
      for (Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
        String key = entry.getKey();
        String add = entry.getValue();
        jgen.writeStringField(key, add);        
      }
    }
        jgen.writeEndObject();
  }

小结:

如果传了token,

代码语言:javascript
复制
org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter#doFilter

会进行解析。

解析失败时会使用

代码语言:javascript
复制
authenticationEntryPoint.commence(request, response,new InsufficientAuthenticationException(failed.getMessage(), failed));

来convert为一个固定的格式:

代码语言:javascript
复制
{
  "error": "unauthorized",
  "error_description": "Full authentication is required to access this resource"
}

解析成功时会将token中的信息放入SpringSecurity约定的一个上下文中:

代码语言:javascript
复制
SecurityContextHolder.getContext().setAuthentication(authResult);
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 的数字化之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Spring Security概览
  • OAuth2概览
  • 校验token
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档