上文地址:SpringSecurityOAuth2(1)(password,authorization_code,refresh_token,client_credentials)获取token
上一篇博客写了一个至简的OAuth2的token认证服务器,只实现了4种获取token的方式 ,对于异常处理,以及无权处理,生成token前的数据完整性校验等等没有涉及,该篇文章对于这些内容做一些补充:
OAUth2的认证适配器AuthorizationServerConfigurerAdapter有三个主要的方法:
AuthorizationServerSecurityConfigurer:
配置令牌端点(Token Endpoint)的安全约束
ClientDetailsServiceConfigurer:
配置客户端详细服务, 客户端的详情在这里进行初始化
AuthorizationServerEndpointsConfigurer:
配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
1、请求前客户端信息完整校验
对于携带数据不完整的请求,可以直接返回给前端,不需要经过后面的验证 client信息一般以Base64编码放在Authorization 中 例如编码前为
client_name:111 (client_id:client_secret Base64编码)
Basic Y2xpZW50X25hbWU6MTEx
新建一个ClientDetailsAuthenticationFilter继承OncePerRequestFilter
/**
* @Description 客户端不带完整client处理
* @Author wwz
* @Date 2019/07/30
* @Param
* @Return
*/
@Component
public class ClientDetailsAuthenticationFilter extends OncePerRequestFilter {
private ClientDetailsService clientDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 只有获取token的时候需要携带携带客户端信息,放过其他
if (!request.getRequestURI().equals("/oauth/token")) {
filterChain.doFilter(request, response);
return;
}
String[] clientDetails = this.isHasClientDetails(request);
if (clientDetails == null) {
ResponseVo resultVo = new ResponseVo(HttpStatus.UNAUTHORIZED.value(), "请求中未包含客户端信息");
HttpUtilsResultVO.writerError(resultVo, response);
return;
}
this.handle(request, response, clientDetails, filterChain);
}
private void handle(HttpServletRequest request, HttpServletResponse response, String[] clientDetails, FilterChain filterChain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
filterChain.doFilter(request, response);
return;
}
MyClientDetails details = (MyClientDetails) this.getClientDetailsService().loadClientByClientId(clientDetails[0]);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(details.getClientId(), details.getClientSecret(), details.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(request, response);
}
/**
* 判断请求头中是否包含client信息,不包含返回null Base64编码
*/
private String[] isHasClientDetails(HttpServletRequest request) {
String[] params = null;
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null) {
String basic = header.substring(0, 5);
if (basic.toLowerCase().contains("basic")) {
String tmp = header.substring(6);
String defaultClientDetails = new String(Base64.getDecoder().decode(tmp));
String[] clientArrays = defaultClientDetails.split(":");
if (clientArrays.length != 2) {
return params;
} else {
params = clientArrays;
}
}
}
String id = request.getParameter("client_id");
String secret = request.getParameter("client_secret");
if (header == null && id != null) {
params = new String[]{id, secret};
}
return params;
}
public ClientDetailsService getClientDetailsService() {
return clientDetailsService;
}
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
}
然后在AuthorizationServerSecurityConfigurer中加入过滤链
/**
* 配置令牌端点(Token Endpoint)的安全约束
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// 加载client的 获取接口
clientDetailsAuthenticationFilter.setClientDetailsService(clientDetailsService);
// 客户端认证之前的过滤器
oauthServer.addTokenEndpointAuthenticationFilter(clientDetailsAuthenticationFilter);
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients(); // 允许表单登录
}
验证效果:
未携带client信息
携带client信息
2、自定义异常返回格式
OAuth2自带的异常返回格式是:
{
"error": "invalid_grant",
"error_description": "Bad credentials"
}
这个格式对前端来说不是很友好,我们期望的格式是:
{
"code":401,
"msg":"msg"
}
下面是具体实现:
新建MyOAuth2WebResponseExceptionTranslator实现 WebResponseExceptionTranslator接口 重写ResponseEntity
/**
* @Description WebResponseExceptionTranslator
* @Author wwz
* @Date 2019/07/30
* @Param
* @Return
*/
@Component
public class MyOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
// 异常栈获取 OAuth2Exception 异常
Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(
OAuth2Exception.class, causeChain);
// 异常栈中有OAuth2Exception
if (ase != null) {
return handleOAuth2Exception((OAuth2Exception) ase);
}
ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
causeChain);
if (ase != null) {
return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e));
}
ase = (AccessDeniedException) throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (ase instanceof AccessDeniedException) {
return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
}
ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer
.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
if (ase instanceof HttpRequestMethodNotSupportedException) {
return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase));
}
// 不包含上述异常则服务器内部错误
return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
}
private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
MyOAuth2Exception exception = new MyOAuth2Exception(e.getMessage(), e);
ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(exception, headers,
HttpStatus.valueOf(status));
return response;
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
this.throwableAnalyzer = throwableAnalyzer;
}
@SuppressWarnings("serial")
private static class ForbiddenException extends OAuth2Exception {
public ForbiddenException(String msg, Throwable t) {
super(msg, t);
}
public String getOAuth2ErrorCode() {
return "access_denied";
}
public int getHttpErrorCode() {
return 403;
}
}
@SuppressWarnings("serial")
private static class ServerErrorException extends OAuth2Exception {
public ServerErrorException(String msg, Throwable t) {
super(msg, t);
}
public String getOAuth2ErrorCode() {
return "server_error";
}
public int getHttpErrorCode() {
return 500;
}
}
@SuppressWarnings("serial")
private static class UnauthorizedException extends OAuth2Exception {
public UnauthorizedException(String msg, Throwable t) {
super(msg, t);
}
public String getOAuth2ErrorCode() {
return "unauthorized";
}
public int getHttpErrorCode() {
return 401;
}
}
@SuppressWarnings("serial")
private static class MethodNotAllowed extends OAuth2Exception {
public MethodNotAllowed(String msg, Throwable t) {
super(msg, t);
}
public String getOAuth2ErrorCode() {
return "method_not_allowed";
}
public int getHttpErrorCode() {
return 405;
}
}
}
定义自己的OAuth2Exception格式 MyOAuth2Exception
/**
* @Description 异常格式
* @Author wwz
* @Date 2019/07/30
* @Param
* @Return
*/
@JsonSerialize(using = MyOAuthExceptionJacksonSerializer.class)
public class MyOAuth2Exception extends OAuth2Exception {
public MyOAuth2Exception(String msg, Throwable t) {
super(msg, t);
}
public MyOAuth2Exception(String msg) {
super(msg);
}
}
定义异常的MyOAuth2Exception的序列化类 MyOAuth2ExceptionJacksonSerializer
/**
* @Description 定义异常MyOAuth2Exception的序列化
* @Author wwz
* @Date 2019/07/11
* @Param
* @Return
*/
public class MyOAuthExceptionJacksonSerializer extends StdSerializer<MyOAuth2Exception> {
protected MyOAuthExceptionJacksonSerializer() {
super(MyOAuth2Exception.class);
}
@Override
public void serialize(MyOAuth2Exception value, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException {
jgen.writeStartObject();
jgen.writeObjectField("code", value.getHttpErrorCode());
jgen.writeStringField("msg", value.getSummary());
jgen.writeEndObject();
}
}
将定义好的异常处理 加入到授权配置的 AuthorizationServerEndpointsConfigurer配置中
/**
* 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore()) // 配置token存储
.userDetailsService(userDetailsService) // 配置自定义的用户权限数据,不配置会导致token无法刷新
.authenticationManager(authenticationManager)
.tokenServices(defaultTokenServices())// 加载token配置
.exceptionTranslator(webResponseExceptionTranslator); // 自定义异常返回
}
演示效果:
3、自定义无权访问处理器
默认的无权访问返回格式是:
{
"error": "access_denied",
"error_description": "不允许访问"
}
我们期望的格式是:
{
"code":401,
"msg":"msg"
}
新建一个MyAccessDeniedHandler 实现AccessDeniedHandler,自定义返回信息:
/**
* @Description 无权访问处理器
* @Author wwz
* @Date 2019/07/30
* @Param
* @Return
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseVo resultVo = new ResponseVo();
resultVo.setMessage("无权访问!");
resultVo.setCode(403);
HttpUtilsResultVO.writerError(resultVo, response);
}
}
在ResourceServerConfigurerAdapter资源配置中增加
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 无权处理器
因为我在请求上增加了注解权限只能ROLE_USER用户访问,然后我登录的是ROLE_ADMIN用户,所以无权处理。
@GetMapping("/hello")
@PreAuthorize("hasRole('ROLE_USER')")
public String hello(Principal principal) {
return principal.getName() + " has hello Permission";
}
4、自定义token无效处理器
默认的token无效返回信息是:
{
"error": "invalid_token",
"error_description": "Invalid access token: 78df4214-8e10-46ae-a85b-a8f5247370a"
}
我们期望的格式是:
{
"code":403,
"msg":"msg"
}
新建MyTokenExceptionEntryPoint 实现AuthenticationEntryPoint
/**
* @Description 无效Token返回处理器
* @Author wwz
* @Date 2019/07/30
* @Param
* @Return
*/
@Component
public class MyTokenExceptionEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Throwable cause = authException.getCause();
response.setStatus(HttpStatus.OK.value());
response.setHeader("Content-Type", "application/json;charset=UTF-8");
try {
HttpUtilsResultVO.writerError(new ResponseVo(401, authException.getMessage()), response);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在 资源配置中ResourceServerConfigurerAdapter中注入:
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(tokenExceptionEntryPoint); // token失效处理器
resources.resourceId("auth"); // 设置资源id 通过client的 scope 来判断是否具有资源权限
}
展示效果: