实战:单点登录的两种实现方式,你学会了吗?

news2024/11/16 5:50:47

概念

单点登录(Single Sign-OnSSO)是一种身份验证服务,允许用户使用单个标识来登录多个应用程序或系统。如下图所示,用户只需要用户名/密码登陆一次就可以访问系统A、系统B和系统C。

在传统的登录方式中,用户必须为每个应用程序或系统提供不同的凭据和密码。如下图所示,用户访问系统A、系统B和系统C都必须用用户名/密码登陆。

这种方式既不方便也容易被攻击者利用,而 SSO 解决了这个问题,使得用户只需通过一次身份验证就可以无缝地访问多个应用程序或系统,从而提高了用户体验的便利性和安全性。

单点登陆的优点

  • 用户体验改善:用户只需要登录一次,就可以访问多个系统或应用程序,不需要重复输入用户名和密码。这可以大大提高用户的工作效率。

  • 安全性增强:单点登陆可以提供更高级别的安全性,因为用户只需要在一个系统中进行身份验证,其他系统就可以共享这个身份验证信息。这可以有效地防止黑客入侵多个系统。

  • 管理更方便:单点登陆可以简化管理员的工作,因为它可以集中管理用户和权限。管理员可以在一个系统中管理多个系统的用户和权限,这样可以更方便地进行管理和维护。

单点登陆的实现方式

  • 共享身份验证:多个系统共享一个身份验证系统,用户只需要在一个系统中进行身份验证,就可以访问所有系统。这种方式需要建立一个共享的身份验证系统,这样可以保证用户信息的安全性。

  • 代理身份验证:一个系统代表其他系统进行身份验证,用户在登录时输入用户名和密码,然后其他系统会代表用户进行身份验证。这种方式需要建立一个代理系统,这样可以保证用户信息的安全性。

  • 基于令牌的身份验证:用户在登录后,会获得一个令牌,这个令牌可以在多个系统上进行身份验证。这种方式需要建立一个令牌管理机制,这样可以保证用户信息的安全性。

实战一

架构图

  1. 用户输入用户名/密码登陆 ServiceA 系统;

  2. 用户点击 ServiceA 系统中的某个按钮跳转到 ServiceB 系统,在跳转时需要带上 ServiceA 系统颁发的 ticket 票据;

  3. ServiceB 系统拿 ServiceA 系统的 ticket 去获取 ServiceA 系统的用户信息;

  4. ServiceA 系统会校验该 ticket 票据,然后将用户信息返回给 ServiceB 系统;

  5. ServiceB 系统根据用户信息生成 token 并附带重定向地址返回给 ServiceA 系统;

  6. ServiceA 系统就可以拿着获取的 token 去访问 ServiceB 系统的资源信息了。

代码实现

数据库

首先是初始化数据库,用户、公司等表依据自己的具体业务而定,此处不再赘述。提供公共的单点登陆信息表。

CREATE TABLE `sso_client_detail` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `platform_name` varchar(64) DEFAULT NULL COMMENT '应用名称',
  `platform_id` varchar(64) NOT NULL COMMENT '应用标识',
  `platform_secret` varchar(64) NOT NULL COMMENT '应用秘钥',
  `encrypt_type` varchar(32) NOT NULL DEFAULT 'RSA' COMMENT '加密方式:AES或者RSA',
  `public_key` varchar(1024) DEFAULT NULL COMMENT 'RSA加密的应用公钥',
  `sso_url` varchar(128) DEFAULT NULL COMMENT '单点登录地址',
  `remark` varchar(1024) DEFAULT NULL COMMENT '备注',
  `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
  `update_date` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
  `del_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '删除标志,0:正常;1:已删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='单点登陆信息表'

插入测试数据

