RicardoZym 发表于 2022-4-9 16:18

oauth使用token时自定义返回结构体

## 自定义获取token,使用token异常时的返回结构体

我们都知道在使用OAuth2获取token及刷新和check token时,返回的结果是不规律的,如下


但正常情况下,许多接口调用都希望有统一的返回结构体,以便于能够正常解析,如 code,msg,data这种格式的返回结构体。
我们通过查看源码,发现TokenEndpoint这个类是token调用的接口类,我们找到获取token的接口代码如下:


我们无法编辑oauth2的源代码,但我们可以通过切面的方式进行数据处理。

如下:

```java
@Component
@Aspect
public class AuthTokenAspect {

    //日志
    private static final Logger LOG = LoggerFactory.getLogger(AuthTokenAspect.class);

    /** 定义获取token切入点 */
    @Pointcut("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
    public void tokenPoint(){
    }

    @Around("tokenPoint()")
    public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
      GenericResult result = new GenericResult();
      try {
            Object proceed = pjp.proceed();
            if (null != proceed) {
                ResponseEntity<OAuth2AccessToken> responseEntity = (ResponseEntity<OAuth2AccessToken>)proceed;
                OAuth2AccessToken body = responseEntity.getBody();
                if (responseEntity.getStatusCode().is2xxSuccessful()) {
                  result.setMsg(BaseConstants.SUCCESS);
                  result.setData(body);
                } else {
                  LOG.error("获取token 错误:{}", responseEntity.getStatusCode().toString());
                  result.setCode(String.valueOf(responseEntity.getStatusCode().value()));
                  result.setMsg(responseEntity.getStatusCode().name());
                  result.setData(body);
                }
            }
      } catch (Exception e) {
            String message = e.getMessage();
            if (message.contains("expired")) {
                result.setCode("ec_expired_token");
                result.setMsg("token过期,请重新登陆");
                result.setData(message);
            } else {
                result.setCode("ec_invalid_token");
                result.setMsg(message);
            }
      }
      return ResponseEntity
                .status(200)
                .body(result);
    }
}
```

在这里进行如此处理,就能够在获取token接口时返回一个自定义的返回结构体了。

但是以上处理只是针对获取token接口的返回结构体,当你在调用其他方法接口需要进行token验证但你携带的token过期了或者是无效token时,我们会被直接拦截在网关getway里。因为网关这里会对你的调用携带的token进行校验,不通过直接返回,这样就会导致又无法统一返回结构体了。

我们可以在网关中做如下配置:

首先我们找到出现此问题的原因源代码: OAuth2AuthenticationProcessingFilter这个过滤器 找到如下这段作妖的代码,当你的携带的token过期或失效的时候,执行这行代码就会出错,Authentication authentication = tokenExtractor.extract(request);然后被catch到,进行数据处理,然后返回。所以我们要针对catch后的数据处理做调整。

```java
        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);
        }
```

我们看到这行数据处理,authenticationEntryPoint.commence(request, response,
                                        new InsufficientAuthenticationException(failed.getMessage(), failed)); 然后我们发现这个是可以进行配置的。所以首先写如下类

```java
package com.travelsky.etermcloud.gateway.config;

import com.alibaba.fastjson.JSON;
import com.travelsky.etermcloud.gateway.vo.GenericResult;
import org.apache.http.entity.ContentType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Component
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {


    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws ServletException {

      try {
            GenericResult result = new GenericResult();
            String message = authException.getMessage();
            //区分 无效token和 过期token的错误码
            if (message.contains("expired")) {
                result.setCode("ec_expired_token");
                result.setMsg("token过期,请重新登陆");
                result.setData(message);
            } else {
                result.setCode("ec_invalid_token");
                result.setMsg(message);
            }
            response.setContentType(ContentType.APPLICATION_JSON.toString());
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write(JSON.toJSONString(result));
      } catch (Exception e) {
            throw new ServletException();
      }
    }

}
```

然后我们再将它在config中进行配置,如下:

```java
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        AuthExceptionEntryPoint authExceptionEntryPoint;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
                resources.authenticationEntryPoint(authExceptionEntryPoint);
        }
}
```

