SpringSecurityOAuth2的自定义返回方案

前言

使用Spring Security OAuth2时,源码对于返回消息没有做一个统一的封装。为了统一规范,我们可以采取一些措施。

下图是原初访问**/oauth/token**获取token时,成功和失败(源码抛出异常时的响应示例)

image-20230316145452918

image-20230316145519096

基于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类中接受了处理,并返回了结果

阅读源码发现image-20230316145920029

postAccessTokenhandleException方法处理了信息的返回

那么思路很暴力很简单,切点表达式,直接对这两个方法做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是我自己封装的同意返回体

得到返回结果:

image-20230316150151554

image-20230316150205344

基于全局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;

/**
* Oauth2登录认证
*/
@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;

/**
* jti
* JWT的唯一标识
* 可避免重放攻击
*/
private String jti;

}

对于异常信息,我直接定义一个全局异常来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

@ResponseBody//将信息返回为 json格式
@ExceptionHandler(Exception.class)//此方法捕获Exception异常
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//状态码返回500
public RestResponse<String> doException(Exception e) {
log.error("捕获异常:{}", e.getMessage());
return RestResponse.validFail(e.getMessage());
}

}

image-20230316154512487

image-20230316154522929

总结

为了”统一规范“,这里造成了不少的牺牲,让代码侵入性变的很强,我认为不如和前端约定好,关于OAuth2的模块的返回结果进行针对性的处理。