SpringBoot集成Shiro+Jwt+Redis

1. 概述

首先需要知道为什么使用 Shiro+Jwt+Redis 进行登录认证和权限控制。

1. 为什么用Shiro?
主要用的是 shiro 的登录认证和权限控制功能。
Shiro 参见本栏目文章 🍃《Shiro实战》

2. 为什么用Jwt?
Shiro 默认的 Session 机制来帮助实现权限管理,用于维护用户的状态信息。而 JWTtoken认证 的一种具体实现方式,相对于传统的 session认证方式,有如下优点:

  • 跨域支持:cookie 是无法跨域的,而 token 由于没有用到 cookie(前提是将 token 放到请求头中),所以跨域后不会存在信息丢失问题。
  • 无状态:token 机制在服务端不需要存储 session 信息,因为 token 自身包含了所有登录用户的信息,所以可以减轻服务端压力,节约服务器资源,并且可以很容易地分布式横向扩展应用
  • 更适用CDN:可以通过内容分发网络请求服务端的所有资料。
  • 更适用于移动端:当客户端是非浏览器平台时,cookie 是不被支持的,此时采用 token 认证方式会简单很多。
  • 无需考虑CSRF:由于不再依赖 cookie,所以采用 token 认证方式不会发生 CSRF,所以也就无需考虑 CSRF 防御。

JWT 参见本栏目文章 🍃《JWT详解》

3. 为什么用Redis?

  1. JWT 本身不能续期,结合 Redis,可以实现续期主动登出
  2. Redis可以缓存用户信息,减少数据库压力。
  3. 可以实现分布式环境下的会话共享。

2. Springboot整合Shiro+Jwt+Redis

在这里插入图片描述

2.1 JWT配置

2.1.1 依赖

<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.19.0</version>
</dependency>

2.1.2 配置JWT

shiro:jwt:# 加密秘钥secret: kGgySFGcfQML4ZOvvlE7856YvSCsbjBf# token有效时长,1天,单位毫秒expire: 86400000header:# 加密算法alg: HS256# token类型typ: JWT

2.1.2 JWT工具类

