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 {

    @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;
    }

}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023 hnust 湖科大 嵌入式 实验报告+代码及复习资料等

2023 hnust 湖科大 嵌入式 实验报告代码及复习资料等 目录 流水灯 1 8位数码管动态扫描 3 按键输入 5 温度与关照 7 看门狗 9 内容 报告 代码 下载链接 https://pan.baidu.com/s/1LIN8rm42yrukXliI3XyZ1g?pwd1111

Java高阶数据结构-----并查集(详解)

目录 &#x1f9d0;一.并查集的基本概念&实例&#xff1a; &#x1f92a;二.并查集代码&#xff1a; &#x1f602;三&#xff1a;并查集的一些习题&#xff1a; A.省份数量 B.等式方程的可满足性 &#x1f9d0;一.并查集的基本概念&实例&#xff1a; 并查集概念&…

vue操作蓝牙教程

项目背景 想在VUE中使用蓝牙功能&#xff0c;百度了好久也尝试了好多都没法实现。 概念讲价 如果要在浏览器中使用蓝牙&#xff0c;去搜索关键字【navigator.bluetooth】&#xff0c;搜索后发现这根本不是想要的结果。 解决方法 去搜索关键字【uniappbluetoothvue】&#x…

Web前端三大主流框架简介与优缺点对比分析

随着互联网的快速发展&#xff0c;Web前端开发技术不断进步&#xff0c;各种前端框架应运而生&#xff0c;极大地提高了开发效率和用户体验。在众多框架中&#xff0c;React、Vue.js 和 Angular 是目前最受欢迎的三大主流框架。本文将对它们进行详细介绍&#xff0c;并对它们的…

110.网络游戏逆向分析与漏洞攻防-装备系统数据分析-装备与技能描述信息的处理

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果&#xff0c;代码看不懂是正常的&#xff0c;只要会抄就行&#xff0c;抄着抄着就能懂了 内容…

网络数据库后端相关面试题(其三)

18&#xff0c; 传输控制协议tcp和用户数据报协议udp有哪些区别 第一&#xff0c;tcp是面向字节流的&#xff0c;基本的传输单位是tcp报文段&#xff1b;而udp是面向报文的&#xff0c;基本传输单位是用户数据报。 第二&#xff0c; tcp注重安全可靠性&#xff0c;连接双方在…

Linux网络 - HTTP协议

文章目录 前言一、HTTP协议1.urlurl特殊字符 requestrespond 总结 前言 上一章内容我们讲了在应用层制定了我们自己自定义的协议、序列化和反序列化。 协议的制定相对来讲还是比较麻烦的&#xff0c;不过既然应用层的协议制定是必要的&#xff0c;那么肯定已经有许多计算机大佬…

内存分配器性能优化

背景 在之前我们提到采用自定义的内存分配器来解决防止频繁 make 导致的 gc 问题。gc 问题本质上是 CPU 消耗&#xff0c;而内存分配器本身如果产生了大量的 CPU 消耗那就得不偿失。经过测试初代内存分配器实现过于简单&#xff0c;产生了很多 CPU 消耗&#xff0c;因此必须优…

果汁机锂电池充电,5V升压12.7V 升压恒压芯片SL1571B

在现代化的日常生活中&#xff0c;果汁机已经逐渐成为了许多家庭厨房的必备电器。随着科技的不断进步&#xff0c;果汁机的性能也在不断提升&#xff0c;其中锂电池的应用更是为果汁机带来了前所未有的便利。而5V升压12.7V升压恒压芯片SL1571B&#xff0c;作为果汁机锂电池充电…

skywalking9.4 链路追踪

下载&#xff0c;很慢很慢很慢&#xff01;&#xff01;&#xff01;&#xff01; jdk 使用jdk17 skywalking-apm 9.4 java-agent 9.0 idea 本地开发配置 第1行配置按实际来&#xff1b; 第2行自定义&#xff0c;一般和微服务名称相同&#xff1b; 第3行ip写安装的机器ip,端…

QQ音乐绿钻API接口:解锁更多音乐可能性

