SpringSecurity(二)OAuth2认证详解-spring-security-oauth2-authorization-server

2023-04-24 07:48:10

 1、OAuth2.0 简介

OAuth 2.0是用于授权的行业标准协议。OAuth 2.0为简化客户端开发提供了特定的授权流,包括Web应用、桌面应用、移动端应用等。

1.1 OAuth2.0 相关名词解释Resource owner(资源拥有者):拥有该资源的最终用户,他有访问资源的账号密码;Resource server(资源服务器):拥有受保护资源的服务器,如果请求包含正确的访问令牌,可以访问资源;Client(客户端):访问资源的客户端,会使用访问令牌去获取资源服务器的资源,可以是浏览器、移动设备或者服务器;Authorization server(认证服务器):用于认证用户的服务器,如果客户端认证通过,发放访问资源服务器的令牌。1.2 四种授权模式Authorization Code(授权码模式):正宗的OAuth2的授权模式,客户端先将用户导向认证服务器,登录后获取授权码,然后进行授权,最后根据授权码获取访问令牌;Implicit(简化模式):和授权码模式相比,取消了获取授权码的过程,直接获取访问令牌;Resource Owner Password Credentials(密码模式):客户端直接向用户获取用户名和密码,之后向认证服务器获取访问令牌;Client Credentials(客户端模式):客户端直接通过客户端认证(比如client_id和client_secret)从认证服务器获取访问令牌。1.3 、OAuth2框架

Spring Security提供了OAuth 2.0 完整支持,主要包括:

OAuth 2.0核心 - spring-security-oauth2-core.jar:包含为OAuth 2.0授权框架和OpenID Connect Core 1.0提供支持的核心类和接口;OAuth 2.0客户端 - spring-security-oauth2-client.jar:Spring Security对OAuth 2.0授权框架和OpenID Connect Core 1.0的客户端支持;OAuth 2.0 JOSE - spring-security-oauth2-jose.jar:包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。框架旨在提供安全地传输双方之间的权利要求的方法。它由一系列规范构建: JSON Web令牌(JWT) JSON Web签名(JWS) JSON Web加密(JWE) JSON Web密钥(JWK)

要使用OAuth2,需要引入spring-security-oauth2模块,通过之前源码分析,Spring 通过OAuth2ImportSelector类对Oauth2.0进行支持,当引入oauth2模块,Spring会自动启用 OAuth2 客户端配置 OAuth2ClientConfiguration。

1.4 OAuth 2.0客户端提供功能

OAuth 2.0客户端功能为OAuth 2.0授权框架中定义的客户端角色提供支持。 可以使用以下主要功能:

授权代码授予客户凭证授权Servlet环境的WebClient扩展(用于发出受保护的资源请求)

HttpSecurity.oauth2Client()提供了许多用于自定义OAuth 2.0 Client的配置选项。

@EnableWebSecurity public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Client() .clientRegistrationRepository(this.clientRegistrationRepository()) .authorizedClientRepository(this.authorizedClientRepository()) .authorizedClientService(this.authorizedClientService()) .authorizationCodeGrant() .authorizationRequestRepository(this.authorizationRequestRepository()) .authorizationRequestResolver(this.authorizationRequestResolver()) .accessTokenResponseClient(this.accessTokenResponseClient()); } } 2、OAuth 2.0 认证服务

Spring Security OAuth2 实现了OAuth 2.0授权服务,简化了程序员对OAuth 2.0的实现,仅需要简单配置OAuth 2.0认证参数即可快速实现认证授权功能。

2.1 Spring Security OAuth2 提供的程序实现

Spring Security OAuth2 中的提供者角色实际上是在授权服务和资源服务之间分配的,使用Spring Security OAuth2,您可以选择将它们拆分到两个应用程序中,并具有多个共享的资源服务授权服务。

2.1.1 授权服务

对令牌的请求由Spring MVC控制器端点处理,对受保护资源的访问由标准Spring Security请求过滤器处理。为了实现OAuth 2.0授权服务器,Spring Security过滤器链中需要以下端点:

AuthorizationEndpoint用于服务于授权请求。预设网址:/oauth/authorize。TokenEndpoint用于服务访问令牌的请求。预设网址:/oauth/token。2.1.2 资源服务

要实现OAuth 2.0资源服务器,需要以下过滤器:

将OAuth2AuthenticationProcessingFilter用于加载的身份验证给定令牌的认证访问请求。2.2 集成 OAuth 2.0 认证授权及资源管理2.2.1 项目准备引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- ... other dependency elements ... --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> 2.2.1 配置授权服务

在配置授权服务器时,必须考虑客户端用于从最终用户获取访问令牌的授予类型(例如,授权代码,用户凭据,刷新令牌)。服务器的配置用于提供客户端详细信息服务和令牌服务的实现,并全局启用或禁用该机制的某些方面。但是请注意,可以为每个客户端专门配置权限,使其能够使用某些授权机制和访问授权。也就是说,仅因为您的提供程序配置为支持“客户端凭据”授予类型,并不意味着授权特定的客户端使用该授予类型。

使用@EnableAuthorizationServer注解开启Oauth2认证。

@EnableAuthorizationServer批注用于配置OAuth 2.0授权服务器机制以及任何@Beans实现的机制

AuthorizationServerConfigurer(有一个便捷的适配器实现,其中包含空方法)。以下功能委托给由Spring创建并传递到的单独的配置器

AuthorizationServerConfigurer: ClientDetailsServiceConfigurer:定义客户端详细信息服务的配置程序。可以初始化客户详细信息,或者您可以仅引用现有商店。AuthorizationServerSecurityConfigurer:定义令牌端点上的安全约束。AuthorizationServerEndpointsConfigurer:定义授权和令牌端点以及令牌服务。

提供者配置的一个重要方面是将授权代码提供给OAuth客户端的方式(在授权代码授予中)。OAuth客户端通过将最终用户定向到授权页面来获得授权码,用户可以在该页面上输入她的凭据,从而导致从提供者授权服务器重定向回带有授权码的OAuth客户端。

源码清单:

