Spring boot+Security OAuth2 爬坑日记(4)自定义异常处理 上

上篇 Spring boot+Security OAuth2 爬坑日记(3)自定义登录和授权页面

为了方便与前端更好的交互,服务端要提供友好统一的信息返回格式,(他好我也好 ->_-> ),Spring Security OAuth2 提供了自定义异常的入口;我们需要做的就是实现对应的接口,然后将实现的类配置到对应的入口即可。默认的信息返回格式如下:

{
    "error": "invalid_grant",
    "error_description": "Bad credentials"
}

需要处理的其实就两个地方的异常信息,分别是认证服务器的异常信息资源服务器的异常信息;现在就从这两个地方入手

认证服务器已异常处理

自定义 ExceptionTranslator 实现认证服务器的异常信息处理,新建类 BootOAuth2WebResponseExceptionTranslator 实现WebResponseExceptionTranslator 接口,实现其ResponseEntity<OAuth2Exception> translate(Exception e)方法;认证发生的异常在这里能捕获到,在这里我们可以将我们的异常信息封装成统一的格式返回即可,这里怎么处理因项目而异,这里我直接复制了DefaultWebResponseExceptionTranslator 实现方法,我这里要处理的格式如下:

{
	"status":401,
	"msg":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxmsg"
}
  1. 定义自己的OAuth2Exception

    @JsonSerialize(using = BootOAuthExceptionJacksonSerializer.class)
    public class BootOAuth2Exception extends OAuth2Exception {
        public BootOAuth2Exception(String msg, Throwable t) {
            super(msg, t);
        }
    
        public BootOAuth2Exception(String msg) {
            super(msg);
        }
    }	
    
  2. 定义异常BootOAuth2Exception 的序列化类

    	public class BootOAuthExceptionJacksonSerializer extends StdSerializer<BootOAuth2Exception> {
    
        	protected BootOAuthExceptionJacksonSerializer() {
        	   super(BootOAuth2Exception.class);
        	}
    	
    	    @Override
            public void serialize(BootOAuth2Exception value, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException {
                jgen.writeStartObject();
                jgen.writeObjectField("status", value.getHttpErrorCode());
                String errorMessage = value.getOAuth2ErrorCode();
                if (errorMessage != null) {
                    errorMessage = HtmlUtils.htmlEscape(errorMessage);
                }
                jgen.writeStringField("msg", errorMessage);
                if (value.getAdditionalInformation()!=null) {
                    for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
                        String key = entry.getKey();
                        String add = entry.getValue();
                        jgen.writeStringField(key, add);
                    }
                }
                jgen.writeEndObject();
            }
    }
    
  3. 定义自己的WebResponseExceptionTranslator 类名为BootOAuth2WebResponseExceptionTranslator

    @Component("bootWebResponseExceptionTranslator")
    public class BootOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator {
    
        private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
    
    
        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()));
            }
            
            BootOAuth2Exception exception = new BootOAuth2Exception(e.getMessage(),e);
    
            ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(exception, headers,
                    HttpStatus.valueOf(status));
    
            return response;
    
        }
        
        ..........
    
    
  4. BootOAuth2WebResponseExceptionTranslator 类加入授权服务器的配置中

    @Configuration
    @EnableAuthorizationServer
    public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
        ......
        
        @Autowired
        private WebResponseExceptionTranslator bootWebResponseExceptionTranslator;
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            ......
            
            // 处理 ExceptionTranslationFilter 抛出的异常
            endpoints.exceptionTranslator(bootWebResponseExceptionTranslator);
            
            ......
          
        }
    }
    

到这里你以为服务端的自定义异常就结束了;然而并没有结束,在代码中我的客户端信息每次都是放在请求头中进行发送,当我们的客户端信息不正确时服务端不会发送错误json信息而是让你重新登录,在一些app中是不能使用网页的,所以我们定义一个自己filter来处理客户端认证逻辑,filter如下:

@Component
public class BootBasicAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private ClientDetailsService clientDetailsService;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {


        if (!request.getRequestURI().equals("/oauth/token") ||
                !request.getParameter("grant_type").equals("password")) {
            filterChain.doFilter(request, response);
            return;
        }

        String[] clientDetails = this.isHasClientDetails(request);

        if (clientDetails == null) {
            BaseResponse bs = HttpResponse.baseResponse(HttpStatus.UNAUTHORIZED.value(), "请求中未包含客户端信息");
            HttpUtils.writerError(bs, 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;
        }


        BootClientDetails details = (BootClientDetails) this.clientDetailsService.loadClientByClientId(clientDetails[0]);
        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(details.getClientId(), details.getClientSecret(), details.getAuthorities());

        SecurityContextHolder.getContext().setAuthentication(token);


        filterChain.doFilter(request,response);
    }

    // 判断请求头中是否包含client信息,不包含返回false
    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;
    }
}

写好我们的filter之后,将其配置在BasicAuthenticationFilter之前配置如下

public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
		
	......
	
    @Autowired
    private BootBasicAuthenticationFilter filter;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		......
	
        security.addTokenEndpointAuthenticationFilter(filter);

       .......

    }
}