@Component
public class JwtUtil {@Value("${shiro.jwt.secret}")private String secret;@Value("${shiro.jwt.expire}")private Long expire;@Value("${shiro.jwt.header.alg}")private String headerAlg;@Value("${shiro.jwt.header.typ}")private String headerTyp;/*** 生成token*/public String getToken(String account, long currentTimeMillis) {// 设置秘钥StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(account).append(secret);// 设置jwt头headerMap<String, Object> headerClaims = new HashMap<>();headerClaims.put("alg", headerAlg); // 签名算法headerClaims.put("typ", headerTyp); // token 类型// 设置jwt的header,负载paload以及加密算法String token = JWT.create().withHeader(headerClaims).withClaim("account" ,account).withClaim("expire", currentTimeMillis + expire).sign(Algorithm.HMAC256(stringBuilder.toString()));return token;}/*** 无需秘钥就能获取其中的信息* 解析token.* {* "account": "account",* "timeStamp": "134143214"* }*/public Map<String, String> parseToken(String token) {HashMap<String, String> map = new HashMap<String, String>();// 解码 JWTDecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");Claim expire = decodedJwt.getClaim("expire");map.put("account", account.asString());map.put("expire", expire.asLong().toString());return map;}/*** 解析token获取账号.*/public String getAccount(String token) {HashMap<String, String> map = new HashMap<String, String>();DecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");return account.asString();}/*** 校验token是否正确* @param token Token* @return boolean 是否正确*/public boolean verify(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");Claim expire = decodedJwt.getClaim("expire");StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(account.asString()).append(secret);// 验证JWT的签名和有效性Algorithm algorithm = Algorithm.HMAC256(stringBuilder.toString());JWTVerifier verifier = JWT.require(algorithm).build();try {verifier.verify(token);return true; // 验证通过} catch (JWTVerificationException e) {return false; // 验证失败}}/*** 校验token是否过期* @param token Token* @return boolean 是否正确*/public boolean isExpired(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim expire = decodedJwt.getClaim("expire");// 验证过期时间Long expireTime = expire.asLong();if (System.currentTimeMillis() > expireTime) {return true;}return false;}/*** 获取token过期时间* @param token Token* @return boolean 是否正确*/public long getExpiredTime(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim expire = decodedJwt.getClaim("expire");return expire.asLong();}}

2.2 Redis配置

2.2.1 依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2.2 配置 Redis 地址

spring:redis:host: localhostport: 6379password: 123456database: 0timeout: 5000lettuce:pool:max-idle: 16max-active: 32min-idle: 8

2.2.3 Redis序列化

/*** redis序列化*/
@Configuration
public class RedisConfig {@Bean(name = "redisTemplate")public RedisTemplate<String, Object> getRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {// 设置序列化Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 配置redisTemplateRedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(lettuceConnectionFactory);RedisSerializer<?> stringSerializer = new StringRedisSerializer();// key序列化redisTemplate.setKeySerializer(stringSerializer);// value序列化redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// Hash key序列化redisTemplate.setHashKeySerializer(stringSerializer);// Hash value序列化redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}

2.2.4 Redis工具类

封装 RedisTemplate

/*** RedisUtil 工具类*/
@Component
public class RedisUtil {// 使用jwt的过期时间毫秒private final long defaultTimeout = 1*24*60*60*1000;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 是否存在指定的key** @param key* @return*/public boolean hasKey(String key) {return Boolean.TRUE.equals(redisTemplate.hasKey(key));}/*** 删除指定的key** @param key* @return*/public boolean delete(String key) {return Boolean.TRUE.equals(redisTemplate.delete(key));}//- - - - - - - - - - - - - - - - - - - - -  String类型 - - - - - - - - - - - - - - - - - - - -/*** 根据key获取值** @param key 键* @return 值*/public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);}/*** 将值放入缓存** @param key   键* @param value 值* @return true成功 false 失败*/public void set(String key, String value) {set(key, value, defaultTimeout);}/*** 将值放入缓存并设置时间** @param key   键* @param value 值* @param time  时间(秒) -1为无期限* @return true成功 false 失败*/public void set(String key, String value, long time) {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(key, value, defaultTimeout, TimeUnit.SECONDS);}}//- - - - - - - - - - - - - - - - - - - - -  object类型 - - - - - - - - - - - - - - - - - - - -/*** 根据key读取数据*/public Object getObject(final String key) {if (StringUtils.isBlank(key)) {return null;}try {return redisTemplate.opsForValue().get(key);} catch (Exception e) {e.printStackTrace();}return null;}/*** 写入数据*/public boolean setObject(final String key, Object value) {if (StringUtils.isBlank(key)) {return false;}try {setObject(key, value , defaultTimeout);return true;} catch (Exception e) {e.printStackTrace();}return false;}public boolean setObject(final String key, Object value, long time) {if (StringUtils.isBlank(key)) {return false;}if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(key, value, defaultTimeout, TimeUnit.SECONDS);}return true;}
}

2.2.5 Redis常量

public class RedisConstant {private RedisConstant() {}public static final String PREFIX_ACCESS_TOKEN = "access_token";public static final String PREFIX_SHIRO_JWT = "shiro:jwt:";
}

2.3 Shiro配置

🍃《Shiro实战》已经介绍 Shiro 基本用法,如果需要整合 JWT 进行 token 认证,主要涉及三块改动:

  1. 关闭 Shiro 原有的 Session 功能,依赖于 Token 来进行认证。涉及 ShiroConfig 类改造。
  2. 配置 Shiro过滤器链 来指定哪些 URL 需要进行 JWT 认证。涉及 ShiroConfig 类改造。
  3. 自定义 Realm 类中认证方法 doGetAuthenticationInfo 进行逻辑改造。涉及 CustomRealm 类改造。

2.3.1 ShiroConfig

@Configuration
public class ShiroConfig {@Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher() {// 散列算法:这里使用 SHA-256 算法;HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("SHA-256");// 散列的次数,比如散列两次,相当于 SHA-256(SHA-256(""));credentialsMatcher.setHashIterations(1024);// storedCredentialsHexEncoded 默认是 true,此时用的是密码加密用的是 Hex 编码;false 时用 Base64 编码credentialsMatcher.setStoredCredentialsHexEncoded(true);return credentialsMatcher;}@Beanpublic CustomRealm customRealm() {CustomRealm customRealm = new CustomRealm();// 将 HashService 注入到自定义的 Realm 中,告诉 realm,使用 hashedCredentialsMatcher 加密算法类来验证密文customRealm.setCredentialsMatcher(hashedCredentialsMatcher());return customRealm;}@Beanpublic SecurityManager securityManager() {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 设置自定义 RealmsecurityManager.setRealm(customRealm());// 禁用 Shiro 的 Session 存储,这样可以确保 shiro 不会创建或使用 Session,而是依赖于无状态的 Token 来进行认证。DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();defaultSessionStorageEvaluator.setSessionStorageEnabled(false);subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);securityManager.setSubjectDAO(subjectDAO);return securityManager;}@Bean(name = "shiroFilter")public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();// 注入 securityManagershiroFilterFactoryBean.setSecurityManager(securityManager);// 添加自定义 Filter,并且取名为 jwtMap<String, Filter> filterMap = new HashMap<>();filterMap.put("jwt", new JwtFilter());shiroFilterFactoryBean.setFilters(filterMap);/*//登录shiroFilterFactoryBean.setLoginUrl("/login");//首页shiroFilterFactoryBean.setSuccessUrl("/index");//错误页面,认证不通过跳转shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");shiroFilterFactoryBean.setUnauthorizedUrl("/error");*/// 设置 Shiro 的过滤器链来指定哪些 URL 需要进行 JWT 认证。Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();// authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问filterChainDefinitionMap.put("/login", "anon");filterChainDefinitionMap.put("/api/**", "anon");// /** 必须放在所有权限设置的最后,表示对所有资源起作用,本例使用自定义 jwtfilterChainDefinitionMap.put("/**", "jwt");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}/*** **  开启 Shiro 的注解(如@RequiresRoles、@RequiresPermissions),需借助 SpringAOP 扫描使用 Shiro 注解的类,并在必要时进行安全逻辑验证* **  配置以下两个 bean (DefaultAdvisorAutoProxyCreator(可选)和 AuthorizationAttributeSourceAdvisor)即可实现此功能* * @return*/// 配置 DefaultAdvisorAutoProxyCreator,执行权限注解 @RequiresPermissions 会调用两次 doGetAuthorizationInfo() 方法。故不注入该配置。/*@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}@Bean@DependsOn({"lifecycleBeanPostProcessor"})public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();advisorAutoProxyCreator.setProxyTargetClass(true);return advisorAutoProxyCreator;}*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());return authorizationAttributeSourceAdvisor;}
}

其中,
在这里插入图片描述

该配置将 禁用 Shiro 的 Session 存储,这样可以确保 shiro 不会创建或使用 Session,而是依赖于 无状态的 Token 来进行认证

在这里插入图片描述