@Configuration @EnableAuthorizationServer public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManager; @Autowired private UserService userService; /** * 自定义授权服务配置 * 使用密码模式需要配置 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints.authenticationManager(authenticationManager) .userDetailsService(userService); } /** * 配置认证客户端 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //自定义客户端配置 } /** * 自定义授权令牌端点的安全约束 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //自定义安全约束 //.... } } 2.2.1.1 授权服务配置

AuthorizationServerEndpointsConfigurer 定义授权和令牌端点以及令牌服务。

endpoints.tokenStore(tokenStore)//自定义令牌存储策略 //默认除密码模式外,所有授权模式均支持,密码模式需要显示注入authenticationManager开启 .authenticationManager(authenticationManager) .userDetailsService(userDetailServiceImpl)//自定义用户密码加载服务 .tokenGranter(tokenGranter)//定义控制授权 .exceptionTranslator(webResponseExceptionTranslator);//自定义异常解析 2.2.1.2 客户端加载策略配置

将ClientDetailsServiceConfigurer(从您的回调AuthorizationServerConfigurer)可以用来定义一个内存中或JDBC实现客户的细节服务。客户的重要属性是:

clientId:(必填)客户端ID。secret:(对于受信任的客户端是必需的)客户端密钥(如果有)。scope:客户端的范围受到限制。如果范围未定义或为空(默认值),则客户端不受范围的限制。authorizedGrantTypes:授权客户使用的授权类型。默认值为空。authorities:授予客户端的权限(常规的Spring Security权限)。

可以通过直接访问底层存储(例如的情况下为数据库表JdbcClientDetailsService)或通过ClientDetailsManager接口(这两种实现都ClientDetailsService可以实现)来更新正在运行的应用程序中的客户端详细信息。

内存加载客户端配置,直接通过ClientDetailsServiceConfigurer添加客户端配置@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("admin")//配置client_id .secret(passwordEncoder.encode("admin123456"))//配置client_secret .accessTokenValiditySeconds(3600)//配置访问token的有效期 .refreshTokenValiditySeconds(864000)//配置刷新token的有效期 .redirectUris("http://www.baidu.com")//配置redirect_uri,用于授权成功后跳转 .scopes("all")//配置申请的权限范围 .authorizedGrantTypes("authorization_code","password","client_credentials","refresh_token");//配置grant_type,表示授权类型 } 自定义ClientDetailsService,redis+jdbc方式加载客户端缓存 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(redisClientDetailsService); redisClientDetailsService.loadAllClientToCache();// } @Service public class RedisClientDetailsService extends JdbcClientDetailsService { //继承JdbcClientDetailsService,扩展redis缓存加载客户端,优先从缓存获取客户端配置,缓存没有再从数据库加载 2.2.1.3 令牌管理策略

AuthorizationServerTokenServices定义了管理OAuth 2.0令牌所需的操作。在开发过程需要注意:

创建访问令牌后,必须存储身份验证,以便接受访问令牌的资源以后可以引用它。访问令牌用于加载用于授权其创建的身份验证。

在创建AuthorizationServerTokenServices实现时,您可能需要考虑使用DefaultTokenServices,可以使用插入许多策略来更改访问令牌的格式和存储。默认情况下,它会通过随机值创建令牌,并处理所有其他事务(除了将令牌委派给的令牌的持久性)TokenStore。默认存储是内存中的实现。

InMemoryTokenStore对于单个服务器,默认设置非常合适(例如,低流量,并且在发生故障的情况下不与备份服务器进行热交换)。大多数项目都可以从此处开始,并且可以在开发模式下以这种方式运行,以轻松启动没有依赖性的服务器。JdbcTokenStore是JDBC版本的同样的事情,它存储在关系数据库中令牌数据。如果可以在服务器之间共享数据库,请使用JDBC版本;如果只有一个,则可以扩展同一服务器的实例;如果有多个组件,则可以使用Authorization and Resources Server。要使用,JdbcTokenStore您需要在类路径上使用“ spring-jdbc”。存储的JSON Web令牌(JWT) 版本将有关授权的所有数据编码到令牌本身中(因此根本没有后端存储,这是一个很大的优势)。一个缺点是您不能轻易地撤销访问令牌,因此通常授予它们的期限很短,并且撤销是在刷新令牌处进行的。另一个缺点是,如果您在令牌中存储了大量用户凭证信息,则令牌会变得很大。JwtTokenStore是不是一个真正的“存储”在这个意义上,它不坚持任何数据,但它起着翻译令牌值和认证信息相同的角色DefaultTokenServices。2.2.1.4 自定义定义UserService实现UserDetailsService@Component public class UserService implements UserDetailsService { @Autowired private QtAdminService qtAdminService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String clientId = "admin"; UserDto userDto = qtAdminService.loadUserByUsername(username); if (userDto == null) { throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); } userDto.setClientId(clientId); SecurityUser securityUser = new SecurityUser(userDto); if (!securityUser.isEnabled()) { throw new DisabledException(MessageConstant.ACCOUNT_DISABLED); } else if (!securityUser.isAccountNonLocked()) { throw new LockedException(MessageConstant.ACCOUNT_LOCKED); } else if (!securityUser.isAccountNonExpired()) { throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED); } else if (!securityUser.isCredentialsNonExpired()) { throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED); } return securityUser; } } 2.2.1.5 定义令牌端点上的安全约束

在对请求授权的端点进行访问之前需要对授权信息中传递的客户端信息进行认证,客户端认证通过后才会访问授权端点。根据授权参数传递方式不同,对客户端进行认证的Filter也可能不一样:

请求/oauth/token的,如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter请求/oauth/token的,如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走BasicAuthenticationFilter认证

可以AuthorizationServerSecurityConfigurer添加客户端信息验证策略

@Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()") .addTokenEndpointAuthenticationFilter(customBasicAuthenticationFilter);//添加自定义客户端验证策略 } //客户端验证策略控制 public void configure(HttpSecurity http) throws Exception { this.frameworkEndpointHandlerMapping(); if (this.allowFormAuthenticationForClients) { this.clientCredentialsTokenEndpointFilter(http); } Iterator var2 = this.tokenEndpointAuthenticationFilters.iterator(); while(var2.hasNext()) { Filter filter = (Filter)var2.next(); http.addFilterBefore(filter, BasicAuthenticationFilter.class); } http.exceptionHandling().accessDeniedHandler(this.accessDeniedHandler); } private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) { ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(this.frameworkEndpointHandlerMapping().getServletPath("/oauth/token")); clientCredentialsTokenEndpointFilter.setAuthenticationManager((AuthenticationManager)http.getSharedObject(AuthenticationManager.class)); OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint(); authenticationEntryPoint.setTypeName("Form"); authenticationEntryPoint.setRealmName(this.realm); clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint); clientCredentialsTokenEndpointFilter = (ClientCredentialsTokenEndpointFilter)this.postProcess(clientCredentialsTokenEndpointFilter); http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class); return clientCredentialsTokenEndpointFilter; } private ClientDetailsService clientDetailsService() { return (ClientDetailsService)((HttpSecurity)this.getBuilder()).getSharedObject(ClientDetailsService.class); } private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping() { return (FrameworkEndpointHandlerMapping)((HttpSecurity)this.getBuilder()).getSharedObject(FrameworkEndpointHandlerMapping.class); } public void addTokenEndpointAuthenticationFilter(Filter filter) { this.tokenEndpointAuthenticationFilters.add(filter); } 2.2.2 添加SpringSecurity配置

允许认证相关路径的访问及表单登录

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override public void configure(HttpSecurity http) throws Exception { http.csrf() .disable() .authorizeRequests() .antMatchers("/oauth/**", "/login/**", "/logout/**") .permitAll() .anyRequest() .authenticated() .and() .formLogin() .permitAll(); } } 2.2.3 Oauth2 验证

启动应用,进行Oauth2 认证服务进行验证 Oauth2 密码模式验证

使用密码请求该地址获取访问令牌:http://localhost:10001/oauth/token使用Basic认证通过client_id和client_secret构造一个Authorization头信息; 在body中添加以下参数信息,通过POST请求获取访问令牌; { "access_token": "a690d4e6-185f-4d1d-bc62-0067bd8b6ec9", "token_type": "bearer", "refresh_token": "55a04005-e2d9-44df-99df-01b57429d424", "expires_in": 3599, "scope": "all" } 2.3、Spring Security oauth2 授权认证核心源码分析

OAuth2 授权认证大致可以分为两步:

客户端认证Filter拦截/oauth/token请求,对授权参数传递的client_id和client_secret进行认证,认证通过继续访问/oauth/token端点;/oauth/token端点进行授权认证。2.3.1 /oauth/token 认证核心处理流程图
2.3.2 TokenEndpoint(/oauth/token) 认证源码分析@RequestMapping( value = {"/oauth/token"}, method = {RequestMethod.POST} ) public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { if (!(principal instanceof Authentication)) { throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter."); } else { //1. 获取clientId String clientId = this.getClientId(principal); //2. 根据客户端id加载客户端信息 ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId); //3. 根据客户端信息和请求参数组装TokenRequest TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); //4. 有没有传clientId验证 if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) { throw new InvalidClientException("Given client ID does not match authenticated client"); } else { if (authenticatedClient != null) { //5. 授权范围scope校验 this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient); } //6. grant_type是否存在值,对应四种授权模式和刷新token if (!StringUtils.hasText(tokenRequest.getGrantType())) { throw new InvalidRequestException("Missing grant type"); //是否简化模式 } else if (tokenRequest.getGrantType().equals("implicit")) { throw new InvalidGrantException("Implicit grant type not supported from token endpoint"); } else { //是否是授权码模式 if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) { this.logger.debug("Clearing scope of incoming token request"); tokenRequest.setScope(Collections.emptySet()); } //是否刷新令牌 if (this.isRefreshTokenRequest(parameters)) { tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope"))); } //7. 授权控制,并返回AccessToken OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); if (token == null) { throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType()); } else { return this.getResponse(token); } } } } } 2.4 资源服务器2.4.1 资源服务器配置

资源服务器(可以与授权服务器或单独的应用程序相同)提供受OAuth2令牌保护的资源。Spring OAuth提供了实现此保护的Spring Security身份验证过滤器。您可以@EnableResourceServer在@Configuration类上将其打开,并使用进行配置(根据需要)ResourceServerConfigurer。可以配置以下功能:

tokenServices:定义令牌服务(的实例ResourceServerTokenServices)的bean 。resourceId:资源的ID(可选,但建议使用,并且将由auth服务器验证(如果存在))。资源服务器的其他扩展点(例如,tokenExtractor用于从传入请求中提取令牌)请求受保护资源的匹配器(默认为全部)受保护资源的访问规则(默认为普通的“已认证”)HttpSecuritySpring Security中配置程序允许的受保护资源的其他自定义

该@EnableResourceServer注释添加类型的过滤器OAuth2AuthenticationProcessingFilter 自动Spring Security的过滤器链。 代码清单:

@Configuration @EnableResourceServer public class Oauth2SourceConfig { //配置资源url保护策略 @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .authenticated() .and() .requestMatchers() .antMatchers("/user/**");//配置需要保护的资源路径 } //自定义资源保护令牌策略 public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(tokenStore); } } 2.4.2 使用令牌获取受保护资源
2.4.3 源码分析2.4.3.1 OAuth2AuthenticationProcessingFilter

资源服务认证入口Filter

public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean { //省略...... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { boolean debug = logger.isDebugEnabled(); HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; try { //1. 从BearerTokenExtractor 获取Authentication 信息 Authentication authentication = this.tokenExtractor.extract(request); if (authentication == null) { if (this.stateless && this.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(this.authenticationDetailsSource.buildDetails(request)); } //2. OAuth2AuthenticationManager 进行token认证 Authentication authResult = this.authenticationManager.authenticate(authentication); if (debug) { logger.debug("Authentication success: " + authResult); } //3. 将认证结果放置SecurityContextHolder上下文 this.eventPublisher.publishAuthenticationSuccess(authResult); SecurityContextHolder.getContext().setAuthentication(authResult); } } catch (OAuth2Exception var9) { SecurityContextHolder.clearContext(); if (debug) { logger.debug("Authentication request failed: " + var9); } this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A")); this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9)); return; } chain.doFilter(request, response); } //省略...... } 2.4.3.2 BearerTokenExtractor

从请求 Header中获取token

protected String extractHeaderToken(HttpServletRequest request) { Enumeration headers = request.getHeaders("Authorization"); String value; do { if (!headers.hasMoreElements()) { return null; } value = (String)headers.nextElement(); } while(!value.toLowerCase().startsWith("Bearer".toLowerCase())); String authHeaderValue = value.substring("Bearer".length()).trim(); request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim()); int commaIndex = authHeaderValue.indexOf(44); if (commaIndex > 0) { authHeaderValue = authHeaderValue.substring(0, commaIndex); } return authHeaderValue; } 2.4.3.3 OAuth2AuthenticationManager

资源服务认证token校验实现 程序片段:

public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (authentication == null) { throw new InvalidTokenException("Invalid token (token not found)"); } else { String token = (String)authentication.getPrincipal(); //1. 从验证token存储介质获取请求传递的Access Token获取对应的验证信息 OAuth2Authentication auth = this.tokenServices.loadAuthentication(token); if (auth == null) { throw new InvalidTokenException("Invalid token: " + token); } else { //2. 验证token并加载验证信息 Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds(); if (this.resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(this.resourceId)) { throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + this.resourceId + ")"); } else { this.checkClientDetails(auth); if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails(); if (!details.equals(auth.getDetails())) { details.setDecodedDetails(auth.getDetails()); } } auth.setDetails(authentication.getDetails()); auth.setAuthenticated(true); return auth; } } } } 3、OAuth2 扩展3.1 自定义异常处理3.1.1 自定义授权端点处理异常

授权服务器中的错误处理使用标准的Spring MVC功能,即@ExceptionHandler端点本身中的方法。但是其原生的异常信息可能与我们实际使用的异常处理不一致,需要进行转义。可以自定义WebResponseExceptionTranslator,向授权端点提供异常处理,这是更改响应异常处理的最佳方法。

//省略 @Autowired private WebResponseExceptionTranslator webResponseExceptionTranslator; /** * 自定义授权服务配置 * 使用密码模式需要配置 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints.authenticationManager(authenticationManager) .userDetailsService(userService) .exceptionTranslator(webResponseExceptionTranslator);// } //省略 /** * 实现WebResponseExceptionTranslator接口,自定义授权端点异常处理 */ @Component public class CustomOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator { private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); @Override public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception { Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(e); Exception ase = (OAuth2Exception)this.throwableAnalyzer.getFirstThrowableOfType( OAuth2Exception.class, causeChain); if (ase != null) { return this.handleOAuth2Exception((OAuth2Exception)ase); } ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType( AuthenticationException.class, causeChain); if (ase != null) { return this.handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e)); } ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); if (ase instanceof AccessDeniedException) { return this.handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase)); } ase = (HttpRequestMethodNotSupportedException)this.throwableAnalyzer.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain); if(ase instanceof HttpRequestMethodNotSupportedException){ return this.handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase)); } return this.handleOAuth2Exception(new UnsupportedResponseTypeException("服务内部错误", 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", "Bearer", e.getSummary())); } CustomOauthException exception = new CustomOauthException(e.getMessage(),e); ResponseEntity<OAuth2Exception> response = new ResponseEntity(exception, headers, HttpStatus.valueOf(status)); return response; } //省略 3.1.2 自定义匿名用户访问无权限资源时的异常

当访问未纳入Oauth2保护资源或者访问授权端点时客户端验证失败,抛出异常,AuthenticationEntryPoint. Commence(..)就会被调用。这个对应的代码在ExceptionTranslationFilter中,当ExceptionTranslationFilter catch到异常后,就会间接调用AuthenticationEntryPoint。默认使用LoginUrlAuthenticationEntryPoint处理异常,当抛出依次LoginUrlAuthenticationEntryPoint会将异常呈现给授权服务器默认的Login视图。

访问未纳入Oauth2资源管理的接口 当访问未纳入Oauth2资源管理的接口时,因为应用接入安全框架,因此依旧会进行权限验证,当用户无权访问时会有ExceptionTranslationFilter 拦截异常并将异常呈现到默认的登录视图提示用户登录: 调用授权端点,客户端校验失败 当调用授权端点(/oauth/token)时,根据前面的源码我们知道在授权认证前,会先通过客户端验证Filter进行客户端验证,当客户端验证失败会抛出异常并由ExceptionTranslationFilter 拦截,将异常呈现给默认的登录视图:

源码分析:

//顶层授权认证异常处理Point package org.springframework.security.web; import ... public interface AuthenticationEntryPoint { void commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3) throws IOException, ServletException; }

当ExceptionTranslationFilter catch到异常后,就会间接调用AuthenticationEntryPoint。

package org.springframework.security.web.access; import ... public class ExceptionTranslationFilter extends GenericFilterBean { //省略...... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; try { chain.doFilter(request, response); this.logger.debug("Chain processed normally"); } catch (IOException var9) { throw var9; } catch (Exception var10) { Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10); RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); } if (ase == null) { if (var10 instanceof ServletException) { throw (ServletException)var10; } if (var10 instanceof RuntimeException) { throw (RuntimeException)var10; } throw new RuntimeException(var10); } if (response.isCommitted()) { throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10); } //异常处理,间接调用AuthenticationEntryPoint.commence this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase); } } //省略...... private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { this.logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception); this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) { this.logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception); this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); } else { this.logger.debug("Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception); this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource"))); } } } ////异常处理,间接调用AuthenticationEntryPoint.commence protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { SecurityContextHolder.getContext().setAuthentication((Authentication)null); this.requestCache.saveRequest(request, response); this.logger.debug("Calling Authentication entry point."); this.authenticationEntryPoint.commence(request, response, reason); } //省略...... //默认的异常处理,会将异常呈现给默认的Login视图 package org.springframework.security.web.authentication; import ... public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { //省略... public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String redirectUrl = null; if (this.useForward) { if (this.forceHttps && "http".equals(request.getScheme())) { redirectUrl = this.buildHttpsRedirectUrlForRequest(request); } if (redirectUrl == null) { String loginForm = this.determineUrlToUseForThisRequest(request, response, authException); if (logger.isDebugEnabled()) { logger.debug("Server side forward to: " + loginForm); } RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm); dispatcher.forward(request, response); return; } } else { redirectUrl = this.buildRedirectUrlToLoginPage(request, response, authException); } this.redirectStrategy.sendRedirect(request, response, redirectUrl); } //省略

默认的视图呈现异常肯定不符合我们实际的应用,因此需要对此类异常进行自定义处理。

package com.easy.mall.exception; import ... @Component @AllArgsConstructor public class CustomAuthExceptionEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); CommonResult<String> result = CommonResult.failed(); result.setCode(HttpStatus.HTTP_UNAUTHORIZED); if (e != null) { result.setMessage("unauthorized"); result.setData(e.getMessage()); } response.setStatus(HttpStatus.HTTP_UNAUTHORIZED); PrintWriter printWriter = response.getWriter(); printWriter.append(JSONObject.toJSONString(result)); } } @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override public void configure(HttpSecurity http) throws Exception { http.csrf() .disable() .authorizeRequests() .antMatchers("/oauth/**", "/login/**", "/logout/**") .permitAll() .anyRequest() .authenticated() .and() .formLogin() .permitAll(); //web 安全控制添加注册自定义的错误处理 http.exceptionHandling().authenticationEntryPoint(new CustomAuthExceptionEntryPoint()); } } 自定义异常处理后的效果
3.1.3 自定义受OAuth2令牌保护的资源认证失败异常

受OAuth2令牌保护的资源无权限访问异常时,异常由原生的Oauth2authenticationentrypoint处理,但是其原生的异常信息可能与我们实际使用的异常处理不一致,需要进行转义。

原生的异常信息响应:{ "error": "invalid_token", "error_description": "Invalid access token: 1" } 自定义异常@Configuration @EnableResourceServer public class Oauth2SourceConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .authenticated() .and() .requestMatchers() .antMatchers("/user/**");//配置需要保护的资源路径 } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.authenticationEntryPoint(new CustomAuthExceptionEntryPoint());//自定义受令牌保护资源服务异常处理 } } 自定义异常处理效果 { "code": 401, "data": "Invalid access token: 1", "message": "unauthorized" } 3.1.4 自定义受OAuth2令牌保护的资源无权限访问异常

//待定

3.1.5 Security自定义异常分析总结

根据上述一系列源码分析,我们知道Security是通过一系列Filter过滤链实现授权认证,不同情况和场景其过滤链不一样,因此当出现异常也通常由不同的异常处理器进行处理,因此需要针对不同情况进行自定义处理。

附录构造Basic Auth认证头 /** * 构造Basic Auth认证头信息 * * @return */ private String getHeader() { String auth = APP_KEY + ":" + SECRET_KEY; byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(Charset.forName("US-ASCII"))); String authHeader = "Basic " + new String(encodedAuth); return authHeader; }


以上就是关于《SpringSecurity(二)OAuth2认证详解-spring-security-oauth2-authorization-server》的全部内容,本文网址:https://www.7ca.cn/baike/20092.shtml,如对您有帮助可以分享给好友,谢谢。
标签:
声明

排行榜