在我们日常生活中&#xff0c;音乐是不可或缺的一部分。无论是在上班途中&#xff0c;还是在健身房锻炼时&#xff0c;我们都可以通过听音乐来放松自己。然而&#xff0c;在现如今的音乐市场中&#xff0c;有时候我们会觉得收听的歌曲有限&#xff0c;想要尝试更多不同的音乐类…

量产导入 | DFT和ATE概述

什么是DFT DFT(Design for Test),即可测性设计。 一切为了芯片流片后测试所加入的逻辑设计,都叫DFT。 DFT只是为了测试芯片制造过程中有没有缺陷,而不是用来验证芯片功能的,芯片功能的完善应该应该是在芯片开发过程用先进验证方法学去做的。 芯片制造过程相当复杂,工艺缺陷…

降价潮背后:大模型落地门槛真的降了吗?

“比起价格门槛&#xff0c;AI大模型的应用门槛&#xff0c;更难跨越。” 大模型争相降价下&#xff0c;AI应用的门槛真的降低了吗&#xff1f; 答案还真不一定。因为除了价格门槛&#xff0c;AI大模型还有应用门槛。甚至&#xff0c;后者比前者更具挑战性。 B端业务场景向来…

3D感知视觉表示与模型分析:深入探究视觉基础模型的三维意识

在深度学习与大规模预训练的推动下&#xff0c;视觉基础模型展现出了令人印象深刻的泛化能力。这些模型不仅能够对任意图像进行分类、分割和生成&#xff0c;而且它们的中间表示对于其他视觉任务&#xff0c;如检测和分割&#xff0c;同样具有强大的零样本能力。然而&#xff0…

Java集合的组内平均值怎么计算

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; 在Java中&#xff0c;经常需要对集合进行各种操作&#xff0c;其中之一就是计算集合的组内平均值。本文将介绍如何使用Java集合来计算组内平均值&#xff0c;并提供一些示例代码和实用技巧。 1. 使用Java 8 Stream A…

MMdeploy在cuda+tensorrt下的配置和编译

MMdeploy在cudatensorrt下的配置和编译 Python安装配置MMdeploy配置openmmlab系列从工程安装mmdeploy MMdeploy_runtime以及demo编译安装量化编译runtime和demo Python安装配置MMdeploy 配置openmmlab系列 pip install -U openmim如果mim命令遭遇故障&#xff0c;或者安装失败…

龙迅LT9211D MIPIDSI/CSI桥接到2 PORT LVDS,支持 3840x2160 30Hz分辨率

龙迅LT9211D描述&#xff1a; LT9211D是一款高性能的MIPI DSI/CSI-2到双端口LVDS转换器。LT9211D反序列化输入的MIPI视频数据&#xff0c;解码数据包&#xff0c;并将格式化的视频数据流转换为AP和移动显示面板或摄像机之间的LVDS发射机输出。LT9211D支持最大12.5 dB输入均衡和…

boost asio异步服务器(3)增加发送队列实现全双工通信

增加发送节点 构造发送节点&#xff0c;管理发送数据。发送节点的类如下。 这个发送节点用于保证发送和接收数据的有效性。 增加发送队列 前边实现的是一个简单的echo服务器&#xff0c;也就是服务器将收到的内容发送给对应的客户端。但是在实际的服务器设计中&#xff0c;服务…

《精通ChatGPT:从入门到大师的Prompt指南》第7章:创意写作

第7章&#xff1a;创意写作 7.1 角色设定 角色设定是创意写作中最关键的环节之一。成功的角色设定能够让读者对故事产生共鸣&#xff0c;使故事更加生动有趣。角色不仅仅是情节发展的载体&#xff0c;更是读者情感的投射对象。因此&#xff0c;深入了解如何设定一个生动而有深…

讯方技术与华为终端签署鸿蒙合作协议,将为企业助培百万鸿蒙人才

1月18日&#xff0c;鸿蒙生态千帆启航仪式在深圳举行&#xff0c;华为宣布HarmonyOS NEXT鸿蒙星河版开发者预览面向开发者开放申请&#xff0c;这意味着鸿蒙生态进入第二阶段&#xff0c;将加速千行百业的应用鸿蒙化。讯方技术总裁刘国锋、副总经理刘铭皓应邀出席启航仪式&…