INSERT INTO cheetah.sso_client_detail
(id, platform_name, platform_id, platform_secret, encrypt_type, public_key, sso_url, remark, create_date, create_by, update_date, update_by, del_flag)
VALUES(1, 'serviceA', 'A9mQUjun', 'Y6at4LexY5tguevJcKuaIioZ1vS3SDaULwOtXW63buBK4w2e1UEgrKmscjEq', 'RSA', NULL, 'http://127.0.0.1:8081/sso/url', NULL, '2023-05-23 16:55:26', 'system', '2023-05-30 13:16:16', NULL, 0);
  • platform_idplatform_secret,阿Q是使用 apache 的 commons-lang3 包下的RandomStringUtils.randomAlphanumeric()方法生成的。

  • sso_url就是上边提到的 ServiceB 系统的地址。

  • encrypt_typepublic_key在此方式中未使用,可以忽略。

细心的阿Q还给大家准备了一个接口,只需要传入 platformName 和 ssoUrl 就可以自动生成单点登陆信息。

接下来我们就进入真正的代码部分了,回复“sso”即可获取实战源码。

A跳转B

/**
 * com.itaq.cheetah.serviceA.controller.PortalController#jump
 * title:跳转 ServiceB
 * <pre>
 * 1. 前端点击Jump链接触发此接口调用
 * 2. 此接口生成ticket并携带着请求 ServiceB
 *      2.1 ServiceB拿着ticket请求我方服务获取用户信息
 *      2.2 ServiceB获取到我方用户信息并进行数据同步
 *      2.3 ServiceB返回一个链接,连接中带 token
 * 3. 重定向到返回的链接实现登录
 * </pre>
 *
 * @param req 单点跳转请求体
 * @return ServiceB单点登录地址
 */
@PostMapping("/jumpB")
public WrapperResult<String> jump(@RequestBody @Validated SsoJumpReq req) {
 log.debug("单点登录:{}", req.getPlatformName());
 //1、判断该平台名称是否存在
 SsoClientDetail one = iSsoClientDetailService.getOne(
   new LambdaQueryWrapper<SsoClientDetail>().eq(SsoClientDetail::getPlatformName, req.getPlatformName())
 );
 if (Objects.isNull(one)) {
  return WrapperResult.faild("不存在的app");
 }
 //2、校验本系统的 token,并从中获取用户信息
 /*
 * 示例
 * Result<Token> result = authorizationApi.checkToken(req.getToken());
 */

 //3、生成ticket,并将用户信息与其绑定存入redis
 String ticket = UUID.randomUUID().toString().replaceAll("-", "");
 UserInfo userInfo = new UserInfo();
 userInfo.setId(1L);
 userInfo.setUsername("阿Q");
 redisTemplate.opsForValue().set(RedisConstants.TICKET_PREFIX + ticket, userInfo, 5, TimeUnit.MINUTES);

 String ssoUrl = one.getSsoUrl();
 Map<String, Object> data = new HashMap<>(1);
 data.put("ticket", ticket);
 //4、发送http请求,把ticket通过设置好的ssoUrl传给ServiceB
 WrapperResult<SsoRespDto> ssoRespDto = HttpRequest
   .get(ssoUrl)
   .queryMap(data)
   .connectTimeout(Duration.ofSeconds(120))
   .readTimeout(Duration.ofSeconds(120))
   .execute()
   .asValue(new TypeReference<WrapperResult<SsoRespDto>>() {
   });
 log.info("请求ServiceB 结果:{}", JsonUtils.toPrettyString(ssoRespDto));
 return WrapperResult.success(ssoRespDto.getData().getRedirectUrl());
}

B获取票据,并请求A获取用户信息

/**
 * com.itaq.cheetah.serviceB.controller.SsoController#sso
 * 获取票据,并请求ServiceA 获取用户信息
 * @param ticket 票据
 * @return 返回地址供sso跳转
 * @throws JsonProcessingException 异常
 */