  • filterMap.put("jwt", new JwtFilter()) 添加自定义 Filter,并且取名为 jwt
  • filterChainDefinitionMap.put("/**", "jwt") 配置 Shiro 的过滤器链来指定哪些 URL 需要进行 JWT 认证。

2.3.2 自定义过滤器JwtFilter

自定义的 JWT 过滤器类。

/***  jwt过滤器,作为shiro的过滤器,对请求进行拦截并处理*/
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter {private String errorMsg;/*** 过滤器拦截请求的入口方法*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {// 判断请求头是否带上“Token”HttpServletRequest httpServletRequest = (HttpServletRequest) request;String token = httpServletRequest.getHeader("Authorization");// 游客访问电商平台首页可以不用携带 tokenif (StringUtils.isEmpty(token)) {return true;}try {// 交给自定义 Realm 处理// getSubject(request, response).login(new JwtToken(token));  //getSubject(request, response) 等同于 SecurityUtils.getSubject()SecurityUtils.getSubject().login(new JwtToken(token));return true;} catch (Exception e) {errorMsg = e.getMessage();e.printStackTrace();return false;}}/*** isAccessAllowed()方法返回false,即认证不通过时进入onAccessDenied方法*/@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {HttpServletResponse httpServletResponse = (HttpServletResponse) response;httpServletResponse.setStatus(400);httpServletResponse.setContentType("application/json;charset=utf-8");PrintWriter out = httpServletResponse.getWriter();out.println(JSONUtil.toJsonStr(Result.error(errorMsg)));out.flush();out.close();return false;}/*** 对跨域访问提供支持** @param request* @param response* @return* @throws Exception*/@Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest = (HttpServletRequest) request;HttpServletResponse httpServletResponse = (HttpServletResponse) response;httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));// 跨域发送一个option请求if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(HttpStatus.OK.value());return false;}return super.preHandle(request, response);}}
  • isAccessAllowed:过滤器拦截请求的入口方法。后续会交由自定义 Realm 处理。
  • onAccessDenied:认证不通过时,需要做什么处理。
  • preHandle:对跨域访问提供支持

2.3.3 自定义AuthenticationToken

JWTFilter 传递给 Realmtoken 必须是 AuthenticationToken 的实现类,通过这个类将 stringtoken 转型成 AuthenticationToken,主要目的是在 Realm 的认证和授权的时候,能够获取到 token

/** 将 String 的 token 转型成 AuthenticationToken,供 Realm 认证和授权使用*/
public class JwtToken implements AuthenticationToken {private String token;public JwtToken(String token) {this.token = token;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}
}

✨注:需要重写 getPrincipalgetCredentials 方法。

2.3.4 自定义Realm

自定义 Realm 类中认证方法 doGetAuthenticationInfo 进行逻辑改造。

Shiro+Jwt+Redis

shiro整合redis jwt

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

jenkins 构建报错 Cannot run program “sh”

原因 在 windows 操作系统 jenkins 自动化部署的时候, 由于自动化构建的命令是 shell 执行的,而默认windows 从 path 路径拿到的 shell 没有 sh.exe &#xff0c;因此报错。 解决方法 前提是已经安装过 git WINR 输入cmd 打开命令行, 然后输入where git 获取 git 的路径, …

Springboot——对接支付宝实现扫码支付

文章目录 前言官方文档以及说明1、申请沙箱2、进入沙箱获取对应的关键信息3、拿到系统生成的公钥和密钥 注意事项创建springboot项目1、引入依赖2、配置连接参数3、创建配置类&#xff0c;用于接收这些参数4、中间类的定义(订单类)5、编写测试接口场景一、pc端请求后端后&#…

【云备份项目】json以及jsoncpp库的使用

目录 1.JSON 2.什么是 JSON&#xff1f; 3.JSON 发展史 4.为什么要使用 JSON&#xff1f; 5.JSON 的不足 6.JSON 应该如何存储&#xff1f; 7.什么时候会使用 JSON 7.1.定义接口 7.2.序列化 7.3.生成 Token 7.4.配置文件 8.JSON的语法规则 8.1.对象和数组 8.2.JS…

【C++篇】在秩序与混沌的交响乐中: STL之map容器的哲学探寻

文章目录 C map 容器详解&#xff1a;高效存储与快速查找前言第一章&#xff1a;C map 的概念1.1 map 的定义1.2 map 的特点 第二章&#xff1a;map 的构造方法2.1 常见构造函数2.1.1 示例&#xff1a;不同构造方法 2.2 相关文档 第三章&#xff1a;map 的常用操作3.1 插入操作…

HOT100_最大子数组和

class Solution {public int maxSubArray(int[] nums) {int[] dp new int[nums.length];int res nums[0];dp[0] nums[0];for(int i 1; i< nums.length; i){dp[i] Math.max(nums[i] ,dp[i-1] nums[i]);res Math.max(res, dp[i]);}return res;} }

contenteditable实现需要一个像文本域一样的可编辑框

我这里是因为左上和右下有一个固定的模板&#xff0c;所有用textarea有点不方便&#xff0c;查了下还有一个方法可以解决就是在需要编辑的元素上加上 :contenteditable"true" 完整代码如下&#xff0c;因为这个弹窗是两用的&#xff0c;所以用messageType做了一下判…

SpringBoot源码解析(一)

SpringBoot自动装配原理 SpringBootApplication注解 我们在使用SpringBoot时&#xff0c;通常使用的是SpringBootApplication这个注解&#xff0c;比如&#xff1a; 而这个注解的定义为下图&#xff0c;可以发现这个注解上有另外三个注解&#xff1a;SpringBootConfiguration…

WPF+MVVM案例实战与特效(二十四)- 粒子字体效果实现

文章目录 1、案例效果2、案例实现1、文件创建2.代码实现3、界面与功能代码3、总结1、案例效果 提示:这里可以添加本文要记录的大概内容: 2、案例实现 1、文件创建 打开 Wpf_Examples 项目,在 Views 文件夹下创建窗体界面 ParticleWindow.xaml,在 Models 文件夹下创建粒子…

js中怎么把excel和pdf文件转换成图片打包下载

index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>文件转图片工具</title><!-- 本…

盘点 2024 十大免费/开源 WAF

WAF 是 Web Application Firewall 的缩写&#xff0c;也被称为 Web 应用防火墙。区别于传统防火墙&#xff0c;WAF 工作在应用层&#xff0c;对基于 HTTP/HTTPS 协议的 Web 系统有着更好的防护效果&#xff0c;使其免于受到黑客的攻击。 近几年经济增速开始放缓&#xff0c;科…

蓝牙资讯|苹果AirPods Pro 2推出听力测试、助听器和听力保护等功能

苹果推送iOS 18.1 系统版本更新&#xff0c;AirPods Pro 2 用户也在 iOS 18.1 中获得了强大的新功能。 运行固件 7B19 的 AirPods Pro 2 用户&#xff0c;搭配 iOS 18.1 系统的 iPhone&#xff0c;将获得三项强大的听力健康功能&#xff1a;听力测试、助听器和听力保护。 听力…

如何检查雷池社区版 WAF 是否安装成功?

容器运行状态检查&#xff1a; 使用命令行检查&#xff1a;打开终端&#xff0c;连接到安装雷池的服务器。运行 docker ps 命令&#xff0c;查看是否有与雷池相关的容器正在运行。 如果能看到类似 safeline-mgt、safeline-tengine 等相关容器&#xff0c;并且状态为 Up&#x…

【AI开源项目】Botpress - 开源智能聊天机器人平台及其部署方案

文章目录 Botpress 概述Botpress 的定位 Botpress 的主要特点1. OpenAI 集成2. 易于使用3. 定制和扩展性4. 多平台支持5. 集成和扩展 API6. 活跃的社区和详尽的文档 部署方案集成集成开发集成部署机器人示例开发工具代理本地开发先决条件从源代码构建 Botpress 如何解决常见问题…

Rust 力扣 - 1652. 拆炸弹

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们只需要遍历长度长度为k的窗口&#xff0c;然后把窗口内数字之和填充到结果数组中的对应位置即可 题解代码 impl Solution {pub fn decrypt(code: Vec<i32>, k: i32) -> Vec<i32> {let n c…

HTMLCSS:打造酷炫下载安装模拟按钮

效果演示 这段代码通过HTML和CSS创建了一个具有交互效果的下载按钮&#xff0c;当复选框被选中时&#xff0c;会触发一系列动画和样式变化&#xff0c;模拟了一个下载和安装的过程&#xff0c;包括圆形的动画、文本的显示和隐藏等。 HTML <div class"container&quo…

【C++、数据结构】哈希表——散列表(一)(概念/总结)

「前言」 &#x1f308;个人主页&#xff1a; 代码探秘者 &#x1f308;C语言专栏&#xff1a;C语言 &#x1f308;C专栏&#xff1a; C / STL使用以及模拟实现 &#x1f308;数据结构专栏&#xff1a; 数据结构 / 十大排序算法 &#x1f308;Linux专栏&#xff1a; Linux系统编…

WindowsDocker安装到D盘,C盘太占用空间了。

Windows安装 Docker Desktop的时候,默认位置是安装在C盘,使用Docker下载的镜像文件也是保存在C盘,如果对Docker使用评率比较高的小伙伴,可能C盘空间,会被耗尽,有没有一种办法可以将Docker安装到其它磁盘,同时Docker的数据文件也保存在其他磁盘呢? 答案是有的,我们可以…

mac|安装redis及RedisDesk可视化软件

一、安装 通过Homebrew安装 brew install redis 在安装过程可以得到以下信息&#xff1a; 1、启动redis或重新登陆redis brew services start redis 如果只想在前端运行&#xff0c;而不是在后端&#xff0c;则使用以下命令 /opt/homebrew/opt/redis/bin/redis-server /opt…

程序中怎样用最简单方法实现写excel文档

很多开发语言都能找到excel文档读写的库&#xff0c;但是在资源极其受限的环境下开发&#xff0c;引入这些库会带来兼容性问题。因为一个小功能引入一堆库&#xff0c;我始终觉得划不来。看到有项目引用的jar包有一百多个&#xff0c;看着头麻&#xff0c;根本搞不清谁依赖谁。…

重读《人月神话》(12)-未雨绸缪(Plan to Throw One Away)

对程序员而言&#xff0c;一个不容忽视的事实是&#xff1a;任何系统都将经历变更&#xff0c;最初精心设计的软件也可能因不断的修补而变得面目全非。无论设计多么完美&#xff0c;随着时间推移&#xff0c;系统难免陷入混乱&#xff0c;只是程度和速度有所不同。因此&#xf…