SpringSecurity(二十四)--OAuth2:使用JWT和加密签名(下)非对称密钥加密

news2024/11/17 21:42:41

一、前言

由于上文对称密钥涉及到的内容比较多,所以这一节的非对称密钥加密拆开成这一节单独讲解。
所以大家尽量先阅读完上一章的内容后再浏览这一章内容会更好。

二、使用通过JWT和非对称密钥签名的令牌

本节将实现OAuth2身份验证的一个示例,其中授权服务器和资源服务器会使用一个非对称密钥对来对令牌签名和验证令牌。有时只让授权服务器和资源服务器共享一个密钥的做法是不可行的。通常,如果授权服务器和资源服务器不是由同一组织开发的,就会发生这种情况。在这种情况下,就可以认为授权服务器不“信任:资源服务器,因此我们不希望授权服务器与资源服务器共享密钥。而且,使用对称密钥,资源服务器就拥有了过多的功能:不仅可以验证令牌,还可以对它们签名(这种情况示例见下图):
在这里插入图片描述
ps:对称密钥是私钥。有该密钥的人可以用它进入系统,所以请永远不要在不安全通道交换对称密钥。如果需要在系统之外共享密钥,它就不应该是对称的
当我们不能在授权服务器和资源服务器之间认定一种可信的关系时,就要使用非对称密钥对。由于这个原因,我们就需要知道如何实现这样的系统。
什么是非对称密钥对?它是如何工作的?这个概念很简单。非对称密钥对有两个密钥:一个称为私钥,另一个称为公钥。授权服务器将使用私钥对令牌进行签名,而其他人也只能使用私钥对令牌进行签名。
在这里插入图片描述
公钥与私钥是结合在一起的,这就是我们将其称为一对的原因。但是公钥只能用于验证签名。没有人可以使用公钥对令牌进行签名(见下图)
在这里插入图片描述

2.1、生成密钥对

本节将讲解如何生成一个非对称密钥对。这里需要一个密钥对用来配置后面实现的授权服务器和资源服务器。这是一个非对称密钥对(这意味着授权服务器使用它的私钥签署令牌,而资源服务器则使用公钥验证签名)。为了生成该密钥对,这里使用了keytool和OpenSSL,它们是两个简单易用的命令行工具。Java的JDK会安装keytool,所以我们的计算机一般都安装了它。而对于OpenSSL.则需要从官网处下载它。如果使用OpenSSL自带的Git Bash,则不需要单独安装它。有了工具之后,需要运行两个命令:

  • 生成一个私钥
  • 获取之前所生成私钥的公钥

2.1.1、生成一个私钥

要生成私钥,可以运行下面代码片段中的keytool命令。它将在名为mbw.jks的文件中生成一个私钥。这里还是用了密码”mbw123"保护私钥,并且使用别名"mbw"为密钥指定一个名称。在下面的命令中,可以看到用来生成密钥的算法,即RSA。

keytool -genkeypair -alias mbw -keyalg RSA -keypass mbw123 -keystore mbw.jks -storepass mbw123

运行后,回答相关问题生成你的dn后输入y,在keytool.exe所在文件夹下就生成了mbw.jks文件:
在这里插入图片描述

2.1.2、获取公钥

要获取先前所生成私钥的公钥,可以运行这个keytool命令:

keytool -list -rfc --keystore mbw.jks | openssl x509 -inform pem -pubkey

在生成公钥时,系统会提示我们输入密码;这里使用的是mbw123.然后就可以在输出中找到公钥和证书。(对于本示例而言,只有密钥的值是必要的。)这个密钥应该类似于下面的代码片段:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7HHwNVxcW6iYwyzqbMt
awSkqiARgh6pz5eArBa1KzG3dCH8pupONozjD0G+PCAEu/hCwxEYglLHVLfcuQil
8b8rWuGXVZgA+VHohEEO5KHibKqazGpJGSDQkJG6VNnfuMasZ7DUeQpyyUI6RRkz
CSU7NuQAHHX5J9QEtrEAodv4Mpla1sKTraxMxjb3+BUtTFvW4iSr9fNJWyUsoxDs
6mOhRYEOUUiBl2P8USm7HK7M7rWy90BRG7hOFvKKjXiyM+d2uRN6ASOWVQ168ZcD
0iOdUeBF4sNowZWqWoY4DOiLp0bFRQIDkKuwxGvwrBRNA/K2J+HY0Jz2ULXhdGlU
iQIDAQAB
-----END PUBLIC KEY-----

就是这样!现在我们有了一个用于JWT签名的私钥和一个用于验证签名的公钥。接下来只需要在授权和资源服务器中配置它们即可。

2.1.3、实现使用私钥的授权服务器

本节要将授权服务器配置为使用私钥签名JWT。这里首先我们将之前的mbw.jks复制到应用程序的resources文件夹中。需要将密钥添加到resources文件夹中,因为直接从类路径读取它会更容易。但是,将其放入类路径中的做法并不是强制的。在application.yaml文件中,存储了文件名、密钥的别名,以及用于保护私钥而生成的密码。我们需要这些详细信息用来配置JwtTokenStore.下面代码片段展示了yaml文件中的内容:

password: mbw123
privateKey: mbw.jks
alias: mbw

与之前使用对称密钥相比,唯一更改的是JwtAccessTokenConverter对象的定义。这里仍然使用JwtTokenStore,我们仍将使用JwtAccessTokenConverter对象设置私钥。

import com.mbw.security.service.ClientDetailsServiceImpl;
import com.mbw.security.service.UserDetailsServiceImpl;
import com.mbw.security.token.JsonRedisTokenStore;
import com.mbw.security.token.enhancer.CustomTokenEnhancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import java.util.ArrayList;
import java.util.List;


@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Autowired
	private ClientDetailsServiceImpl clientDetailsServiceImpl;
	@Autowired
	private UserDetailsServiceImpl userDetailsServiceImpl;
	@Autowired
	private CustomTokenEnhancer customTokenEnhancer;
	@Autowired
	private JsonRedisTokenStore jsonRedisTokenStore;

	@Value("${jwt.key}")
	private String jwtKey;

	@Value("${privateKey}")
	private String privateKey;

	@Value("${password}")
	private String password;

	@Value("${alias}")
	private String alias;

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.authenticationManager(authenticationManager)
				.userDetailsService(userDetailsServiceImpl)
				.tokenEnhancer(tokenEnhancerChain())
				.tokenStore(tokenStore());
		DefaultTokenServices tokenService = getTokenStore(endpoints);
		endpoints.tokenServices(tokenService);
	}

	@Bean
	public TokenEnhancerChain tokenEnhancerChain() {
		TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
		List<TokenEnhancer> enhancers = new ArrayList<>();
		enhancers.add(jwtAccessTokenConverter());
		enhancers.add(customTokenEnhancer);
		enhancerChain.setTokenEnhancers(enhancers);//将自定义Enhancer加入EnhancerChain的delegates数组中
		return enhancerChain;
	}

	//配置TokenService参数
	private DefaultTokenServices getTokenStore(AuthorizationServerEndpointsConfigurer endpoints) {
		DefaultTokenServices tokenService = new DefaultTokenServices();
		tokenService.setTokenStore(endpoints.getTokenStore());
		tokenService.setSupportRefreshToken(true);
		tokenService.setClientDetailsService(endpoints.getClientDetailsService());
		tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
		//token有效期 1小时
		tokenService.setAccessTokenValiditySeconds(3600);
		//token刷新有效期 15天
		tokenService.setRefreshTokenValiditySeconds(3600 * 12 * 15);
		tokenService.setReuseRefreshToken(false);
		return tokenService;
	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.withClientDetails(clientDetailsServiceImpl);
	}


	/**
	 * 解决访问/oauth/check_token 403的问题
	 *
	 * @param security
	 * @throws Exception
	 */
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		// 允许表单认证
		security
				.tokenKeyAccess("permitAll()")
				.checkTokenAccess("permitAll()")
				.allowFormAuthenticationForClients();

	}

	@Bean
	public TokenStore tokenStore() {
		return new JwtTokenStore(jwtAccessTokenConverter());
	}

	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
		KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource(privateKey), password.toCharArray());
		jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));
		return jwtAccessTokenConverter;
	}
}

现在可以启动该授权服务器并调用/oauth/token端点来生成一个新的访问令牌。当然,这里只是创建了一个普通的JWT,但是现在的区别是,要验证它的签名,需要使用密钥对中的公钥。
如果遇到启动报错说找不到jks文件的,把idea关了重启就好了,我idea2019.3是这样处理成功的,下面是postman的运行截图:
在这里插入图片描述
然后若现在我们使用这个token去请求资源服务器,肯定会失败的:
在这里插入图片描述
所以我们现在要去资源服务器配置公钥,在配置公钥之前,我们先把授权服务器的这行代码删除
在这里插入图片描述
原因之后会讲到。
首先在yaml文件配置公钥:

jwt:
  key: MjWP5L7CiD
  publicKey: -----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7HHwNVxcW6iYwyzqbMtawSkqiARgh6pz5eArBa1KzG3dCH8pupONozjD0G+PCAEu/hCwxEYglLHVLfcuQil8b8rWuGXVZgA+VHohEEO5KHibKqazGpJGSDQkJG6VNnfuMasZ7DUeQpyyUI6RRkzCSU7NuQAHHX5J9QEtrEAodv4Mpla1sKTraxMxjb3+BUtTFvW4iSr9fNJWyUsoxDs6mOhRYEOUUiBl2P8USm7HK7M7rWy90BRG7hOFvKKjXiyM+d2uRN6ASOWVQ168ZcD0iOdUeBF4sNowZWqWoY4DOiLp0bFRQIDkKuwxGvwrBRNA/K2J+HY0Jz2ULXhdGlUiQIDAQAB-----END PUBLIC KEY-----

然后在资源服务器这块儿对公钥进行配置:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import javax.servlet.http.HttpServletResponse;


@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

	@Value("${jwt.publicKey}")
	private String publicKey;

	@Override
	public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
		resources.tokenStore(tokenStore());
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
				.csrf().disable()
				.exceptionHandling()
				.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
				.and()
				.authorizeRequests()
				.antMatchers("/test/**").authenticated()
				.and()
				.httpBasic();
	}


	@Bean
	public TokenStore tokenStore(){
		return new JwtTokenStore(jwtAccessTokenConverter());
	}

	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter(){
		JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
		jwtAccessTokenConverter.setVerifierKey(publicKey);
		return jwtAccessTokenConverter;
	}
}

然后启动资源服务器测试被保护的资源:
在这里插入图片描述

2.2、使用一个暴露公钥的端点

本节将讨论一种让资源服务器获知公钥的方法–用授权服务器暴露公钥。还记得我们上一节将授权服务器删掉的那一行代码吗,这不就来了?在上一节中,我们使用了公私密钥对来对令牌进行签名和验证。其中在资源服务器配置了公钥。资源服务器使用公钥验证JWT。但是,如果想更改密钥时,会发生什么情况呢》最好不要永远保持同一份密钥对,这就是本节要实现的内容。随着时间的推移,应该定期旋转密钥!这将使得系统不容易受到密钥失窃的影响。
在这里插入图片描述
到目前为止,我们已经在授权服务器端配置了私钥,在资源服务器配置了公钥。而将密钥设置在两个地方使得密钥更难管理。不过如果只在一端配置它们,则可以更容易地管理键。解决方案是将整个密钥对迁至授权服务器端,并允许授权服务器使用端点暴露公钥。
在这里插入图片描述
那么下面就进入开发
对于授权服务器,我们保持和之前一样的配置即可,只需要能确保能够访问端点即可,也就是暴露公钥。的确,Spring Boot已经配置了这样的端点,但也只是这样而已。默认情况下,对该端点的所有请求都会被拒绝。我们需要重写该端点的配置,并允许任何具有客户端凭据的人访问它。代码如下:

import com.mbw.security.service.ClientDetailsServiceImpl;
import com.mbw.security.service.UserDetailsServiceImpl;
import com.mbw.security.token.JsonRedisTokenStore;
import com.mbw.security.token.enhancer.CustomTokenEnhancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import java.util.ArrayList;
import java.util.List;


