自定义获取token,使用token异常时的返回结构体
我们都知道在使用OAuth2获取token及刷新和check token时,返回的结果是不规律的,如下
但正常情况下,许多接口调用都希望有统一的返回结构体,以便于能够正常解析,如 code,msg,data这种格式的返回结构体。
我们通过查看源码,发现TokenEndpoint这个类是token调用的接口类,我们找到获取token的接口代码如下:
我们无法编辑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了,以上只是我个人的简单处理,如有不正确请指正。有更好的建议也可探讨