SpringSecurity(二十一)--OAuth2:实现资源服务器(中)实现带有JdbcTokenStore的黑板模式

news2024/10/2 22:26:22

一、前言

本章将实现授权服务器和资源服务器使用共享数据库的应用程序。这一架构方式被称为黑板模式。这一架构方式被称为黑板模式。为什么叫黑板模式呢?因为可以将其视为使用黑板管理令牌的授权服务器和资源服务器。这种颁发和验证令牌的方法的优点是消除了资源服务器和授权服务器之间的直接通信。但是,这意味着要添加一个共享数据库,而这可能会成为瓶颈。与任何架构一样,实际上它适用于各种情况。例如,如果各服务已经共享了一个数据库,那么对访问令牌也使用这种方法可能也是合理的。出于这个原因,了解如何实现这个方法对你来说可能是很重要的。
并且学习该章的前提需要把前面的授权,资源服务器的搭建先给整完,否则会很懵,不知道这块儿代码哪来的这种情况。

二、实现黑板模式

我们将继续之前的项目基础上进行修改。首先解释一下黑板模式的架构:当授权服务器颁发令牌时,它也会将令牌存储在与资源服务器共享的数据库中
在这里插入图片描述
它还意味着资源服务器在需要验证令牌时将访问该数据库.

在这里插入图片描述
如上图,资源服务器在共享数据库中搜索令牌。如果令牌存在,则资源服务器将在数据库中找到与它相关的详细信息,包括用户名及其权限。有了这些详细信息,资源服务器就可以对请求进行授权。
在授权服务器和资源服务器上,代表在Spring Security中管理令牌的对象的契约是TokenStore。对于授权服务器,可以把它想象成在我们以前使用SecurityContext的身份验证架构中的位置,身份验证完成后,授权服务器会使用TokenStore生成一个令牌
在这里插入图片描述
对于资源服务器。身份验证过滤器要使用TokenStore验证令牌并查找稍后用于授权的用户详细信息。然后资源服务器会将用户的详细信息存储在安全上下文中
在这里插入图片描述

ps:授权服务器和资源服务器实现了两种不同的职责,但这些职责不一定必须由两个独立的应用程序执行。不过在大多数真实的实现中,我们都会在不同的应用程序中开发它们,但是,也可以选择在同一个应用程序实现这两者。在这种情况下,就不需要建立任何调用或使用一个共享数据库。但是,如果在同一个应用程序中实现这两种职责,授权服务器和资源服务器就都可以访问相同的bean.因此,它们可以使用相同的令牌存储,而不需要进行网络调用或访问数据库。请大家记住这句结论,这个我们后面搭建redisTokenStore会用到这个结论。
SpringSecurity为TokenStore接口提供了各种实现,在大多数情况下,我们都不需要编写自己的实现。例如,对于前面的所有授权服务器实现,我们都没有指定TokenStore实现。SpringSecurity提供了一个InMemoryTokenStore类型的默认令牌存储。可以想见,在所有这些情况下,令牌都会存储在应用程序的内存中。它们没有被持久化!如果重启授权服务器,那么启动前颁发的令牌将不再生效
为了使用黑板模式实现令牌管理,Spring Security提供了JdbcTokenStore实现。顾名思义,这个令牌存储直接通过JDBC与数据库一起工作。它的工作原理类似于之前讨论的JdbcUserDetailsManager,但与用户管理不同的是,JdbcTokenStore管理的是令牌。
ps:我们只是在这一章中选择了JdbcTokenStore实现黑板模式,但是也可以选择使用TokenStore持久化令牌,甚至你可以自己重写你自己的tokenStore以达到你的理想的持久化的方式,例如下一章会说到的重写一个TokenStore以达到redis存储令牌,并且将相关数据放进本地缓存caffeine.那么本章我们还是以JdbcTokenStore为例子进行讲解。
JdbcTokenStore期望数据库中有两个表。它会使用一个表存储访问令牌(该表的名称默认规定为oauth_access_token)和一个表存储刷新令牌(该表的名称默认规定为oauth_refresh_token)。用于存储令牌的表将同时持久化刷新令牌
ps:注意如果我们不对JdbcTokenStore进行重写的话,表名和表的结构都是默认规定好的,不去重写是不能任意去更改表名表的结构等。
如下图,JdbcTokenStore默认已经封装了很多的SQL语句,这也证明你不能在不重写JdbcTokenStore的情况下去更改表的结构。
在这里插入图片描述
但是,如果你想使用其他表或者其他列甚至是属性结构,SpringSecurity也允许你去自定义JdbcTokenStore,当然你必须重写它用来检索或存储令牌的所有相关SQL。那这里我们就不对JdbcTokenStore进行重写,采取默认结构去进行讲解:
同样的,既然和sql挂钩,我们就得从表结构开始说起,以下两张表结构已经固定好了。大家直接copy就行:

DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token`  (
  `token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'MD5加密的access_token的值',
  `token` blob NULL COMMENT 'OAuth2AccessToken.java对象序列化后的二进制数据',
  `authentication_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'MD5加密过的username,client_id,scope',
  `user_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '登录的用户名',
  `client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端ID',
  `authentication` blob NULL COMMENT 'OAuth2Authentication.java对象序列化后的二进制数据',
  `refresh_token` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'MD5加密后的refresh_token的值',
  PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '访问令牌' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token`  (
  `token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'MD5加密过的refresh_token的值',
  `token` blob NULL COMMENT 'OAuth2RefreshToken.java对象序列化后的二进制数据',
  `authentication` blob NULL COMMENT 'OAuth2Authentication.java对象序列化后的二进制数据'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '更新令牌' ROW_FORMAT = Dynamic;

然后授权服务器和资源服务器均加入Mybatis和mysq-javal相关依赖:

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

在application.yaml文件中,需要添加数据源的定义。以下代码片段提供了该定义:

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
mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml,classpath:/META-INF/modeler-mybatis-mappings/*.xml
  typeAliasesPackage: com.mbw.pojo
  global-config:
    banner: false
  configuration:
    map-underscore-to-camel-case: false
    cache-enabled: false
    call-setters-on-nulls: true
    jdbc-type-for-null: 'null'
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

接下来我们需要在之前配置的授权服务器配置类AuthServerConfig中注入数据源,然后定义和配置令牌存储。下面代码显示了这一更改:

import com.mbw.security.service.ClientDetailsServiceImpl;
import com.mbw.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Autowired
	private ClientDetailsServiceImpl clientDetailsServiceImpl;
	@Autowired
	private UserDetailsServiceImpl userDetailsServiceImpl;
	//注入application.yaml文件中的数据源
	@Autowired
	private DataSource dataSource;

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.authenticationManager(authenticationManager)
		.userDetailsService(userDetailsServiceImpl)
				.tokenStore(tokenStore());
	}

	@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 JdbcTokenStore(dataSource);
	}
}

并且我们可以对token本身做相关的设置,例如设置accessToken和refreshToken的有效时间,这里SpringSecurity给我们提供了TokenService类,我们可以通过配置它来对token的一些属性作设置,这里我们也在授权服务器配置类配置即可,完整的AuthServerConfig类如下:

import com.mbw.security.service.ClientDetailsServiceImpl;
import com.mbw.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Autowired
	private ClientDetailsServiceImpl clientDetailsServiceImpl;
	@Autowired
	private UserDetailsServiceImpl userDetailsServiceImpl;
	@Autowired
	private JsonRedisTokenStore jsonRedisTokenStore;
	//注入application.yaml文件中的数据源
	@Autowired
	private DataSource dataSource;

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

	//配置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 JdbcTokenStore(dataSource);
	}
}

现在可以启动授权服务器并颁发令牌,这个和前面章节获取令牌是一样的,我们仍然可以采取password授权类型颁发令牌:
在这里插入图片描述
响应中返回的访问令牌也可以在oauth_access_token表中作为其记录而找到。由于数据库会持久化令牌,因此及时授权服务器关闭或者重新启动后,资源服务器也可以验证已颁发且未过期的令牌。
在这里插入图片描述
因为配置了刷新令牌授权类型,所以会接收到一个刷新令牌。出于这个原因,还可以在oauth_refresh_token表中找到刷新令牌的记录。
在这里插入图片描述
且我们还可以通过该refreshToken重新获取一个新的token和refreshToken:
在这里插入图片描述
现在是配置资源服务器的时候,以便它也可以使用相同的数据库。所以我们也需要在之前的资源服务器上加入和授权服务器同样的数据源相关依赖和配置,这里我就不作代码演示了,直接copy授权服务器直接写的配置和依赖即可。
然后来到资源服务器的配置类中,需要注入数据源并配置JdbcTokenStore:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
	
	@Autowired
	private DataSource dataSource;

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

	@Bean
	public TokenStore tokenStore(){
		return new JdbcTokenStore(dataSource);
	}

}

现在可以启动资源服务器,并使用之前颁发的访问令牌访问/test/yidou测试端点。
在这里插入图片描述
这样,我们就实现了黑板模式,用于资源服务器和授权服务器之间的通信。其中使用了一个名为JdbcTokenStore的TokenStore实现。现在可以将令牌持久化到数据库中了,并且可以避免在资源服务器和授权服务器之间使用直接调用来验证令牌。但是让授权服务器和资源服务器都依赖于同一个数据库是一个缺点。在大量请求的情况下,共享数据库可能成为一个瓶颈,并影响系统性能。那么就有了可以让redis存储令牌让响应速度快一点,但是这种方案治标不治本,那么有其他实现选项吗?答案是肯定的:在JWT中使用签名令牌,这个我们后面会讲到。
最后的最后,让大家思考一点,在对资源服务器去作相关配置的时候,我说了为了让资源服务器和授权服务器使用相同的tokenStore(共享数据库),所以需要在资源服务器配置和授权服务器同样的tokenStore,这样的做法真的好吗,真的合适吗?说的绝一点,有时候我们需要对TokenStore重写,其中关于用户详细信息的内容涉及到UserDetails,那在授权服务器这个项目重写配置后,你总不能把同样的UserDetails类又放到资源服务器项目上去,然后再重写一个相同的TokenStore,代码太耦合了,这样的做法肯定是不合适的。
再结合一下如果授权服务器和资源服务器如果在同一个项目可以访问相同的bean,而不需要在资源服务器额外配置tokenStore,只需要在授权服务器配置这个点,大家可以去思考下有没有更好地解决方式呢?这个我将在下一章通过redis重写TokenStore中提一提我的解决方案也是我在看了一些oauth项目解决方案初步总结出的。

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

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

相关文章

_Linux (ipc命令)

ipcs 查看进程间通信资源/ipcrm 删除进程间通信资源 -m 针对共享内存的操作 ipcs -mipcrm -m shmid(例如下图的5) -q 针对消息队列的操作 -s 针对信号量的操作 -a 针对所有资源的操作 key 唯一值(共享内存名字)shmid 共享内存标识owner 共享内存拥有者名字perms 拥有者对共…

ISP-ASF

1. 概述 1.1 高频与低频区分&#xff1a; 如何区分图像的高频信息和低频信息&#xff0c;所谓高频就是该像素点与周围像素差异较大&#xff0c;常见于一副图像的边缘细节和噪声等&#xff1b;而低频就是该像素点与周围像素差异变化不大&#xff0c;一般体现为图像的平坦区&am…

关于使用pytorch-lightning版本过低的一些问题

今天run了一下这篇Aspect Sentiment Quad Prediction as Paraphrase Generation论文的代码&#xff0c;遇到的都是pytorch-lightning版本问题。 首先是安装pytorch-lightning pip3 install pytorch-lightning -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.…

HTML简单的个人博客网站 DIV学生网页设计作品 dreamweaver作业静态HTML网页设计模板 个人网页作业制作

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

卷起来了!熬夜学习阿里P8全彩版并发编程图册,涨薪就在眼前

并发编程可以总结为三个核心问题&#xff1a;分工、同步、互斥。 并发编程可以总结为三个核心问题&#xff1a;分工、同步、互斥。所谓分工指的是如何高效地拆解任务并分配给线程&#xff0c;而同步指的是线程之间如何协作&#xff0c;互斥则是保证同一时刻只允许一个线程访问共…

一种基于物理信息极限学习机的PDE求解方法

**作者|**PINN山里娃&#xff0c;作者主页 **研究方向|**物理信息驱动深度学习 不确定性 人工智能 偏微分方程 极限学习机 该作者聚焦深度学习模型与物理信息结合前沿研究&#xff0c;提供了一系列AI for science研究进展报告及代码实现&#xff0c;旨在实现物理信息驱动深度学…

动态规划PTA总结

0动态规划 最优子结构&&最值问题&&重叠子问题 ---> 动态规划 引用别人的文章 1数字三角形 1.1题目 给定一个由 n行数字组成的数字三角形如下图所示。试设计一个算法&#xff0c;计算出从三角形 的顶至底的一条路径(每一步可沿左斜线向下或右斜线向下)&am…

HTML+CSS美食静态网页设计——简单牛排美食餐饮(9个页面)公司网站模板企业网站实现

&#x1f468;‍&#x1f393;静态网站的编写主要是用HTML DIVCSS JS等来完成页面的排版设计&#x1f469;‍&#x1f393;,常用的网页设计软件有Dreamweaver、EditPlus、HBuilderX、VScode 、Webstorm、Animate等等&#xff0c;用的最多的还是DW&#xff0c;当然不同软件写出的…

SAP MM 为UB类型的STO执行VL10B,报错-没有项目类别表存在(表T184L NL 0002 V)-之对策

SAP MM 为UB类型的STO执行VL10B&#xff0c;报错-没有项目类别表存在&#xff08;表T184L NL 0002 V)-之对策 业务人员创建好了UB类型的转储单据后&#xff0c;试图执行事务代码VL10B&#xff0c;未能成功&#xff0c;报错如下&#xff1a; 报错信息&#xff1a;4500000246 00…

【数据结构】——带头双向循环链表

目录 1.带头双向循环链表 2.链表实现 2.1可完成带头双向可循环链表节点的结构体 2.2申请一个可双向循环的节点 2.3初始化链表 2.4尾插 2.5尾删 2.6头插 2.7头删 2.8打印 2.9查找&#xff08;修改&#xff09; 2.10在pos之前插入x 2.11删除pos位置 2.12判空 2.13记…

Springboot图书馆管理系统毕业设计、Springboot图书借阅系统设计与实现 毕设作品参考

功能清单 【后台管理员功能】 广告管理&#xff1a;设置小程序首页轮播图广告和链接 留言列表&#xff1a;所有用户留言信息列表&#xff0c;支持删除 会员列表&#xff1a;查看所有注册会员信息&#xff0c;支持删除 资讯分类&#xff1a;录入、修改、查看、删除资讯分类 录入…

【毕业设计源码】基于微信小程序的校园第二课堂(课外活动)管理系统

该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程等学习内容。 目录 一、项目介绍&#xff1a; 二、文档学习资料&#xff1a; 三、模块截图&#xff1a; 四、开发技术与运行环境&#xff1a; 五、代码展示&#xff1a; 六、数据库表截图&#xff1a…

【数据库原理及应用】——数据库设计(学习笔记)

&#x1f4d6; 前言&#xff1a;数据库的设计是指基于现有的数据库管理系统&#xff0c;针对具体应用构建适合的数据库逻辑模式和物理结构&#xff0c;并据此建立数据库及其应用系统&#xff0c;使之能有效地存储和管理数据&#xff0c;满足各类用户的应用需求。本章将介绍数据…

PaddleOCR简单使用教程-Windows

说明 最近公司业务需要用到图文识别类似的功能&#xff0c;所以查阅了许多工具之后选择用百度开源的PaddleOCR来进行使用 先看官方简介: 百度飞桨PaddleOCR旨在打造一套丰富、领先、且实用的OCR工具库&#xff0c;助力开发者训练出更好的模型&#xff0c;并应用落&#xff0c;支…

Flink系列之Flink 流式编程模式总结

title: Flink系列 一、Flink 流式编程模式总结 1.1 基础总结 官网&#xff1a; https://flink.apache.org/ Apache Flink — Stateful Computations over Data Streams 三个任意&#xff1a; 任意的数据源 Source任意的计算类型 Transformation任务的数据目的地 Sink其中关于…

trt多流、多batch、多context

&#xff08;1&#xff09;一个engine可以创建多个context&#xff0c;一个engine可以有多个执行上下文&#xff0c;允许一组权值用于多个重叠推理任务。例如&#xff0c;可以使用一个引擎和一个上下文在并行CUDA流中处理图像。每个上下文将在与引擎相同的GPU上创建。 &#xf…

跨境电商市场也“内卷”,出海卖家如何破圈?

近些年来&#xff0c;跨境电商从业者都在调侃本行业越来越“卷”&#xff0c;大家需要铆足了劲竞争更有利的资源&#xff0c;以往的流量红利期似乎一去不复返了。事实上&#xff0c;很多跨境电商卖家对这种局势并不陌生&#xff0c;一些人甚至经历了国内电商从流量红利期至流量…

复习计算机网络——第三章记录(1)

数据链路层 功能&#xff1a;通过一些数据链路层的协议&#xff0c;在不太可靠的物理链路上实现可靠的数据传输。 相关基本概念&#xff1a; 1、结点&#xff08;node&#xff09;&#xff1a;网络中的主机&#xff08;host&#xff09;和路由器&#xff08;router&#xff…

JS实现简易计算器(input表单)

实现效果: 代码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"></head><body><p>整数1:<input type"text" id"num1"></p><p>整数2:<input type"text&q…

Kamiya丨Kamiya艾美捷抗BCMA单抗,克隆Vicky-2说明书

Kamiya艾美捷抗BCMA单抗化学性质&#xff1a; 同义词&#xff1a;B细胞成熟蛋白&#xff0c;TNFRSF 17&#xff0c;CD269。 特异性&#xff1a;识别鼠标BCMA。 物种反应性&#xff1a;老鼠不与人BCMA发生交叉反应。其他物种未经测试。 Ig同种型&#xff1a;大鼠IgG2a 免疫…