@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Autowired
	private ClientDetailsServiceImpl clientDetailsServiceImpl;
	@Autowired
	private UserDetailsServiceImpl userDetailsServiceImpl;
	@Autowired
	private CustomTokenEnhancer customTokenEnhancer;
	@Autowired
	private JsonRedisTokenStore jsonRedisTokenStore;

	@Value("${jwt.privateKey}")
	private String privateKey;

	@Value("${jwt.password}")
	private String password;

	@Value("${jwt.alias}")
	private String alias;

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.authenticationManager(authenticationManager)
				.userDetailsService(userDetailsServiceImpl)
				.tokenEnhancer(tokenEnhancerChain())
				.tokenStore(tokenStore());
		DefaultTokenServices tokenService = getTokenStore(endpoints);
		endpoints.tokenServices(tokenService);
	}

	@Bean
	public TokenEnhancerChain tokenEnhancerChain() {
		TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
		List<TokenEnhancer> enhancers = new ArrayList<>();
		enhancers.add(jwtAccessTokenConverter());
		enhancers.add(customTokenEnhancer);
		enhancerChain.setTokenEnhancers(enhancers);//将自定义Enhancer加入EnhancerChain的delegates数组中
		return enhancerChain;
	}

	//配置TokenService参数
	private DefaultTokenServices getTokenStore(AuthorizationServerEndpointsConfigurer endpoints) {
		DefaultTokenServices tokenService = new DefaultTokenServices();
		tokenService.setTokenStore(endpoints.getTokenStore());
		tokenService.setSupportRefreshToken(true);
		tokenService.setClientDetailsService(endpoints.getClientDetailsService());
		tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
		//token有效期 1小时
		tokenService.setAccessTokenValiditySeconds(3600);
		//token刷新有效期 15天
		tokenService.setRefreshTokenValiditySeconds(3600 * 12 * 15);
		tokenService.setReuseRefreshToken(false);
		return tokenService;
	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.withClientDetails(clientDetailsServiceImpl);
	}


	/**
	 * 解决访问/oauth/check_token 403的问题
	 *
	 * @param security
	 * @throws Exception
	 */
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		// 允许表单认证
		security
				.tokenKeyAccess("isAuthenticated()")
				.checkTokenAccess("permitAll()")
				.allowFormAuthenticationForClients();

	}

	@Bean
	public TokenStore tokenStore() {
		return new JwtTokenStore(jwtAccessTokenConverter());
	}

	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
		KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource(privateKey), password.toCharArray());
		jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));
		return jwtAccessTokenConverter;
	}
}

现在可以启动该授权服务器并调用/oauth/token_key端点来确保正确实现了配置。下面postman展示了其调用,我们需要使用Basic Auth输入资源服务器曾经注册的账号进行认证:
在这里插入图片描述
为了让资源服务器可以使用此端点并获得公钥,只需要在其属性文件中配置该端点和凭据即可。下面的配置代码展示了资源服务器的yaml新增配置:

server:
  port: 9091
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/spring_security?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    initialization-mode: always
  # redis配置
  redis:
    host: 127.0.0.1
    password: 123456
    port: 6379
jwt:
  key: MjWP5L7CiD
security:
  oauth2:
    resource:
      jwt:
        key-uri: http://localhost:9090/oauth/token_key
    client:
      client-id: resourceServer
      client-secret: resourceServerSecret

因为资源服务器现在从授权服务器的/oauth/token_key端点获取公钥,所以不需要在资源服务器配置类中配置它。资源服务器只需要配置保护的资源即可,如下面代码片段所示:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

import javax.servlet.http.HttpServletResponse;


@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
				.csrf().disable()
				.exceptionHandling()
				.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
				.and()
				.authorizeRequests()
				.antMatchers("/test/**").authenticated()
				.and()
				.httpBasic();
	}
}

现在启动资源服务器,带着令牌访问被保护资源:
在这里插入图片描述
至此,Spring Security的主要学习内容就告一段落,非常感谢大家支持!
如果有感兴趣的小伙伴想要项目,可以参考我的gitee中的OAuth2的项目,也是我自己学习一路开发过来的,我也希望大家能够指出我文章中的错误或是解决我文章中的提出的疑问,因为笔者自己本身仍然很菜,后面会慢慢深层学习。最后,再次表示感谢,希望大家能够继续支持我后面的elasticSearch。

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

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

