微服务Redis-Session共享登录状态
一、背景
随着项目越来越大,需要将多个服务拆分成微服务,使代码看起来不要过于臃肿,庞大。微服务之间通常采取feign交互,为了保证不同微服务之间增加授权校验,需要增加Spring Security登录验证,为了多个服务之间session可以共享,可以通过数据库实现session共享,也可以采用redis-session实现共享。
本文采取Spring security做登录校验,用redis做session共享。实现单服务登录可靠性,微服务之间调用的可靠性与通用性
二、代码
本文项目采取 主服务一服务、子服务二 来举例
1、服务依赖文件
主服务依赖
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.5.4'implementation group: 'org.springframework.session', name: 'spring-session-data-redis', version: '2.4.1'implementation(group: 'io.github.openfeign', name: 'feign-httpclient')implementation 'org.springframework.boot:spring-boot-starter-security'
子服务依赖
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.5.4'implementation group: 'org.springframework.session', name: 'spring-session-data-redis', version: '2.4.1'implementation 'org.springframework.boot:spring-boot-starter-security'
2、服务配置文件
主服务配置文件
#redis连接
spring.redis.host=1.2.3.4
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码
spring.redis.password=password
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000
#数据库
spring.redis.database=0
#redis-session配置
spring.session.store-type=redis
#部分post请求过长会报错,需加配置
server.tomcat.max-http-header-size=65536
子服务配置文件
#单独登录秘钥
micService.username=service
micService.password=aaaaaaaaaaaaa
#登录白名单
micService.ipList=1.2.3.4,1.2.3.5,127.0.0.1,0:0:0:0:0:0:0:1
spring.redis.host=1.2.3.4
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码
spring.redis.password=password
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000
#数据库
spring.redis.database=0
#最大请求头限制
server.maxPostSize=-1
server.maxHttpHeaderSize=102400
#redis session缓存
spring.session.store-type=redis
server.servlet.session.timeout=30m
3、登录校验文件
主服务SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {//注入密码加密的类@Beanpublic AuthenticationProvider authenticationProvider() {AuthenticationProvider authenticationProvider = new EncoderProvider();return authenticationProvider;}@Autowiredpublic void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);auth.authenticationProvider(authenticationProvider());}public static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().successHandler((request,response,authentication) -> {HttpSession session = request.getSession();session.setAttribute("TaobaoUser",authentication.getPrincipal());ObjectMapper mapper = new ObjectMapper();response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();TaobaoUser user = (CurrentUser)session.getAttribute("TaobaoUser");out.write(mapper.writeValueAsString(user));out.flush();out.close();}).failureHandler((request,response,authentication) -> {ObjectMapper mapper = new ObjectMapper();response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();out.write(mapper.writeValueAsString(new ExceptionMessage("400",authentication.getMessage())));out.flush();out.close();}).loginPage("/Login.html").loginProcessingUrl("/login").and().authorizeRequests().antMatchers("/api/common/invalidUrl","/**/*.css", "/**/*.js", "/**/*.gif ", "/**/*.png ", "/**/*.jpg", "/webjars/**", "**/favicon.ico", "/guestAccess", "/Login.html","/v2/api-docs","/configuration/security","/configuration/ui","/api/common/CheckLatestVersionInfo").permitAll().anyRequest()//任何请求.authenticated().and().sessionManagement().maximumSessions(-1).sessionRegistry(sessionRegistry());;http.csrf().disable();}@Autowiredprivate FindByIndexNameSessionRepository sessionRepository;@Beanpublic SpringSessionBackedSessionRegistry sessionRegistry(){return new SpringSessionBackedSessionRegistry(sessionRepository);}}
EncoderProvider.java
@Service
public class EncoderProvider implements AuthenticationProvider {public static final Logger logger = LoggerFactory.getLogger(EncoderProvider.class);/*** 自定义验证方式*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {try {//支持用户名和员工号登录 可能为用户名或员工号String username = authentication.getName();String credential = (String) authentication.getCredentials();//加密过程在这里体现TaobaoUser user= userService.getUserData(username);//校验,用户名是否存在if(user==null){throw new DisabledException("用户名或密码错误");}//校验登录状态checkPassword()Collection<GrantedAuthority> authorities = new ArrayList<>();return new UsernamePasswordAuthenticationToken(userCurrent, credential, authorities);} catch (Exception ex) {ex.printStackTrace();throw new DisabledException("登录发生错误 : " + ex.getMessage());}}@Overridepublic boolean supports(Class<?> arg0) {return true;}
子服务SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {//注入密码加密的类@Beanpublic AuthenticationProvider authenticationProvider() {AuthenticationProvider authenticationProvider = new EncoderProvider();return authenticationProvider;}@Autowiredpublic void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(authenticationProvider());}public static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);@Overrideprotected void configure(HttpSecurity http) throws Exception {logger.info("用户登录日志test1 username:"+http.toString());http.formLogin().loginProcessingUrl("/login").successHandler((request,response,authentication) -> {HttpSession session = request.getSession();session.setAttribute("TaobaoUser",authentication.getPrincipal());ObjectMapper mapper = new ObjectMapper();response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();TaobaoUser user = (TaobaoUser )session.getAttribute("TaobaoUser");out.write(mapper.writeValueAsString(user));out.flush();out.close();}).failureHandler((request,response,authentication) -> {ObjectMapper mapper = new ObjectMapper();response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();out.write(mapper.writeValueAsString(new ExceptionMessage("400",authentication.getMessage())));out.flush();out.close();}).loginPage("/Login.html").and().authorizeRequests().antMatchers("/**/*.css", "/**/*.js", "/**/*.gif ", "/**/*.png ","/**/*.jpg", "/webjars/**", "**/favicon.ico", "/Login.html","/v2/api-docs","/configuration/security","/configuration/ui").permitAll().anyRequest().authenticated().and().sessionManagement().maximumSessions(-1).sessionRegistry(sessionRegistry());http.csrf().disable();}@Autowiredprivate FindByIndexNameSessionRepository sessionRepository;@Beanpublic SpringSessionBackedSessionRegistry sessionRegistry(){return new SpringSessionBackedSessionRegistry(sessionRepository);}}
EncoderProvider.java
@Service
public class EncoderProvider implements AuthenticationProvider {public static final Logger logger = LoggerFactory.getLogger(EncoderProvider.class);@Value("${service.username}")private String userName1;@Value("${service.ipList}")private String ipList;/*** 自定义验证方式*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {try {//支持用户名和员工号登录 可能为用户名或员工号String username = authentication.getName();String credential = (String)authentication.getCredentials();TaobaoUser user=new TaobaoUser();if(username.equals(userName1)){List<String> ips = Arrays.asList(ipList.split(","));WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();String remoteIp = details.getRemoteAddress();logger.info("ip为{}-通过用户{}调用接口",remoteIp,username);if(!ips.contains(remoteIp)){throw new DisabledException("无权登陆!");}}else{throw new DisabledException("账户不存在!");}user.setA("A");Collection<GrantedAuthority> authorities = new ArrayList<>();return new UsernamePasswordAuthenticationToken(currentUser, credential, authorities);} catch (Exception ex) {ex.printStackTrace();throw new DisabledException("登录发生错误 : " + ex.getMessage());}}@Overridepublic boolean supports(Class<?> arg0) {return true;}}
4、主服务feign配置
FeignManage.java
#url = "${file.client.url}",
@FeignClient(name="file-service",fallback = FeignFileManageFallback.class,configuration = FeignConfiguration.class)
public interface FeignFileManage {@RequestMapping(value = "/file/upload", method = {RequestMethod.POST}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)ApiBaseMessage fileUpload(@RequestPart("file") MultipartFile file, @RequestParam("fileName") String fileName) ;}
public class FeignManageFallback implements FeignManage{@Overridepublic ApiBaseMessage fileUpload(MultipartFile file, String type) {return ApiBaseMessage.getOperationSucceedInstance("400","失败");}}
FeignFileManageFallback.java
FeignConfiguration.java
@Configuration
@Import(FeignClientsConfiguration.class)
public class FeignConfiguration {/**删除请求头文件*/final String[] copyHeaders = new String[]{"transfer-encoding","Content-Length"};@Beanpublic FeignFileManageFallback echoServiceFallback(){return new FeignFileManageFallback();}@Beanpublic FeignBasicAuthRequestInterceptor getFeignBasicAuthRequestInterceptor(){return new FeignBasicAuthRequestInterceptor();}/*** feign 调用,添加CurrentUser*/private class FeignBasicAuthRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {//1、使用RequestContextHolder拿到刚进来的请求数据ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes != null) {HttpServletRequest request = requestAttributes.getRequest();Enumeration<String> headerNames = request.getHeaderNames();if (headerNames != null) {while (headerNames.hasMoreElements()) {String name = headerNames.nextElement();String values = request.getHeader(name);//删除的请求头if (!Arrays.asList(copyHeaders).contains(name)) {template.header(name, values);}}}}else{template.header("Accept", "*/*");template.header("Accept-Encoding", "gzip, deflate, br");template.header("Content-Type", "application/json");}//增加用户信息if(requestAttributes!=null && SessionHelper.getCurrentUser()!=null){try {template.header("TaobaoUser", URLEncoder.encode(JSON.toJSONString(SessionHelper.getCurrentUser()),"utf-8") );} catch (UnsupportedEncodingException e) {e.printStackTrace();}}}}}
5、主服务session文件
SessionUtil.java
public class SessionUtil {public static CurrentUser getCurrentUser() {HttpSession session = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession();CurrentUser user = (CurrentUser)session.getAttribute("TaobaoUser");return user;}public static void setCurrentUser(String userName){HttpSession session = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession();Collection<GrantedAuthority> authorities = new ArrayList<>();session.setAttribute("TaobaoUser",new CurrentUser(userName, "", authorities));}
}
三、完成配置后
1、直接访问主服务接口,不登录无法访问
2、直接访问自服务,不登录无法访问,(可通过nacos配置用户密码实现登录)
3、主服务通过feign调用子服务接口正常(session已共享)
4、子服务登陆之后,调用主服务理论也可以,需校验下主服务用户侧
相关文章:
微服务Redis-Session共享登录状态
一、背景 随着项目越来越大,需要将多个服务拆分成微服务,使代码看起来不要过于臃肿,庞大。微服务之间通常采取feign交互,为了保证不同微服务之间增加授权校验,需要增加Spring Security登录验证,为了多个服务…...
30道C++ 基础高频题整理(附答案背诵版)
1. C和C有什么区别? C是C语言的超集(我看网上很多文章说这是不对的),这意味着几乎所有的C程序都可以在C编译器中编译和运行。然而,C引入了许多新的概念和特性,使得两种语言在一些关键点上有显著的区别。 …...
【Spark面试】Spark面试题答案
目录 1、spark的有几种部署模式,每种模式特点?(☆☆☆☆☆) 2、Spark为什么比MapReduce块?(☆☆☆☆☆) 3、简单说一下hadoop和spark的shuffle相同和差异?(☆☆☆☆☆…...
Axure的动态面板
目录 动态面板 什么是Auxre动态模板 动态模板的步骤 应用场景 实战案例 轮播图 多功能登录界面 主界面左侧菜单栏 动态面板 什么是Auxre动态模板 动态面板是Axure中的一个重要功能,它允许用户创建可交互的页面,并模拟用户与页面的交互。通过添加元素…...
【STM32】STM32学习笔记-对射式红外传感器计次 旋转编码器计次(12)
00. 目录 文章目录 00. 目录01. NVIC相关函数1.1 NVIC_PriorityGroupConfig函数1.2 NVIC_PriorityGroup类型1.3 NVIC_Init函数1.4 NVIC_InitTypeDef类型 02. 外部中断相关API2.1 GPIO_EXTILineConfig2.2 EXTI_Init2.3 EXTI_GetITStatus2.4 EXTI_ClearITPendingBit2.5 中断回调函…...
后端项目操作数据库-中枢组件Service调用Mapper实现增删改查-实例
接上篇 使用MyBatis配置Mapper实现增删改查 1.Service的基本作用 Service在代码中的的作用是调用Mapper、被Controller调用。是后端项目中非常重要的组件。 用于设计业务流程、业务逻辑,以保障数据的完整性、有效性、安全性。 2. Service使用举例——“添加相册”…...
kafka学习笔记--节点的服役与退役
本文内容来自尚硅谷B站公开教学视频,仅做个人总结、学习、复习使用,任何对此文章的引用,应当说明源出处为尚硅谷,不得用于商业用途。 如有侵权、联系速删 视频教程链接:【尚硅谷】Kafka3.x教程(从入门到调优…...
2023-12-16:用go语言,给定整数数组arr,求删除任一元素后, 新数组中长度为k的子数组累加和的最大值。 来自字节。
2023-12-16:用go语言,给定整数数组arr,求删除任一元素后, 新数组中长度为k的子数组累加和的最大值。 来自字节。 答案2023-12-16: 来自左程云。 灵捷3.5 大体步骤如下: 算法 maxSum1 分析࿱…...
libxls - 编译
文章目录 libxls - 编译概述笔记静态库工程测试控制台exe工程测试备注备注END libxls - 编译 概述 想处理.xls格式的excel文件. 查了一下libxls库可以干这个事. 库地址 https://github.com/libxls/libxls.git 但是这个库的makefile写的有问题, 在mingw和WSL下都编译不了. 好在…...
自建私有git进行项目发布
自建私有git进行博客项目发布 之前尝试过通过建立私有git仓库,来发布自己的hexo静态博客,但是失败了,今天尝试了一下午,算是有了结果。下面记录我的过程。 我的需求: 我有一个服务器,希望在服务器端建一…...
华为HCIP认证H12-821题库上
1、2.OSPF核心知识 (单选题)下面关于0SPF的特殊区域,描述错误的是: A、Totally Stub Area允许ABR发布缺省的三类LSA,不接受五类LSA和细化三类LSA B、NSSA Area和Stub区域的不同在于该区域允许自治系统外部路由的引入,由ABR发布…...
Web安全漏洞分析—文件包含
在当今数字化时代,随着Web应用程序的广泛应用,网络安全问题愈加凸显。其中,文件包含漏洞作为一种常见但危险的安全隐患,为恶意攻击者提供了可乘之机。在这篇博客中,我们将深入探讨文件包含漏洞的本质、攻击手法以及应对…...
C++入门【9-C++循环】
C 循环 有的时候,可能需要多次执行同一块代码。一般情况下,语句是顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推。 编程语言提供了允许更为复杂的执行路径的多种控制结构。 循环语句允许我们多次…...
Python3 数字(Number) ----20231215
Python3 数字(Number) # Python3 数字(Number)# Python 数字数据类型用于存储数值。 # 数据类型是不允许改变的,这就意味着如果改变数字数据类型的值,将重新分配内存空间。# 以下实例在变量赋值时 Number 对象将被创建: var1 = 1 var2 = 10# 您也可以使用del语句删除一些数…...
PyQt6 QToolBar工具栏控件
锋哥原创的PyQt6视频教程: 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计44条视频,包括:2024版 PyQt6 Python桌面开发 视频教程(无废话版…...
nodejs+vue+微信小程序+python+PHP基于大数据的银行信用卡用户的数仓系统的设计与实现-计算机毕业设计推荐
银行信用卡用户的数仓系统综合网络空间开发设计要求。目的是将银行信用卡用户的数仓系统从传统管理方式转换为在网上管理,完成银行信用卡用户的数仓管理的方便快捷、安全性高、交易规范做了保障,目标明确。银行信用卡用户的数仓系统可以将功能划分为管理…...
EMC RI/CI测试方案助您对抗电磁设备干扰!
方案背景 电磁或射频干扰的敏感性,会给工程师带来重大的风险和安全隐患。尤其是在工业、船用和医疗设备环境。这些环境系统中的控制、导航、监控、通信和警报等关键零部件必须具备电磁抗扰水平,以确保系统始终正常运行。 抗扰系统测试方案一般分为传导…...
【机器学习】数据降维
非负矩阵分解(NMF) sklearn.decomposition.NMF找出两个非负矩阵,即包含所有非负元素(W,H)的矩阵,其乘积近似于非负矩阵x。这种因式分解可用于例如降维、源分离或主题提取。 主成分分析(PCA) sklearn.decomposition.PCA使用数据的奇异值分解…...
vue3路由跳转及传参
1.创建项目及路由 1.1 创建文件时记得勾选上vue-router,没有勾选也没有关系 // vue3安装命令 npm create vuelatest // 以下选项可根据自己所需,进行选择,不懂就翻译 ✔ Project name: … <your-project-name> ✔ Add TypeScript? …...
cesium 自定义贴图,shadertoy移植教程。
1.前言 cesium中提供了一些高级的api,可以自己写一些shader来制作炫酷的效果。 ShaderToy 是一个可以在线编写、测试和分享图形渲染着色器的网站。它提供了一个图形化的编辑器,可以让用户编写基于 WebGL 的 GLSL 着色器代码,并实时预览渲染结…...
引用阿里图标库,不知道对应的图标是什么,可在本地显示图标ui,再也不要担心刚来不知道公司图标对应的是什么了
项目中使用了阿里的图标库,但是无法看到对应显示什么,每次都要去阿里图标库里面找 在下载下来的文件中会发现有两个文件一个是iconfont.css和iconfont.json, 这两个文件的数据可以拿到然后显示在页面上 有两个问题: 1:…...
HandlerMethodArgumentResolver用于统一获取当前登录用户
这里记录回顾一些知识,不然就快忘记啦。 环境:SpringBoot 2.0.4.RELEASE需求:很多Controller方法,刚进来要先获取当前登录用户的信息,以便做后续的用户相关操作。准备工作:前端每次请求都传token࿰…...
记录 | mac打开终端时报错:login: /opt/homebrew/bin/zsh: No such file or directory [进程已完成]
mac打开终端时报错:login: /opt/homebrew/bin/zsh: No such file or directory [进程已完成],导致终端没有办法使用的情况 说明 zsh 没有安装或者是安装路径不对 可以看看 /bin 下有没有 zsh,若没有,肯定是有 bash 那就把终端默…...
anolisos8.8安装显卡+CUDA工具+容器运行时支持(containerd/docker)+k8s部署GPU插件
anolisos8.8安装显卡及cuda工具 一、目录 1、测试环境 2、安装显卡驱动 3、安装cuda工具 4、配置容器运行时 5、K8S集群安装nvidia插件 二、测试环境 操作系统:Anolis OS 8.8 内核版本:5.10.134-13.an8.x86_64 显卡安装版本:525.147.05 c…...
Golang 链表的创建和读取 小记
文章目录 链表的相关知识链表的创建:模拟方式建立链表的**递归创建** 链表的读取遍历读取递归读取 完整代码 链表的相关知识 链表有时会具有头节点,头节点的指针指向第一个节点的地址,其本身的数据域可以根据自己的选择进行赋值 接下来我将以将int转…...
实验记录:深度学习模型收敛速度慢有哪些原因
深度学习模型收敛速度慢有哪些原因? 学习率设置不当: 学习率是算法中一个重要的超参数,它控制模型参数在每次迭代中的更新幅度。如果学习率过大,可能会导致模型在训练过程中的振荡,进而影响到收敛速度;如果…...
Arris VAP2500 list_mac_address未授权RCE漏洞复现
0x01 产品简介 Arris VAP2500是美国Arris集团公司的一款无线接入器产品。 0x02 漏洞概述 Arris VAP2500 list_mac_address接口处命令执行漏洞,未授权的攻击者可通过该漏洞在服务器端任意执行代码,写入后门,获取服务器权限,进而控制整个web服务器。 0x03 复现环境 FOFA…...
【Jenkins】节点 node、凭据 credentials、任务 job
一、节点 node Jenkins在安装并初始化完成后,会有一个主节点(Master Node),默认情况下主节点可以同时运行的任务数是2,可以在节点配置中修改(系统管理/节点和云管理)。 Jenkins中的节点&#…...
华为OD机试 - 高效货运(Java JS Python C)
题目描述 老李是货运公司承运人,老李的货车额定载货重量为 wt。 现有两种货物: 货物 A 单件重量为 wa,单件运费利润为 pa货物 B 单件重量为 wb,单件运费利润为 pb老李每次发车时载货总重量刚好为货车额定的载货重量 wt,车上必须同时有货物 A 和货物 B ,货物A、B不可切割…...
基于python netmiko去ssh备份网络设备配置
自己为了便利写出来的基于python netmiko去ssh备份网络设备配置,用过secureCRT的脚本去备份设备配置,但是它没有图形化界面,使用不方便,自己就重新用python开发了一个,同时用pyinstaller打包成可执行程序(这…...
网站用微信登录 要怎么做/怎么制作网站链接
学习笔记 css position absolute relative 区别 相对定位 元素相对于他的起点进行移动,可以设置水平或垂直位置。 在使用相对定位时,无论是否进行移动,元素任然占据原来的空间。 <html><head><style>#div1 {width: 100px…...
怎么上传软件到网站/企业网站优化服务
都在附件中,简单记录一下转载于:https://blog.51cto.com/xflqsw/1439994...
淘宝 做网站空间 条件/牛推网
接上一节,增加数据库身份认证 1、修改Config配置文件auth-enabled为true 2、然后重新载入最新的config配置文件打开数据库 3、验证身份认证功能是否已打开 说明身份认证功能已打开 4、创建admin管理员用户 CREATE USER admin WITH PASSWORD sa_123 WITH ALL PRIVILE…...
公司旅游视频网站模板/免费私人网站建设
在过去一年,越来越多的项目继续或者开始使用React和Redux开发,这是目前前端业内很普遍的一种前端项目解决方案,但是随着开发项目越来越多,越来越多样化时,个人又有了不同的感受和想法。是不是因为已经有了一个比较普遍…...
wordpress前端用什么/浏阳廖主任打人
为什么80%的码农都做不了架构师?>>> 我们开发就是喜欢各种酷炫的东西,对于有洁癖的我,连命令行都不放过了 先上图看效果,命令行显示高亮部分 实现过程: 第一步:.bash_prompt脚本 # ~/.bash_p…...
电商网站会员体制怎么做/郑州专业seo哪家好
1、纯CSS3实现人物摇头动画这次我们要来分享一款超级可爱的纯CSS3人物摇头动画,初始化的时候人物的各个部位是利用CSS3动画效果拼接而成,接下来就是人物听音乐的场景,一边听音乐一边摇着脑袋,十分陶醉的样子,周围还会出…...