前言
使用Spring Security OAuth2时,源码对于返回消息没有做一个统一的封装。为了统一规范,我们可以采取一些措施。
下图是原初访问**/oauth/token**获取token时,成功和失败(源码抛出异常时的响应示例)
基于AOP的切点表示式
观察控制台日志:
1
| 2023-03-16 14:55:08.940 WARN 25476 --- [io-63070-exec-5] o.s.s.o.p.e.TokenEndpoint : Handling error: InvalidGrantException, 用户名或密码错误
|
可以得到程序是在TokenEndpoint类中接受了处理,并返回了结果
阅读源码发现
postAccessToken和handleException方法处理了信息的返回
那么思路很暴力很简单,切点表达式,直接对这两个方法做AOP处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Component @Aspect public class AuthTokenAspect {
@Around("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))") public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable { RestResponse<Object> response = new RestResponse<>(); Object proceed = pjp.proceed(); if (proceed != null) { ResponseEntity<OAuth2AccessToken> responseEntity = (ResponseEntity<OAuth2AccessToken>) proceed; OAuth2AccessToken body = responseEntity.getBody(); if (responseEntity.getStatusCode().is2xxSuccessful()) { response.setCode(0); Map<String, Object> map = new HashMap<>(); assert body != null; map.put("access_token", body.getValue()); map.put("token_type", body.getTokenType()); map.put("refresh_token", body.getRefreshToken().getValue()); map.put("expires_in", body.getExpiresIn()); map.put("scope", body.getScope()); map.put("jti", body.getAdditionalInformation().get("jti")); response.setData(map); response.setMsg("登录成功"); } else { response.setCode(-1); response.setMsg("登录失败"); } } return ResponseEntity .status(200) .body(response); }
@Around("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.handleException(..))") public Object handleException(ProceedingJoinPoint pjp) throws Throwable { Object proceed = pjp.proceed(); ResponseEntity<OAuth2Exception> response = (ResponseEntity<OAuth2Exception>) proceed; return ResponseEntity .status(200) .body(RestResponse.validFail(Objects.requireNonNull(response.getBody()).getMessage())); }
}
|
其中,RestResponse
是我自己封装的同意返回体
得到返回结果:
基于全局MVC增强
本到这里就结束了,可笔者认为此方法不过完美,在网上得到了重写/oauth/token
等MVC端点,再使用@ControllerAdvice增强来实现,十分优雅。故做此纪录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @RestController @RequestMapping("/oauth") public class AuthorityController {
@Resource private TokenEndpoint tokenEndpoint;
@RequestMapping(value = "/token", method = RequestMethod.POST) public RestResponse<Oauth2TokenDto> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder() .accessToken(oAuth2AccessToken.getValue()) .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) .expiresIn(oAuth2AccessToken.getExpiresIn()) .tokenHead("Bearer ") .scope(oAuth2AccessToken.getScope()) .jti(oAuth2AccessToken.getAdditionalInformation().get("jti").toString()).build(); return RestResponse.success(oauth2TokenDto); } }
|
这里新建一个Controller类重写了postAccessToken的处理逻辑,并新建了OAuth2AccessToken类返回结果信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Data @Builder public class Oauth2TokenDto {
private String accessToken;
private String refreshToken;
private String tokenHead;
private int expiresIn;
private Set<String> scope;
private String jti;
}
|
对于异常信息,我直接定义一个全局异常来处理:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Slf4j @ControllerAdvice public class GlobalExceptionHandler {
@ResponseBody @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public RestResponse<String> doException(Exception e) { log.error("捕获异常:{}", e.getMessage()); return RestResponse.validFail(e.getMessage()); }
}
|
总结
为了”统一规范“,这里造成了不少的牺牲,让代码侵入性变的很强,我认为不如和前端约定好,关于OAuth2的模块的返回结果进行针对性的处理。