到这里认证服务器的异常处理的差不多了,下面有个问题;

上述的处理流程只能捕获ExceptionTranslationFilter中抛出的异常,当我在认证服务器有如下配置时,当使用表单登录发生异常时我们置的WebResponseExceptionTranslator是捕获不到异常的;

public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
		
	......
	
    @Autowired
    private BootBasicAuthenticationFilter filter;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		......

       // 允许表单登录
        security.allowFormAuthenticationForClients();
       .......

    }
}

获取token时需要客户端在Form表单中带上客户端的client_idclient_secret,此时的ClientCredentialsTokenEndpointFilter 会去检查client_idclient_secret的合法性,如果不合法抛出的异常由其自己在filter内部实例化的OAuth2AuthenticationEntryPoint来处理该异常,所以上面定义的BootOAuth2WebResponseExceptionTranslator 捕获不到该异常;看如下源码分析,重点看中文注释

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	
	// filter
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
			// 调用子类的 attemptAuthentication(request, response) 方法,这里是调用ClientCredentialsTokenEndpointFilter 的attemptAuthentication方法
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		// 客户端信息不合法(client_id不存在或client_secret不正确)抛出的异常,调用unsuccessfulAuthentication方法处理
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}

		successfulAuthentication(request, response, chain, authResult);
	}

	
	
	
	public abstract Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException, IOException,
			ServletException;

	// 登录失败处理
	protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication request failed: " + failed.toString(), failed);
			logger.debug("Updated SecurityContextHolder to contain null Authentication");
			logger.debug("Delegating to authentication failure handler " + failureHandler);
		}

		rememberMeServices.loginFail(request, response);
		/**
		* 调用其子类 ClientCredentialsTokenEndpointFilter 的afterPropertiesSet()方法中的设置的onAuthenticationFailure方法,这个地方有点绕,
		* 自己跑几遍源码看看就能理解了,接下来就是去看ClientCredentialsTokenEndpointFilter 中的实现
		* */	
		failureHandler.onAuthenticationFailure(request, response, failed);
	}

}
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
	// 异常处理
	private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
	private boolean allowOnlyPost = false;
	public ClientCredentialsTokenEndpointFilter() {
		this("/oauth/token");
	}

	public ClientCredentialsTokenEndpointFilter(String path) {
		super(path);
		setRequiresAuthenticationRequestMatcher(new ClientCredentialsRequestMatcher(path));
		// If authentication fails the type is "Form"
		((OAuth2AuthenticationEntryPoint) authenticationEntryPoint).setTypeName("Form");
	}

	public void setAllowOnlyPost(boolean allowOnlyPost) {
		this.allowOnlyPost = allowOnlyPost;
	}

	/**
	 * @param authenticationEntryPoint the authentication entry point to set
	 */
	public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
		this.authenticationEntryPoint = authenticationEntryPoint;
	}

	// 这个方法在bean初始化时调用
	@Override
	public void afterPropertiesSet() {
		super.afterPropertiesSet();
		setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
			public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
					AuthenticationException exception) throws IOException, ServletException {
				if (exception instanceof BadCredentialsException) {
					exception = new BadCredentialsException(exception.getMessage(), new BadClientCredentialsException());
				}
				authenticationEntryPoint.commence(request, response, exception);
			}
		});
		setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
			public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
					Authentication authentication) throws IOException, ServletException {
				// no-op - just allow filter chain to continue to token endpoint
			}
		});
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {
		if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] { "POST" });
		}
		String clientId = request.getParameter("client_id");
		String clientSecret = request.getParameter("client_secret");
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null && authentication.isAuthenticated()) {
			return authentication;
		}
		if (clientId == null) {
			throw new BadCredentialsException("No client credentials presented");
		}
		if (clientSecret == null) {
			clientSecret = "";
		}
		clientId = clientId.trim();
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
				clientSecret);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, Authentication authResult) throws IOException, ServletException {
		super.successfulAuthentication(request, response, chain, authResult);
		chain.doFilter(request, response);
	}

	protected static class ClientCredentialsRequestMatcher implements RequestMatcher {
		private String path;
		public ClientCredentialsRequestMatcher(String path) {
			this.path = path;
		}
		@Override
		public boolean matches(HttpServletRequest request) {
			String uri = request.getRequestURI();
			int pathParamIndex = uri.indexOf(';');
			if (pathParamIndex > 0) {
				// strip everything after the first semi-colon
				uri = uri.substring(0, pathParamIndex);
			}
			String clientId = request.getParameter("client_id");
			if (clientId == null) {
				// Give basic auth a chance to work instead (it's preferred anyway)
				return false;
			}
			if ("".equals(request.getContextPath())) {
				return uri.endsWith(path);
			}
			return uri.endsWith(request.getContextPath() + path);
		}
	}
}

先看看ClientCredentialsTokenEndpointFilter是如何实现的,流程如下
客户端认证流程

在默认的配置中是不予许表单登录的,具体原因不清楚;如果小伙伴们有强迫症偏要使用表单登录我也没辙啊

测试效果

测试效果