@GetMapping("/url")
public WrapperResult<SsoRespDto> sso(@RequestParam("ticket") String ticket) throws JsonProcessingException {
 log.info("收到票据:{}", ticket);
 //1.根据ticket换取ServiceA用户信息
 Map<String, Object> param = new HashMap<>(1);
 param.put("ticket", ticket);
 String ssoUrl = "http://localhost:8081/getUser";
 String s = HttpRequest
   .get(ssoUrl)
   .queryMap(param)
   .connectTimeout(Duration.ofSeconds(120))
   .readTimeout(Duration.ofSeconds(120))
   .execute()
   .asString();
 ObjectMapper objectMapper = new ObjectMapper();
 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
 WrapperResult<SsoUserInfo> ssoUserInfoWrapperResult = objectMapper.readValue(s, new TypeReference<WrapperResult<SsoUserInfo>>() {
 });
 log.info("ticket登录结果:{}", new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(ssoUserInfoWrapperResult));
 //2.获取到用户信息之后同步到本地数据库
 log.info("获取用户信息同步数据库");
 //3.生成token
 log.info("生成token");
 SsoRespDto respDto = new SsoRespDto();
 //4、将ServiceA要跳转的地址返给ServiceA并携带 ServiceB 的token
 respDto.setRedirectUrl("http://localhost:8082/index?token=123456");
 WrapperResult<SsoRespDto> success = WrapperResult.success(respDto);
 log.info(new ObjectMapper().writeValueAsString(success));
 return success;
}

A提供的获取用户信息接口

/**
 * com.itaq.cheetah.serviceA.controller.PortalController#loginByTicket
 * 根据票据获取用户信息
 * @param ticket 票据信息
 * @return 用户信息
 */
@ApiOperation("根据ticket获取用户信息")
@GetMapping("/getUser")
public WrapperResult<SsoUserInfo> loginByTicket(@RequestParam("ticket") String ticket) {
 log.info("收到票据:{}", ticket);
 UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(RedisConstants.TICKET_PREFIX + ticket);
 if (Objects.isNull(userInfo)) {
  return WrapperResult.faild("无法识别的票据信息");
 }
 //可能 userInfo 中只有少量的用户信息,此处省略了根据用户id查询用户和企业信息的过程,自行编写逻辑代码即可

 SsoUserInfo ssoUserInfo = new SsoUserInfo();
 BeanUtil.copyProperties(userInfo,ssoUserInfo);
 return WrapperResult.success(ssoUserInfo);
}

测试结果

实战二

架构图

这次我们用 ServiceB 系统单点登陆 ServiceA 方式:

  • 用户输入用户名/密码登陆 ServiceB 系统;

  • 用户点击 ServiceB 系统中的某个按钮跳转到 ServiceA 系统,在跳转时需要带上 ServiceB 系统加密后的用户信息;

  • ServiceA 系统拿到 ServiceB 系统加密后的用户信息后进行验签和解密操作;

  • ServiceA 系统将用户信息保存到本地并生成 token 返回给 ServiceB 系统;

  • ServiceB 系统拿到 ServiceA 系统返回的 token 就可以访问 ServiceA 系统的资源信息了;

代码实现

此种方式就用到了上边提到的数据库中的encrypt_typepublic_key字段,其中 public_key 是 ServiceA 给 ServiceB 提供的。为了演示方便直接在application.yml中进行配置。

B的配置

#本服务的appId和appSecret信息,该配置由serviceA提供
appId: A9mQUjun
appSecret: Y6at4LexY5tguevJcKuaIioZ1vS3SDaULwOtXW63buBK4w2e1UEgrKmscjEq

