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

news2024/11/13 10:38:33

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 {

    @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..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添加客户端信息验证策略

@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());
  }
}
  • 自定义异常处理后的效果

客户端验证失败

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

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.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
   */
  @Override
  protected 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
   */
  @Override
  public 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
     */
    @Bean
    CustomAuthenticationProvider customAuthenticationProvider() {
        CustomAuthenticationProvider customAuthenticationProvider = new CustomAuthenticationProvider();
        customAuthenticationProvider.setUserDetailsService(userDetailsService());
        return customAuthenticationProvider;
    }

    /**
     * 将自定义密码认证处理器注册到AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {

        ProviderManager manager = new ProviderManager(
            Arrays.asList(customAuthenticationProvider()));
        return manager;
    }

}

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

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

相关文章

QT 信号和槽 信号关联到信号示例 信号除了可以绑定槽以外,信号还可以绑定信号

信号除了可以关联到槽函数&#xff0c;还可以关联到类型匹配的信号&#xff0c;实现信号的接力触发。上个示例中因为 clicked 信号没有参数&#xff0c;而 SendMsg 信号有参数&#xff0c;所以不方便直接关联。本小节示范一个信号到信号的关联&#xff0c;将按钮的 clicked 信号…

Python 深度探讨 *args

点击下方卡片&#xff0c;关注“小白玩转Python”公众号 作为Python中最独特的语法之一&#xff0c;*args 在编程过程中给我们带来了很多灵活性和便利性。我认为它们反映了“Pythonic”和“Python之禅”。然而&#xff0c;我发现它们对于学习者&#xff08;尤其是初学者&#x…

DeepSpeed Learning Rate Scheduler

Learning Rate Range Test (LRRT) 训练试跑&#xff0c;该lr scheduler从小到大增长lr&#xff0c;同时记录下validatin loss&#xff1b;人来观察在训练多少step之后&#xff0c;loss崩掉&#xff08;diverge)了&#xff0c;进而为真正跑训练&#xff0c;挑选合适的lr区间&…

一、Electron 环境初步搭建

新建一个文件夹&#xff0c;然后进行 npm init -y 进行初始化&#xff0c;然后我们在进行 npm i electron --save-dev , 此时我们按照官网的教程进行一个初步的搭建&#xff0c; 1.在 package.json 文件进行修改 {"name": "electron-ui","version…

嵌入式应用之FIFO模块原理与实现

FIFO介绍与原理 FIFO是First-In First-Out的缩写&#xff0c;它是一个具有先入先出特点的缓冲区。FIFO在嵌入式应用的非常广泛&#xff0c;可以说有数据收发的地方&#xff0c;基本就有FIFO的存在。或者为了降低CPU负担&#xff0c;提高数据处理效率&#xff0c;可以在积累到一…

使用 Scapy 库编写 TCP FIN 洪水攻击脚本

一、介绍 TCP FIN洪水攻击是一种分布式拒绝服务攻击&#xff08;DDoS&#xff09;&#xff0c;攻击者通过向目标服务器发送大量伪造的TCP FIN&#xff08;终止&#xff09;数据包&#xff0c;使目标服务器不堪重负&#xff0c;无法正常处理合法请求。FIN包通常用于关闭一个TCP…

电路笔记 : 嘉立创EDA 导入、查找、设计管理器(快速寻找网络标签)功能+DRC错误检查和处理

导入功能 查找功能 可查找多种类型&#xff0c;如原件名称、网络标签等 设计管理器 图层查看 DRC错误 规则设置 线距问题 大多数PCB制造商能够可靠地生产5 mil间距的走线和间隙。这是一个常见的标准&#xff0c;适合大多数消费级和工业级电子产品。在5 mil以上的间距&#xff…

操作系统复习-存储管理之虚拟内存

虚拟内存概述 有些进程实际需要的内存很大&#xff0c;超过物理内存的容量。多道程序设计&#xff0c;使得每个进程可用物理内存更加稀缺。不可能无限增加物理内存&#xff0c;物理内存总有不够的时候。虚拟内存是操作系统内存管理的关键技术。使得多道程序运行和大程序运行称…

Collections工具类及其案例

package exercise;public class Demo1 {public static void main(String[] args) {//可变参数//方法形参的个数是可以发生变化的//格式&#xff1a;属性类型...名字//int...argsint sum getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);System.out.println(sum);}//底层&#xff1a;可…

6.9总结

Vue生命周期 生命周期&#xff1a;指一个对象从创建到销毁的整个过程生命周期的八个阶段&#xff1a;每触发一个生命周期事件&#xff0c;会自动执行一个生命周期的方法&#xff08;钩子&#xff09; mounted&#xff1a;挂载完成&#xff0c;Vue初始化成功&#xff0c;HTML渲…

简记:为Docker配置服务代理

简记 为Docker配置服务代理 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_28550263/art…

设置路径别名

一、描述 如果想要给路径设置为别名&#xff0c;就是常见的有些项目前面的引入文件通过开头的&#xff0c;也就是替换了一些固定的文件路径&#xff0c;怎么配置。 二、配置 import { defineConfig } from vite import react from vitejs/plugin-react import path from path…

GitHub工程获取第三方PR操作

GitHub工程获取第三方PR操作 1. 源由2. 获取第三方PRStep 1&#xff1a;安装ghStep 2&#xff1a;获取个人TokenStep 3&#xff1a;通过git协议获取代码Step 4&#xff1a;获取第三方PR分支 3. 总结 1. 源由 通常来说&#xff0c;GitHub上通常有三种场景&#xff1a; 工程管理…

MySQLWorkbench导出sql文件

MySQLWorkbench导出sql文件 前言效果图导出操作选择要导出的数据库遇到的问题解决问题查看mysql路径前言 在完成数据库搭建之后,需要为上线做准备,那么就需要导出数据库的建库sql了 本篇文章讲解的是mysql Workbench 导出数据建库脚本 效果图 导出操作 选择要导出的数据库…

什么是智慧零售?智慧零售的发展前景如何?

在零售业的快速发展中&#xff0c;市场竞争日益激烈&#xff0c;产品同质化严重&#xff0c;线下销售与线上商店的竞争加剧&#xff0c;资金成本问题日益凸显。这些问题不仅限制了零售业的发展&#xff0c;也给消费者带来了诸多不便。然而&#xff0c;智慧零售的出现&#xff0…

Java | Leetcode Java题解之第135题分发糖果

题目&#xff1a; 题解&#xff1a; class Solution {public int candy(int[] ratings) {int n ratings.length;int ret 1;int inc 1, dec 0, pre 1;for (int i 1; i < n; i) {if (ratings[i] > ratings[i - 1]) {dec 0;pre ratings[i] ratings[i - 1] ? 1 : …

互联网应用主流框架整合之SpringMVC初始化及各组件工作原理

Spring MVC的初始化和流程 MVC理念的发展 SpringMVC是Spring提供给Web应用领域的框架设计&#xff0c;MVC分别是Model-View-Controller的缩写&#xff0c;它是一个设计理念&#xff0c;不仅仅存在于Java中&#xff0c;各类语言及开发均可用&#xff0c;其运转流程和各组件的应…

如何使用照相机

前言&#xff1a; ” 接上篇&#xff0c;https://t.zsxq.com/19UoFe33k&#xff0c;本文是整理的关于《美国纽约摄影学院 摄影教材》这本书&#xff0c;第一单元 - 第3课 - 如何使用照相机&#xff0c;课后习题及解答。“ 1、对于一架典型的单镜头反光照相机&#xff0c;取景时…

并查集-求有向图中是否有环

目录 一、问题描述 二、解题思路 初始化 遍历边并尝试合并 结果分析 三、代码实现 四、刷题链接 一、问题描述 二、解题思路 题目的目的就是检查有向图中是否存在环&#xff0c;这里提供两种判断方式&#xff1a; 1.使用并查集 2.使用拓扑排序 我们在这里先只给出并查…

【C语言】宏详解(下卷)

前言 紧接上卷&#xff0c;我们继续来了解宏。 宏替换的规则 1.在调用宏时&#xff0c;首先对参数进行检查&#xff0c;看看是否包含任何由#define定义的符号。如果是&#xff0c;它们首先被替换。 2.替换文本随后被插入到程序中原来文本的位置。对于宏&#xff0c;参数名被他…