Spring-Security(二)OAuth2认证详解(持续更新)

Spring Security & Oauth2系列:
 
Spring Security(一) 源码分析及认证流程
Spring Security(二)OAuth2认证详解及自定义异常处理

文章目录

  • 1、OAuth2.0 简介
    • 1.1 OAuth2.0 相关名词解释
    • 1.2 四种授权模式
  • 1.3 、OAuth2框架
    • 1.4 OAuth 2.0客户端提供功能
  • 2、OAuth 2.0 认证服务
    • 2.1 Spring Security OAuth2 提供的程序实现
      • 2.1.1 授权服务
      • 2.1.2 资源服务
    • 2.2 集成 OAuth 2.0 认证授权及资源管理
      • 2.2.1 项目准备
      • 2.2.1 配置授权服务
        • 2.2.1.1 授权服务配置
        • 2.2.1.2 客户端加载策略配置
        • 2.2.1.3 令牌管理策略
        • 2.2.1.4 自定义定义UserService实现UserDetailsService
        • 2.2.1.5 定义令牌端点上的安全约束
      • 2.2.2 添加SpringSecurity配置
      • 2.2.3 Oauth2 验证
    • 2.3、Spring Security oauth2 授权认证核心源码分析
      • 2.3.1 /oauth/token 认证核心处理流程图
      • 2.3.2 TokenEndpoint(/oauth/token) 认证源码分析
    • 2.4 资源服务器
      • 2.4.1 资源服务器配置
      • 2.4.2 使用令牌获取受保护资源
      • 2.4.3 源码分析
        • 2.4.3.1 OAuth2AuthenticationProcessingFilter
        • 2.4.3.2 BearerTokenExtractor
        • 2.4.3.3 OAuth2AuthenticationManager
  • 3、OAuth2 扩展
    • 3.1 自定义异常处理
      • 3.1.1 自定义授权端点处理异常
      • 3.1.2 自定义匿名用户访问无权限资源时的异常
      • 3.1.3 自定义受OAuth2令牌保护的资源认证失败异常
      • 3.1.4 密码认证自定义异常信息
        • 思路
      • 3.1.5 Security自定义异常分析总结
  • 附录

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 {@Overrideprotected 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 {@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate UserService userService;/*** 自定义授权服务配置* 使用密码模式需要配置*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager).userDetailsService(userService);}/*** 配置认证客户端* @param clients* @throws Exception*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {//自定义客户端配置}/*** 自定义授权令牌端点的安全约束* @param security* @throws Exception*/@Overridepublic 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添加客户端配置
@Overridepublic 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方式加载客户端缓存
    @Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(redisClientDetailsService);redisClientDetailsService.loadAllClientToCache();//}@Servicepublic 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 {@Autowiredprivate QtAdminService qtAdminService;@Overridepublic 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..else了,可以使用枚举进行优化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添加客户端信息验证策略

@Overridepublic 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 {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overridepublic 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. 获取clientIdString clientId = this.getClientId(principal);//2. 根据客户端id加载客户端信息ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);//3. 根据客户端信息和请求参数组装TokenRequestTokenRequest 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是否存在值,对应四种授权模式和刷新tokenif (!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. 授权控制,并返回AccessTokenOAuth2AccessToken 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保护策略@Overridepublic 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,想授权端点提供异常处理,这是更改响应异常处理的最佳方法。

//省略
@Autowiredprivate WebResponseExceptionTranslator webResponseExceptionTranslator;/*** 自定义授权服务配置* 使用密码模式需要配置*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager).userDetailsService(userService).exceptionTranslator(webResponseExceptionTranslator);//}
//省略/*** 实现WebResponseExceptionTranslator接口,自定义授权端点异常处理*/
@Component
public class CustomOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator {private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();@Overridepublic 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.commencethis.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.commenceprotected 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 {@Overridepublic 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 {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overridepublic 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());}
}
  • 自定义异常处理后的效果

客户端验证失败

访问未纳入OAuth2受保护资源接口

3.1.3 自定义受OAuth2令牌保护的资源认证失败异常

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

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

3.1.4 密码认证自定义异常信息

思路

通过上一章源码分析知道,Oauth2.0账号密码认证交由AuthenticationManager进行处理,其认证链路AuthenticationManager->ProviderManager->AuthenticationProvider->AbstractUserDetailsAuthenticationProvider->DaoAuthenticationProvider,其中AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider类是密码认证得实际处理类,当密码认证异常时,其返回得异常信息并不满足我们实际业务需求。需要根据业务需求进行重构。重构代码主要是重新实现:

  • AbstractUserDetailsAuthenticationProvider.authenticate
  • DaoAuthenticationProvider.additionalAuthenticationChecks

代码清单:

  1. CustomAuthenticationProvider 继承DaoAuthenticationProvider,重写密码认证逻辑
@Slf4j
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {private UserDetailsChecker preAuthenticationChecks = new CustomAuthenticationProvider.DefaultPreAuthenticationChecks();private UserDetailsChecker postAuthenticationChecks = new CustomAuthenticationProvider.DefaultPostAuthenticationChecks();private boolean forcePrincipalAsString = false;public CustomAuthenticationProvider() {}private UserCache userCache = new NullUserCache();/*** 重新密码校验实现-自定义异常处理* @param userDetails* @param authentication* @throws AuthenticationException*/@Overrideprotected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {if (authentication.getCredentials() == null) {this.logger.debug("Authentication failed: no credentials provided");throw new BadCredentialsException(Resources.getMessage("STATUSCODE_21004","can not get credentials."));} else {String presentedPassword = authentication.getCredentials().toString();PasswordEncoder passwordEncoder = SpringContextUtil.getBean("bCryptPasswordEncoder");if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {this.logger.debug("Authentication failed: password does not match stored value");throw new BadCredentialsException(Resources.getMessage("STATUSCODE_21001","username or password error."));}}}/*** 重新认证核心方法-自定义返回异常* @param authentication* @return* @throws AuthenticationException*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {return this.messages.getMessage("CustomAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");});String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {cacheWasUsed = false;try {user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);} catch (UsernameNotFoundException var6) {this.logger.debug("User '" + username + "' not found");if (this.hideUserNotFoundExceptions) {throw new BadCredentialsException(Resources.getMessage("STATUSCODE_21000","account not exist!"));}throw var6;}Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");}try {this.preAuthenticationChecks.check(user);this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);} catch (AuthenticationException var7) {if (!cacheWasUsed) {throw var7;}cacheWasUsed = false;user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);this.preAuthenticationChecks.check(user);this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);}this.postAuthenticationChecks.check(user);if (!cacheWasUsed) {this.userCache.putUserInCache(user);}Object principalToReturn = user;if (this.forcePrincipalAsString) {principalToReturn = user.getUsername();}return this.createSuccessAuthentication(principalToReturn, authentication, user);}private class DefaultPostAuthenticationChecks implements UserDetailsChecker {private DefaultPostAuthenticationChecks() {}public void check(UserDetails user) {if (!user.isCredentialsNonExpired()) {log.debug("User account credentials have expired");throw new CredentialsExpiredException(Resources.getMessage("STATUSCODE_21005","user credentials have expired."));}}}private class DefaultPreAuthenticationChecks implements UserDetailsChecker {private DefaultPreAuthenticationChecks() {}public void check(UserDetails user) {if (!user.isAccountNonLocked()) {CustomAuthenticationProvider.this.logger.debug("User account is locked");throw new LockedException(Resources.getMessage("STATUSCODE_21006","user account is locked."));} else if (!user.isEnabled()) {CustomAuthenticationProvider.this.logger.debug("User account is disabled");throw new DisabledException(Resources.getMessage("STATUSCODE_21002","user is disabled."));} else if (!user.isAccountNonExpired()) {CustomAuthenticationProvider.this.logger.debug("User account is expired");throw new AccountExpiredException(Resources.getMessage("STATUSCODE_21008","user account has expired."));}}}}

3.1.5 Security自定义异常分析总结

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

附录

  • 1 构造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;}
    1. 修改WEB安全配置,引入自定义密码认证处理器
@Configuration
@EnableWebSecurity
@Import({CustomAuthenticationEntryPoint.class, CustomAccessDeniedHandler.class})
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {//省略/*** 定义自定义密码认证处理器* @return*/@BeanCustomAuthenticationProvider customAuthenticationProvider() {CustomAuthenticationProvider customAuthenticationProvider = new CustomAuthenticationProvider();customAuthenticationProvider.setUserDetailsService(userDetailsService());return customAuthenticationProvider;}/*** 将自定义密码认证处理器注册到AuthenticationManager* @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {ProviderManager manager = new ProviderManager(Arrays.asList(customAuthenticationProvider()));return manager;}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/23761.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大型零售企业总部到分公司数据发放,有没有更优化的方案?

大型零售企业在市场经济中扮演重要角色&#xff0c;是保证基础商品生产、流通和供给的重要一环。随着企业发展&#xff0c;很多大型零售企业都会在全国、乃至全球各地开设分公司&#xff0c;用以降低生产和运营成本&#xff0c;更好地提供本地化服务。 为了保证总部与分公司间信…

Linux环境---在线安装jdk

Linux环境—在线安装jdk 一、使用步骤 1.安装环境 JDK版本&#xff1a;1.8 1.1 建立存放软件的目录 注意&#xff1a;此处本人是将需要按照的软件存放在directory目录下&#xff0c;可根据实际情况调整接收路径。 命令如下&#xff1a; mkdir directory2.安装jdk 2.1 建…

解决Spark流处理产生的小文件问题

做流批一体&#xff0c;湖仓一体的大数据架构&#xff0c;常见的做法就是&#xff1a; 数据源->spark Streaming->ODS&#xff08;数据湖&#xff09;->spark streaming->DWD&#xff08;数据湖&#xff09;->... 那么数据源->spark Streaming->ODS&…

【Selenium+java环境配置】(超详细教程常见问题解决)

Seleniumjava环境配置 windows电脑环境搭建-chrome浏览器1. 下载chrome浏览器2. 查看chrome浏览器版本3. 下载chrome浏览器驱动4.配置系统环境变量PATH 验证环境是否搭建成功1. 创建java项目&#xff0c;添加pom文件中添加依赖2. 编写代码运行 常见问题&解决办法1.访问失败…

移动端 UI 风格,魅力无限

移动端 UI 风格&#xff0c;打造极致体验

Django分页

1、在视图函数文件中引入‘分页器’ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 2、给原来的罗列信息函数&#xff0c;添加分页功能&#xff0c;即按照页码&#xff0c;只返回部分信息。 login_required def article_list(request):article…

【因果推断python】21_匹配2

目录 匹配估计器 匹配估计器 子分类估计器在实践中用得不多&#xff08;我们很快就会明白为什么&#xff0c;主要是因为维度诅咒这个原因&#xff09;&#xff0c;但它让我们很好地、直观地了解了因果推理估计器应该做什么&#xff0c;以及它应该如何控制混淆因素。这使我们能…

Yuan 2.0-M32 是一个基于 Yuan 2.0 架构的双语混合专家 (MoE) 语言模型,旨在以更少的参数和计算量实现更高的准确率

主要创新点&#xff1a; 注意力路由器 (Attention Router): 提出了一种新的路由器网络&#xff0c;考虑了专家之间的相关性&#xff0c;从而提高了模型的准确率。高效计算&#xff1a; 使用 MoE 架构&#xff0c;40B 总参数中仅有 3.7B 激活参数&#xff0c;训练计算消耗仅为同…

大模型创新企业集结!百度智能云千帆AI加速器Demo Day启动

新一轮技术革命风暴席卷而来&#xff0c;为创业带来源源不断的创新动力。过去一年&#xff0c;在金融、制造、交通、政务等领域&#xff0c;大模型正从理论到落地应用&#xff0c;逐步改变着行业的运作模式&#xff0c;成为推动行业创新和转型的关键力量。 针对生态伙伴、创业…

IDEA破解后的配置

以下所有操作都要求进入全局setting而不是某一个项目的setting 进入全局Setting File→close project 进入欢迎页面 低版本 然后点击Setting 关闭自动更新 不关闭有可能会破解失败 Appearance & Behavior->System Settings->Updates下取消Automatically chec…

debian系统apt 国内安装源

debian系统apt 国内安装源&#xff1a; 国内阿里镜像源&#xff1a; deb http://mirrors.aliyun.com/debian stable main non-free contrib deb-src http://mirrors.aliyun.com/debian stable main non-free contrib 打开源文件位置&#xff1a;/etc/apt/sources.list,原来的内…

eNSP学习——RIP路由协议的汇总

目录 主要命令 原理概述 实验目的 实验内容 实验拓扑 实验编址 实验步骤 1、基本配置 2、配置RIPv1协议 3、配置RIPv2自动汇总 4、配置RIPv2手动汇总 需要eNSP各种配置命令的点击链接自取&#xff1a;华为&#xff45;NSP各种设备配置命令大全PDF版_ensp配置命令大全…

蓝桥杯物联网竞赛 比赛总结

CUBEMX配置建议&#xff1a; 对于CUBEMX配置来说stm32l071kbu6的引脚不算太多&#xff0c;功能模块相对的也不多&#xff0c;所以我建议直接熟练到能将所有模块烂熟于心&#xff0c;不用看原理图就能熟练配置下来&#xff0c;因为国赛看原理图去配置太花费时间 我建议学习的时…

小程序 UI 风格,赏心悦目

小程序 UI 风格&#xff0c;赏心悦目

【云原生】Kubernetes----RBAC用户资源权限

目录 引言 一、Kubernetes安全机制概述 二、认证机制 &#xff08;一&#xff09;认证方式 1.HTTPS证书认证 1.1 证书颁发 1.2 config文件 1.3 认证类型 1.4 Service Account 1.4.1 作用 1.4.2 包含内容 1.4.3 与Secret的关系 2.Bearer Tokens 3.基本认证 三、鉴…

Java Web学习笔记17——Vue快速入门

什么是Vue&#xff1f; Vue是一套前端框架&#xff0c;免除原生JavaScript中的DOM操作&#xff0c;简化书写。 基于MVVM&#xff08;Model-View-ViewModel&#xff09;思想&#xff0c;实现数据的双向绑定&#xff0c;将编程的关注点放在数据上。 官网&#xff1a;https://v…

俯视角2D_玩家角色架构

玩家控制 玩家角色蓝图的精灵旋转和摄像机旋转角 1.因为是俯视角的游戏&#xff0c;因此相机和角色的精灵图需要调整为-90 ## 玩家输入 增强输入的映射 为玩家控制器引用增强输入的映射 在游戏模式中应用该玩家控制器 在玩家蓝图中应用输入映射并编写移动逻辑,(需要注意的是…

python-小游戏-弹球对决

python-小游戏-弹球对决 需要安装pygame 代码—game-Pong.py import pygame import random# Initialize pygame pygame.init()# Set up the screen WIDTH 600 HEIGHT 400 BALL_RADIUS 20 PAD_WIDTH 10 PAD_HEIGHT 80 WHITE (255, 255, 255) PURPLE (128, 0, 128) RED…

策略模式的理解和运用

在之前的小游戏项目中&#xff0c;处理websocket长连接请求的时候&#xff0c;需要根据传递数据包的不同类型&#xff0c;进行不同的处理。为了实现这个场景&#xff0c;比较简单的方法就是使用if-else或者switch-case语句&#xff0c;根据条件进行判断。但是这导致了项目代码复…

AI驱动下,需要重新审视比亚迪在电子制造领域的“新神话”?

自4月22日创下新低后&#xff0c;比亚迪电子&#xff08;00285.HK&#xff09;之后趋势走强&#xff0c;截至6月5日收盘&#xff0c;比亚迪电子股价一度突破年内最高价位37.35港元/股&#xff0c;最终收盘36.75港元/股。 区间29个交易日涨超55&#xff05;&#xff0c;远远优于…