encrypt:
  #加密方式 RSA | AES
  type: RSA
  #该配置是serviceA单点登陆serviceB用到的,此处是serviceB单点serviceA,所以用不到
  #如果选择非对称加密,需要使用该配置;本服务的公私钥信息,该配置由serviceB自己生成,并将publicKey给serviceA
  rsa:
    publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KLYE2Tv4qx/duxu8Qvq5ZN58yEjj/uwsxfs96pj+9iOOAUKLur8IIKjR/bi54GICUy0BHO6dzpWc0xqGK170F9NTv0bHe0qbh7jHgzq9MJrfcVD+XZAH17ho5tCGIo+z7CiC+rMWGTqmRopd/EQuzfx4Op4/85hoPlpKxdcxAfys0jpZ9tBMtROPsYKhCz01iDnHV2K95s4UwaQLbbx0VALVaXv1/4Yjw/PW4xK0syW/nqUtVqpfwPuX+fHf+bJ2s4kLnFBNwYAKFSU6znGmtJuq6aoxCunu2PbzI8xc7SYxHEfDqG8Zp29wtZcTJecWSDMBmywlaXjkXLzapvE7QIDAQAB
    privateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQotgTZO/irH927G7xC+rlk3nzISOP+7CzF+z3qmP72I44BQou6vwggqNH9uLngYgJTLQEc7p3OlZzTGoYrXvQX01O/Rsd7SpuHuMeDOr0wmt9xUP5dkAfXuGjm0IYij7PsKIL6sxYZOqZGil38RC7N/Hg6nj/zmGg+WkrF1zEB/KzSOln20Ey1E4+xgqELPTWIOcdXYr3mzhTBpAttvHRUAtVpe/X/hiPD89bjErSzJb+epS1Wql/A+5f58d/5snaziQucUE3BgAoVJTrOcaa0m6rpqjEK6e7Y9vMjzFztJjEcR8Oobxmnb3C1lxMl5xZIMwGbLCVpeORcvNqm8TtAgMBAAECggEBAKMhoQfRFYxMSkIHbluFcP5eyKylDbRoHOp726pvDUx/L/x3XFYBIHCfFOKRFSvk6SQ0WFFe176f27a9Wfu/sh7kVYNcflZw+YsvFXCKsy/70KZ/lr24izy8KHuPSyf6+E/WkW32Ah9fkNtzTFdfIzDv9m1hiIijq0x9l5C87KjNELnbvC0I6vwFOx0ak+JBbpaJ7IRjZxKZup7UIPvt9nbLzcbKelI83An2JUe8HNhrfWxH9UIyMOBoAY+bKCuAbUtHqSlImPiWyiCwE2/Fh7dmPSOAYYp9aZelnhd25jlR+eh4yaUoIID9ubmYVYbjcPW5SSNdfSZMfQ3oa79QeRUCgYEA6K4L+VLRiX8Dg7NCO1fM2+FTv2csTkPX6n7z/uu7kh0+wQDws+/C6Q906OtizvJBIJqFm2jPACNQCvnRixY1srgMJJlH/Rpeb4LtZGwdM1k0jAZIYQcBlGfaq3RaRI/+6+T0xdsh+7VF5A/smp/VXdK2xI3+JbLQ2wm9uN+3yZcCgYEA5Yvly7veDJYf2+8HIQkRhjWrWm1y5lCSe+HG+1ktfqnhN8YEOiPa71u0TXealL0T8EoKsqhWEjomxZ7n0jLigogz7OxxsGAE6HXAiKX0REINNYrq+1qNaqmkfLrhAJyg3JNgTSlb0xd56w7FSqOBttVL9INawGb1P98kYc5OzhsCgYBEfIY1urTGPcZxC2BhSzSXO7mEyv91ge6ZrQhwbj5lgYopEPfIXrgGFXCZ5j7NHu0ghZrx5WWYasxyjpmo0L65fgbE9wEDdLF7LRRmzJPDu2wGEwtW09MZNYBdmv++0ot8L4YEfr1/8xlBSZag5I7O8Oiu7gRyYDGtZy6are7QvQKBgQCaUZnUhOF7/rU+a4yUZf9VBeHD8k7LjaFdDWVzdvmB7P1PPJ185Lv8LN+jMORIWHD+GxjkEQ2ERXnpY7If+zuSW7Tk8/Reib7i9L7SXxc/iFRPCax9/NuTuKavgAdiHOp8P8v/M+3alS7OmuiCDDhZTT46DNDHBrCcFwzjgAo0vwKBgECBs6hEUVsYU74Uc64he8Zgkvj7wZ/yxnFlWmRRERprfBsuiY/y+DAf5ehezSRFpHXUrAkpeVXq2ydnr9BKTs6TV3AxlDMBNSndXsUYHENncR7tEHCSGRFTTu5jxdYA+k47R865Jh+2vQvPaPaXsEKSkDegvcFeUVR/yi5AsDub