这样就ok了,以上只是我个人的简单处理,如有不正确请指正。有更好的建议也可探讨自定义获取token,使用token异常时的返回结构体我们都知道在使用OAuth2获取token及刷新和check token时,返回的结果是不规律的,如下file://C:\Users\zym\Desktop\MD文档\学习\img\image-20220407105411097.png?lastModify=1649492035但正常情况下,许多接口调用都希望有统一的返回结构体,以便于能够正常解析,如 code,msg,data这种格式的返回结构体。我们通过查看源码,发现TokenEndpoint这个类是token调用的接口类,我们找到获取token的接口代码如下:file://C:\Users\zym\Desktop\MD文档\学习\img\image-20220409155016513.png?lastModify=1649492035我们无法编辑oauth2的源代码,但我们可以通过切面的方式进行数据处理。如下:@Component
@Aspect
public class AuthTokenAspect {

    //日志
    private static final Logger LOG = LoggerFactory.getLogger(AuthTokenAspect.class);

    /** 定义获取token切入点 */
    @Pointcut("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
    public void tokenPoint(){
    }

    @Around("tokenPoint()")
    public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
      GenericResult result = new GenericResult();
      try {
            Object proceed = pjp.proceed();
            if (null != proceed) {
                ResponseEntity<OAuth2AccessToken> responseEntity = (ResponseEntity<OAuth2AccessToken>)proceed;
                OAuth2AccessToken body = responseEntity.getBody();
                if (responseEntity.getStatusCode().is2xxSuccessful()) {
                  result.setMsg(BaseConstants.SUCCESS);
                  result.setData(body);
                } else {
                  LOG.error("获取token 错误:{}", responseEntity.getStatusCode().toString());
                  result.setCode(String.valueOf(responseEntity.getStatusCode().value()));
                  result.setMsg(responseEntity.getStatusCode().name());
                  result.setData(body);
                }
            }
      } catch (Exception e) {
            String message = e.getMessage();
            if (message.contains("expired")) {
                result.setCode("ec_expired_token");
                result.setMsg("token过期,请重新登陆");
                result.setData(message);
            } else {
                result.setCode("ec_invalid_token");
                result.setMsg(message);
            }
      }
      return ResponseEntity
                .status(200)
                .body(result);
    }
}在这里进行如此处理,就能够在获取token接口时返回一个自定义的返回结构体了。 但是以上处理只是针对获取token接口的返回结构体,当你在调用其他方法接口需要进行token验证但你携带的token过期了或者是无效token时,我们会被直接拦截在网关getway里。因为网关这里会对你的调用携带的token进行校验,不通过直接返回,这样就会导致又无法统一返回结构体了。我们可以在网关中做如下配置:首先我们找到出现此问题的原因源代码: OAuth2AuthenticationProcessingFilter这个过滤器 找到如下这段作妖的代码,当你的携带的token过期或失效的时候,执行这行代码就会出错,Authentication authentication = tokenExtractor.extract(request);然后被catch到,进行数据处理,然后返回。所以我们要针对catch后的数据处理做调整。    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);
    }我们看到这行数据处理,authenticationEntryPoint.commence(request, response,                                        new InsufficientAuthenticationException(failed.getMessage(), failed)); 然后我们发现这个是可以进行配置的。所以首先写如下类package com.travelsky.etermcloud.gateway.config;

import com.alibaba.fastjson.JSON;
import com.travelsky.etermcloud.gateway.vo.GenericResult;
import org.apache.http.entity.ContentType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Component
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {


    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws ServletException {

      try {
            GenericResult result = new GenericResult();
            String message = authException.getMessage();
            //区分 无效token和 过期token的错误码
            if (message.contains("expired")) {
                result.setCode("ec_expired_token");
                result.setMsg("token过期,请重新登陆");
                result.setData(message);
            } else {
                result.setCode("ec_invalid_token");
                result.setMsg(message);
            }
            response.setContentType(ContentType.APPLICATION_JSON.toString());
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write(JSON.toJSONString(result));
      } catch (Exception e) {
            throw new ServletException();
      }
    }

}然后我们再将它在config中进行配置,如下:@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    AuthExceptionEntryPoint authExceptionEntryPoint;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
      resources.authenticationEntryPoint(authExceptionEntryPoint);
    }
}这样就ok了,以上只是我个人的简单处理,如有不正确请指正。有更好的建议也可探讨

justyvan 发表于 2022-4-14 22:48

mark一下

孤梦丨 发表于 2022-5-13 22:01

markmark

枕下的悲情 发表于 2022-5-29 13:48

感谢分享EXP。
页: [1]
查看完整版本: oauth使用token时自定义返回结构体