相关文章

自定义事件实现rpc的调用C++和蓝图实现举例

参考视频&#xff1a;https://www.youtube.com/watch?vGcZQ2o6LpDI 1.自定义事件的方式实现rpc run on server 修改角色的最大速度&#xff0c;方框1&#xff0c;让客户端先行&#xff0c;速度直接改变&#xff1b;然后方框2&#xff0c;告知服务器&#xff0c;自己的速度已经…

双脚在路上,钢笔在手里,想法在脑中,2023年CSDN将在心头

☔️&#xff08;一&#xff09;行走过的道路 一年的时间说长不长&#xff0c;说短不短&#xff0c;所渡过时光的长短在于你是否留意你曾走过的路。 &#x1f434;① 记得2022年初我所许下的flag&#xff0c;是要在CSDN平台上运用今年一年的时间撰写超50篇的技术文章&#xff0…

MySQL-运算符详解

1. 算数运算符 运算符名称作用示例加法计算两个值或表达式的和SELECT A B-减法计算两个值或表达式的差SELECT A - B*乘法计算两个值或表达式的乘积SELECT A * B/或DIV除法计算两个值或表达式的商SELECT A / B%或MOD求模(求余)计算两个值或表达式的余数SELECT A % B 2. 比较运…

vue导入私有组件和注册全局组件和props自定义属性

目录先下载并配置插件导入私有组件注册全局组件props自定义属性使用先下载并配置插件 导入的时候需要路径,有个符号,但不能提示路径,需要手打路径,会发现很麻烦,这时候可以通过vscode插件来解决 vscode搜索Path Autocomplete 配置插件,点击插件设置—扩展设置,点开任意一个set…

CRM客户关系管理:赢得和留住客户的指南

客户管理是一个涉及协调和管理客户与企业之间互动的过程。它对企业的商誉及其保留和获得新客户的能力有重大影响。 一般来说&#xff0c;客户管理可以分解成四个不同的部分&#xff1a; - 了解客户的需求以及他们想从你这里得到什么 - 满足这些要求并对他们的询问提供充分的…

EIZO船舶触摸屏维修T1502-B

EIZO船舶触摸屏使用注意事项&#xff1a; 1 由于显示器电子零件的性能需要约30分钟才能稳定,因此在电源开启之后,应调整显示器30分钟以上。 2为了降低因长期使用而出现的发光度变化以及保持稳定的发光度,建议您以较低亮度使用显示器。 3 当显示器长期显示一个图像的情况下再…

传统卷积与Transformers 优缺点对比

近两年Transformer如日中天&#xff0c;刷爆各大CV榜单&#xff0c;但在计算机视觉中&#xff0c;传统卷积就已经彻底输给Transformer了吗&#xff1f; 回答1 作者&#xff1a;DLing 链接&#xff1a;https://www.zhihu.com/question/531529633/answer/2819350360 看在工业界还…

【云原生进阶之容器】第二章Controller Manager原理--client-go剖析

2 Client-go Kubernetes 官方从 2016 年 8 月份开始,将 Kubernetes 资源操作相关的核心源码抽取出来,独立出来一个项目 client-go,Kubernetes中使用client-go作为Go语言的官方编程式交互客户端库,提供对api server服务的交互访问。对于k8s的二次开发,熟练掌握client-go是十…

大胆预测,2023年Android 行业什么技术最重要~

随着Android 时代的发展&#xff0c;在2022的这一年里&#xff0c;感觉自己经历了许多&#xff0c;从年初到年底&#xff0c;见证了不是互联网公司的裁员、优化、毕业、输送人才……等一些列的操作&#xff0c;估计有些人和我一样对Android未来感到茫然&#xff0c;不少人可能会…

发表计算机SCI论文,是先写中文,还是直接写英文论文? - 易智编译EaseEditing

经过高考、四六级和研究生考试&#xff0c;我们都有一定的英文基础&#xff0c;也都知道英文和中文的差别就是中国人和欧美人的思维差别。在这里对中英文写作的优缺点进行列举和分析&#xff1a; 直接写英文论文&#xff1a; &#xff08;1&#xff09;中英文表述方式差异明显…