#如果选择非对称加密,则需要使用该配置,该配置由serviceA提供
serviceA:
  rsa:
    publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2BF9EZCscKNXYADtulIDNHaMnoxV5Yu91jpv+LiWabW2EO51b8Sx8+Ei59EebM4r+SMal0k4L2Z+cNagQSP4Wvpss82/MkGO8bnAFSxS2SOKw+a+c2PxByWUxvHo4pbyYGFVWAGDXLiI+IqiO/fEFfpy6rYQzMLDnfgMFngdS4AZmRyTdMKbQs8mWqBE5nC0PoU39o/lFowfgelEjHE9vhjtTha67KhYY3n+ueuxsYdRQ40Mg7aQ0+Kt/qKoSn9yRWyx09DheFAkYl4ZCQfd0sMotLQ4BZtk0YWMNHOc1w+fL1bOumaj7AaJi6nM/VvwylLJyia2GjJIDrdTfHiOnwIDAQAB

A的配置

serviceA:
  rsa:
    privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDYEX0RkKxwo1dgAO26UgM0doyejFXli73WOm/4uJZptbYQ7nVvxLHz4SLn0R5sziv5IxqXSTgvZn5w1qBBI/ha+myzzb8yQY7xucAVLFLZI4rD5r5zY/EHJZTG8ejilvJgYVVYAYNcuIj4iqI798QV+nLqthDMwsOd+AwWeB1LgBmZHJN0wptCzyZaoETmcLQ+hTf2j+UWjB+B6USMcT2+GO1OFrrsqFhjef6567Gxh1FDjQyDtpDT4q3+oqhKf3JFbLHT0OF4UCRiXhkJB93Swyi0tDgFm2TRhYw0c5zXD58vVs66ZqPsBomLqcz9W/DKUsnKJrYaMkgOt1N8eI6fAgMBAAECggEAA5f23o3rcEwnLd+WFJ08lGjMWe63lwPF+oQqTJa1Wbi9+HYe2ecJlqbN79EYknKzZIdi79U17APmYnYPYEX64Xh8yljHr0xL1lVijneYQShILI3v6PdmkNndKZnoZ6xfB59WzgnoZ2hiTs/vdtPeHQd3VdQFX4J1wnDXsp/4zMKi1fDPt7rhqWrP5W6PXcoGGKIkN9zBlqrd1RBdnKXcwfFoHcFf2ikk6g3Kn50YMRe324eiHMm8z7W34Y3iSvZYHcKBMgsDklFerw1WOGHTN61oMr+8/NTtCsy1AnCH4PrwX/ryO17mh5xNzo/ZSZRRezR92/hmwUIuOO+3FWIE4QKBgQD05wYMVlGKn1fm+sn4hn+ErC6NifXj3MkNdjs8oSHzLrYr6ea6xIvbxesZvqzqz1Fh68bHjpJPOBKwgFnl7+dLXYLNmKjry1iK0o/MMZTtrGUwMEnWHRrpmxXH6B0cnBecZUReuJ9XfKZIfd9ksHHsUY7IGv1CHcblVP/IhrpnxwKBgQDh2/n0cAh1jygGevlXGK/rxuRSlbVgtxJWLAtY8Yolf2BklSiTwmqtp7nzNn8sxRvgfQCZaLqpjC/o/wtC3Ba5b4StJQejoXkCNhVmRdLbIQ2tUxwAElPjFhWf3C5/4B6uBeLyC9izp4wTSYbNbPKxcUGkkfpPbWdHsFZOG4gSaQKBgA/me/cLF6o3ZD6j478WBGt5vmAEKAnOSONt3LS4BXtDeiJpwkg4AJiZRgVa4uEv6qm/5B0KvacVDemVu8B5DfxPqvFsSvNcNXh16U4pnfC8c6loSTL0ms21+vkKsfEslT/bN1ArDnVgq28jdQCVkB/2v51wWycSxdoX5a+AR9P7AoGAMvTwZefI4M0VmLCyBKZ7OlS7Oq6wJ0vmhS6WuNB1/JPKaacFaqDYdKl82JSZCL7H1VQeiH4KbypDvOud3M3PCrNQWcga+x35MTiGh3aFZg8FCO/RR2rbJkbbRh/lFdC420ZUt4tYrt/ESK20DjDgaIxG5RxSPw1N2ey87A5mGtECgYEAlA12yuxBb6qmG3OUSlacSfcKnxZIC3L1IMqxlXL8eG3MB4dI6QYesc3odmaxmy9csgHs+pTyLfM3yB9Ocl572OW5WcEnod5o1EIup9hxB4IG/xSECYVFHlGKfIgbd/JhWtqloYZrwx+kVX/Iw02z18R32DRqBtK4MQ3klOYH86s=