下篇 Spring boot+Security OAuth2 爬坑日记(5)自定义异常处理 下

参考文章和文档

  1. Spting Security OAuth
  2. Spring Security Oauth2 自定义 OAuth2 Exception

微信公众号
在这里插入图片描述

源码地址 Github
  • 7
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
### 回答1: springcloud是一个开源的微服务框架,它基于Spring Boot,并提供了一整套解决方案,用于构建分布式系统中的各个微服务。通过使用springcloud,我们可以轻松实现服务注册与发现、负载均衡、断路器、配置中心等功能,简化了微服务开发和管理的复杂度。 springboot是一个基于Spring的轻量级开发框架,它通过开箱即用的原则,提供了一种快速构建应用程序的方式。使用springboot,我们可以简化繁琐的配置,只需少量的代码即可实现一个功能完整的应用程序,并且可以方便地和其他Spring生态的框架进行集成。 OAuth2是一种授权协议,用于保护Web应用程序、移动应用程序和API的资源。通过OAuth2协议,用户可以授权第三方应用程序访问他们的资源,而无需提供他们的密码。它提供了一种安全且可扩展的机制来处理用户身份验证和授权,并且被广泛应用于各种应用程序中。 Spring Security是一个Java框架,用于提供身份验证和访问控制的功能。它可以轻松地集成到Spring应用程序中,提供了一套强大的API和安全策略,用于保护应用程序免受各种攻击,包括身份验证和授权、会话管理、密码加密等。 Redis是一种内存数据存储系统,它以键值对的形式存储数据,并支持多种数据结构,如字符串、列表、集合、有序集合等。Redis具有高速、持久化和可扩展性等特点,可用于缓存、消息队列、分布式锁等各种场景。在使用Spring框架开发时,我们可以使用Redis作为缓存层,提高应用程序的性能和响应速度。 综上所述,Spring Cloud提供了构建和管理微服务的解决方案,Spring Boot简化了应用程序的开发,OAuth2和Spring Security提供了安全和授权的功能,而Redis作为内存数据存储系统,为应用程序提供了可扩展的缓存和数据存储能力。这些技术和框架相互协作,可以帮助开发者更快速、更安全地构建分布式系统。 ### 回答2: Spring Cloud是一个用于构建分布式系统的开发工具包,它提供了多个子项目来解决分布式系统的常见问题,例如服务注册与发现、配置管理、断路器、负载均衡等。Spring Boot是用于简化Spring应用程序开发的工具,它提供了一种自动配置的方式来快速搭建和运行Spring应用。OAuth2是一个开放标准,用于授权访问特定资源,它允许用户使用某个网站的授权信息来访问其他网站上的受保护资源。Spring Security是一个全面的身份验证和授权框架,它提供了一套安全服务,用于保护Web应用程序中的资源。Redis是一个高性能的键值存储系统,它常被用作缓存、队列、消息中间件等。 结合以上几个技术,可以构建一个基于Spring Cloud的分布式系统,使用Spring Boot快速搭建各个服务,使用Spring Security进行身份验证和授权管理。而OAuth2可以用于保护系统中的资源,通过认证服务器进行用户认证和授权,使得只有授权的用户才能访问相应的资源。Spring SecurityOAuth2可以集成使用,通过Spring Security提供的权限管理功能来管理不同角色对资源的访问权限。同时,将Redis作为缓存服务器,可用于提高系统的性能和响应速度。 总之,Spring Cloud、Spring BootOAuth2、Spring Security和Redis等技术可以在构建分布式系统时发挥重要作用,帮助我们快速搭建实现各个功能模块,并提供高性能和安全性。 ### 回答3: Spring Cloud是一套基于Spring Boot的微服务框架,它提供了在分布式系统中构建和管理各种微服务的解决方案。它具有服务注册与发现、负载均衡、熔断、服务网关等功能,可以方便地实现微服务架构。 Spring Boot是一个用于快速开发基于Spring框架的应用程序的工具,它简化了Spring应用程序的配置和部署流程。它提供了自动化配置、内嵌服务器、开箱即用的特性,使得我们只需要关注业务逻辑的开发而不用过多关注框架的配置。 OAuth2是一种开放标准的授权协议,它使得用户可以通过授权的方式将与用户相关的信息共享给第三方应用程序。它使用令牌的方式进行授权,具有安全性高、可扩展性好的优点,常用于实现单点登录和授权管理。 Spring Security是一个用于在Java应用程序中提供身份验证和访问控制的框架。它可以与Spring BootSpring Cloud集成,提供了认证、授权、密码加密等功能,帮助我们更好地保护应用程序的安全。 Redis是一种高性能的键值存储系统,它支持多种数据结构,如字符串、列表、哈希表等。它具有高并发读写、持久化、分布式等特点,常用于缓存、消息队列、会话管理等场景。 综上所述,Spring Cloud提供了构建微服务的解决方案,Spring Boot简化了Spring应用程序的开发,OAuth2实现了授权管理,Spring Security提供了身份验证和访问控制,而Redis则可以用于缓存和数据存储。这些技术的结合可以帮助我们构建安全、高效的分布式系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值