【图像算法】pytesseract简单实现图片数字识别

【前置目的】 识别视频中是否包含目标元素&#xff1b; 抽象自动化&#xff0c;就是处理一段含有时间戳的视频&#xff1b; 再核心就是对视频进行图片裁减&#xff0c;识别出图片中的数字&#xff0c;做数学计算延时。 【学习地址】 环境&#xff1a;mac、python3、pytesserac…

PCB阻焊桥的工艺设计,华秋一文告诉你

PCB表面的一层漆称为阻焊油墨&#xff0c;也就是PCB线路板防焊油墨。阻焊油墨是PCB线路板中非常常见也是主要使用的油墨。阻焊油墨一般90%都是绿色的&#xff0c;但也有其他颜色&#xff0c;例如&#xff1a;红色、蓝色、黑色、白色、黄色称之为杂色油墨。 阻焊油墨的作用就是…

星尘数据完成5000万元A轮融资,Autolabeling加速自动驾驶量产

近日&#xff0c;国内领先的AI数据服务商星尘数据宣布完成A轮融资5000万元人民币&#xff0c;本轮融资由华映资本领投&#xff0c;小米生态链背景的厚天资本和瑞夏资本跟投。融资将用于端到端的数据闭环系统研发、商务拓展以及供应商合作。星尘数据创始人、CEO章磊表示&#xf…

用Python制作一个文件解压缩工具

经常由于各种压缩格式的不一样用到文件的解压缩时就需要下载不同的解压缩工具去处理不同的文件&#xff0c;以至于桌面上的压缩工具就有三四种&#xff0c;于是使用python做了一个包含各种常见格式的文件解压缩的小工具。 常见的压缩格式主要是下面的四种格式&#xff1a; zip…

New File Format:SpreadJS v16 Crack

New File Format 有你需要的更新内容&#xff0c;请大家及时更新到最新版 SpreadJS V16 has just released, and with it, Ω578867473 some exciting new features, including a new SpreadJS file format and enhancements to the TableSheet, Designer, Calculation, and W…

基于51单片机的数字电压表(TCL549)(Proteus仿真+程序)

编号&#xff1a;31 基于51单片机的数字电压表&#xff08;TCL549&#xff09; 功能描述&#xff1a; 本系统由51单片机最小系统TCL549模块一路模拟量输入模块液晶1602显示模块 1、主控制器是89C52单片机 2、TCL54模数转换器进行A/D转换&#xff0c;读取电压一路数据&#xf…

VSCode(Flutter开发)使用的 4 个 技巧

1.更清晰的文件夹结构 在创建一个新的 flutter 项目后&#xff0c;有太多的文件。但是我们可以在 VSCode 中用非常简单的步骤来构造这些文件: 打开命令面板(Ctrl/Cmd Shift P) 键入“首选项: 打开设置(JSON)” 将以下代码行添加到 setings.json: { "explorer.fileNes…

yapi的安装

Yapi的安装 Yapi是一款不错的接口管理软件&#xff0c;我主要用它来进行接口Mock。 Yapi安装所需环境&#xff1a; Node.js&#xff08;7.6&#xff09;Mongodb&#xff08;2.6&#xff09;git 各环境安装地址&#xff1a; git&#xff1a;https://git-scm.com/downloadsN…

[极客大挑战 2019]Buy Flag1(BUUCTF)

前言: 这篇文章还是是为了帮助一些 像我这样的菜鸟 找到简单的题解 题目描述 解题工具: 我爱用edit this cookie2和hackerbar&#xff0c; 当然也可以burpsuite和fiddler抓包 解题过程: 看到他说flag要100000000 MONEY&#xff0c; 还要是Cuits students&#xff0c; …

windows11 elasticsearch-head 插件安装

1.elasticsearch-head 插件介绍 elasticSearch-head就是一款能连接ElasticSearch搜索引擎&#xff0c;并提供可视化的操作页面对elasticSearch搜索引擎进行各种设置和数据检索功能的管理插件&#xff0c;如在head插件页面编写RESTful接口风格的请求&#xff0c;就可以对Elastic…