B跳转A并加密用户信息

/**
 * com.itaq.cheetah.serviceB.controller.ToServiceAController#redirectToServiceA
 * 跳转 ServiceA 服务
 *
 * @return ServiceA返回的重定向链接
 */
@GetMapping
public WrapperResult<String> redirectToServiceA() {
 //1、构建用户信息
 SsoUserInfo data = buildSsoUserInfo();
    
 Long timestamp = System.currentTimeMillis();
 String flowId = UUID.randomUUID().toString();
 String businessId = "sso";
 String dataEncrypt;
 String encryptType = configProperties.getEncryptType();
 //2、根据配置选择哪种方式加密
 switch (encryptType) {
  case "AES":
   AES aes = new AES(configProperties.getAppSecret().getBytes(StandardCharsets.UTF_8));
   dataEncrypt = aes.encryptBase64(JsonUtils.toString(data), StandardCharsets.UTF_8);
   break;
  case "RSA":
   RSA rsa = new RSA(AsymmetricAlgorithm.RSA_ECB_PKCS1.getValue(), null, configProperties.getServiceAPublicKey());
   dataEncrypt = rsa.encryptBase64(JsonUtils.toString(data), StandardCharsets.UTF_8, KeyType.PublicKey);
   break;
  default:
   return WrapperResult.faild("未配置加密方式");
 }
 //3、将以下信息进行签名
 SsoSignSource build = SsoSignSource.builder()
   .platformId(configProperties.getAppId())
   .platformSecret(configProperties.getAppSecret())
   .businessId(businessId)
   .data(dataEncrypt)
   .flowId(flowId)
   .timestamp(timestamp)
   .build();
 String sign = build.sign();
 log.info("sign source={}", JsonUtils.toPrettyString(build));

 //4、构建请求体
 ToServiceAReq req = ToServiceAReq.builder()
   .platformId(configProperties.getAppId())
   .businessId("sso")
   .flowId(flowId)
   .timestamp(timestamp)
   .sign(sign)
   .data(dataEncrypt)
   .build();

    //5、跳转A的操作
 String s = HttpRequest.post("http://localhost:8081/serviceA")
   .bodyString(JsonUtils.toString(req))
   .execute()
   .asString();
 log.info("结果:{}", s);
 return WrapperResult.success(s);
}

A获取用户信息后续操作

/**
 * com.itaq.cheetah.serviceA.controller.ServiceAController#sso
 *
 * @return
 */
@PostMapping
public WrapperResult<SsoRespDto> sso(@VerifySign ToServiceAReq req) {
 log.info("收到单点登录ServiceA的请求:{}", JsonUtils.toPrettyString(req));
 //同步用户信息

 //模拟登陆生成token

 //返回拼接的url?token=xxx
 //返回拼接的url?token=xxx
    String url ="127.0.0.1:8081/index?token=xxx";
    SsoRespDto ssoRespDto = new SsoRespDto();
    ssoRespDto.setRedirectUrl(url);
    return WrapperResult.success(ssoRespDto);
}

测试

补充知识

本文中用到的 RSA 的密钥是通过在线网站https://www.bchrt.com/tools/rsa/生成的,当然大家也可以使用 hutool 中的 RSA 类来生成,也可以使用 java 自带的 security 来生成。

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

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

相关文章

HTML5 progress和meter控件

在HTML5中&#xff0c;新增了progress和meter控件。progress控件为进度条控件&#xff0c;可表示任务的进度&#xff0c;如Windows系统中软件的安装、文件的复制等场景的进度。meter控件为计量条控件&#xff0c;表示某种计量&#xff0c;适用于温度、重量、金额等量化的表现。…

【CSS】文字扫光 | 渐变光

码来 可调整角度与颜色值来改变效果 <p class"gf-gx-color">我是帅哥</p> <style>.gf-gx-color {background: -webkit-linear-gradient(135deg,red,red 25%,red 50%,#fff 55%,red 60%,red 80%,red 95%,red);-webkit-text-fill-color: transparen…

中国物流成本高在哪里?怎么降低?

随着中国经济的快速发展&#xff0c;物流行业也得到了快速发展。然而&#xff0c;尽管中国物流行业的规模已经达到了世界领先水平&#xff0c;但中国物流成本也一直是业内关注的一个问题。那么&#xff0c;中国物流成本高在哪里&#xff1f;怎么降低呢&#xff1f;本文将从多个…

Surface渲染流程解析:如何实现车载智能座舱的高质量图像显示?

SurfaceFlinger简介 SurfaceFlinger是Android系统中负责图形渲染和显示的一个系统服务&#xff0c;SurfaceFlinger负责将来自多个应用程序的屏幕缓冲区组合成单个屏幕缓冲区&#xff0c;并将最终结果输出到系统的显示设备上。SurfaceFlinger在Android系统中是一个非常重要的服…

大数据分析案例-基于逻辑回归算法构建心脏病发作预测模型

🤵‍♂️ 个人主页:@艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞👍🏻 收藏 📂加关注+ 喜欢大数据分析项目的小伙伴,希望可以多多支持该系列的其他文章 大数据分析案例合集…

C++基础:二维费用的背包问题

注意&#xff1a;如果你还没搞定&#xff08;指的是真正理解&#xff09;01背包&#xff0c;请不要看。看了脑壳更晕 什么是二维费用的背包问题&#xff1f;请看AcWing上的一道题&#xff1a; 有 N 件物品和一个容量是 V 的背包&#xff0c;背包能承受的最大重量是 M。 每件物…

【几分醉意赠书活动 - 05期】 | 《编程语言系丛图书》

个人主页&#xff1a; 陈老老老板的博客主页传送门 几分醉意.的博客主页传送门 赠书活动 | 第五期 本期好书推荐&#xff1a;《编程语言系列丛书》 粉丝福利&#xff1a;书籍赠送&#xff1a;共计送出30本 参与方式&#xff1a;关注公众号&#xff1a;码上天空 回复关键词&…

如果高考要考编程的话?不敢想,不敢想......

前几天不是高考嘛。 高考确实是当前时代下&#xff0c;比较公平的一个比武台了。说是人生中一次逆天改命的机会我觉得也不为过。 毋庸置疑&#xff0c;高考确实非常重要。但是其实我站在现在这个时间点&#xff0c;距离高考已经过去了 多年时间&#xff0c;回望这段经历的时候…

JavaWeb(HTML/CSS)

一.web概念概述 JavaWeb&#xff1a; 使用Java语言开发基于互联网的项目 做什么&#xff1a;做网页&#xff0c;其架构有; C/S: Client/Server 客户端/服务器端 是&#xff1a;在用户本地有一个客户端程序&#xff0c;在远程有一个服务器端程序 优点&#xff1a;对于用户来说…

Java入门 —— 打开Java世界的大门

目录 一. 了解Java 二.Java入门 三.变量 一. 了解Java 1.什么是程序 ——计算机执行某些操作或解决某个问题而编写的一系列有序指令的集合 2.Java是如何诞生的 ——1990年&#xff0c;sun公司启动了绿色计划&#xff0c;1992年sun公司创建了oak语言&#xff0c;后改名为Java…

Vue.js中的两大指令:v-on和v-bind,实现页面动态渲染和事件响应

Vue.js中的两大指令&#xff1a;v-on和v-bind&#xff0c;实现页面动态渲染和事件响应 一、Vue指令&#xff08;一&#xff09;v-bind指令&#xff08;二&#xff09;v-on指令1. 基本使用&#xff08;1&#xff09;最基本的语法 2. Vue中获取事件对象(了解)3. v-on 事件修饰符4…

Vue 利用Canvas标签实现动态验证码校验(前端必备附源码)

文章目录 一、前言二、Canvas简介- 什么是Canvas?- Canvas的基本使用 三、动态验证码的具体实现- 在项目中创建 SIdentify.vue 文件- 再创建要使用该组件的文件&#xff0c;App.vue 一、前言 当我们在平时进行登录或者注册账号的时候&#xff0c;往往都会遇到验证动态验证码的…

高级网工必会组网方案,你pick哪一种?

企业组网非常考验网工的内功&#xff0c;选择何种组网方案、合适的网关位置、如何保证网关可靠性等等一系列问题&#xff0c;每个环节都考验网工的理论功底和实操经验。 大型企业如大型医院、银行、省市县政府单位、电厂、汽车行业等&#xff0c;网络的稳定性&#xff0c;往往…

JUC高级-0608

重新看JUC课程&#xff0c;选择周阳讲的JUC 1.前置知识 lombok插件 Lombok是一个Java库&#xff0c;它通过注解的方式&#xff0c;能够在编译时自动为类生成构造函数、getters、setters、equals、hashCode和toString方法&#xff0c;以及其他常用方法&#xff0c;从而使我们…

Monocle3个性化分析作图:拟时热图/拟时基因GO分析/拟时基因趋势分析

Mnocle3往期精彩内容&#xff0c;因为monocle2有问题&#xff0c;且官网也放弃了monocle2&#xff0c;目前用的比较主流的拟时方法就是monocle3了。Mnocle3我们也写过全面的内容&#xff0c;不论是基础的分析还是个性化分析&#xff1a;Monocle3&#xff08;1&#xff09;&…

【C#】并行编程实战:并行编程简介

本章内容为多线程编程入门知识&#xff0c;旨在介绍多线程的特点&#xff0c;以及提供了C#部分基础的多线程API使用。 1、进程与线程 这一小节包含大量概念和基础知识&#xff0c;虽然建议阅读但确实比较枯燥。 可以直接跳到后面的实际应用的章节。 进程 狭义定义&#xff1a;正…

2.数据表的基本操作

SQL句子中语法格式提示&#xff1a; 1.中括号&#xff08;[]&#xff09;中的内容为可选项&#xff1b; 2.[&#xff0c;...]表示&#xff0c;前面的内容可重复&#xff1b; 3.大括号&#xff08;{}&#xff09;和竖线&#xff08;|&#xff09;表示选择项&#xff0c;在选择…

思科(Cisco)7000交换机软件版本升级步骤

思科&#xff08;Cisco&#xff09;交换机软件版本升级步骤 一、准备软件版本 在思科官方网站&#xff08;思科官网传送门&#xff09;下载你需要的系统版本文件&#xff0c;将软件版本准备好拷贝到U盘。 二、准备设备 将交换机加电启动&#xff0c;通过CRT- console进行连接…

使用JMeter进行接口高并发测试

一般的网络接口测试&#xff0c;功能性测试postman较为好用&#xff0c;需要测试高并发的情况下&#xff0c;可以用Jmeter来进行测试&#xff0c;postman是串行&#xff0c;而Jmeter可以多线程并行测试。 官网 Apache JMeter - Apache JMeter™正在上传…重新上传取消https://j…

100个句子记3500个单词

Typical of the grassland dwellers of the continent is the American antelope, or pronghorn. [ˈtɪpɪkl]典型[ˈɡrɑːslnd]草原[dweləz]居民[ˈkɒntɪnənt]大陆 [ˈntɪləʊp] [prɒŋhɔːn] 1.美洲羚羊&#xff0c;或称叉角羚&#xff0c;是该大陆典型的草原动物…