5_springboot_shiro_jwt_多端认证鉴权_禁用Cookie

news2024/11/18 20:39:00

1. Cookie是什么

​ Cookie是一种在客户端(通常是用户的Web浏览器)和服务器之间进行状态管理的技术。当用户访问Web服务器时,服务器可以向用户的浏览器发送一个名为Cookie的小数据块。浏览器会将这个Cookie存储在客户端,为这个Cookie设置了时间后,这个Cookie就会存储到客户端端的磁盘上。如果没有设置时间,它就会存放在浏览器所开启的进程内存中。在随后对该服务器的请求中,每次都会自动附带上这个Cookie。

​ Cookie通常包含一些基本信息,如唯一标识符、用户偏好设置、会话状态等。服务器通过解析这些Cookie来识别用户的身份、维持会话状态、个性化用户体验,甚至追踪用户行为。

每个Cookie都有特定的属性,如名称(name)、值(value)、过期时间(expiration date)、域(domain)、路径(path)等。服务器可以根据这些属性来确定何时发送或接收Cookie,以及如何处理它们。

​ 简而言之,Cookie是Web应用中实现用户状态保持和个性化服务的重要手段,但它同时也涉及到用户隐私问题,因为它们可以被用于追踪用户在互联网上的活动。出于隐私保护原因,现代浏览器都提供了对Cookie的管理和控制功能,允许用户禁用、删除或限制特定网站的Cookie使用。

​ 上一章讨论了Shiro如何保存会话,每个会话都会有一个SessionID。当一个会话被创建后,默认情况下这个SessionID作为Key被保存起来,上章保存到了Redis中。同时会被放入到Cookie中响应给浏览器,这样浏览器以后每次访问服务端,都会携带这个SessionID,服务端收到这个SessionID后,到Redis中获取Session数据,这样就能够识别到这个用户了。这就是保持会话的原理

2. 禁用Cookie

浏览器是可以禁用Cookie的,一旦浏览器禁用了Cookie后,传递SessionID没法传递到服务端,那就没法实现会话保持了。有一种办法是在所有请求的URL上添加一个请求参数如:JSESSIONID=2e8e4189-9254-4651-a77b-151f504efc3d, 这个请求参数就是SessionID,服务端读取这个参数来获取SessionID从而实现保持会话,这种办法被称为 URL重写(URL rewrite)。但是这种方法相对比较麻烦,业内采用这种方式的不多。

2.1 为什么要禁用Cookie

有一些场景比如 服务端要为小程序提供API服务,需要与小程序应用之间保持会话,还有原生的手机应用客户端程序,这些都没有使用浏览器,没法使用Cookie。

而我们的服务端需要做到为多端提供服务,不同渠道的客户端与服务端之间保持会话如何进行统一?我们的思路是禁用掉Cookie,禁用后,服务端就不会再向客户端响应Cookie数据了,取而代之的是将SessionID 作为响应数据返回给客户端,客户端收到响应后,将这个SessionID保存起来,后面每次发出请求的时候,取出这个SessionID,放入到请求头中,服务端收到请求之后,从请求头中取出SessionID,从而实现会话跟踪。

其实就是把浏览器自动发送Cookie变成了客户端程序发送SessionID,只不过方式是放在请求头中的。但是Shiro默认是开启Cookie的,即使我们禁用了Cookie,Shiro也不会到请求头中去取,这些都需要我们自己写代码去进行改造。

2.2 服务端禁用Cookie

在前一章节中,自己配置了SessionManager, 配置的是DefaultWebSessionManager 我们可以直接在服务端配置,让服务端不产生Cookie。为了方便比较,这里把禁用前和禁用后的相应信息截图出来。

现在不禁用Cookie, 用 Api fox 工具发起一次正确的登录,看看请求和响应报文分别是什么:( Api fox 工具的实际请求=>请求代码=>HTTP 可以看到请求报文)

  • 请求报文

    POST /login HTTP/1.1
    Host: 127.0.0.1:8080
    User-Agent: Apifox/1.0.0 (https://apifox.com)
    Accept: */*
    Host: 127.0.0.1:8080
    Connection: keep-alive
    Content-Type: application/x-www-form-urlencoded
    
    username=administrator&password=admin
    
  • 响应报文
    请添加图片描述
    可以看到,服务器端产生了Cookie,并返回给了客户端。

现在在服务端禁用Cookie,代码如下(第13,15行):

package com.qinyeit.shirojwt.demos.configuration;
@Configuration
@Slf4j
public class ShiroConfiguration {
    // sessionManager配置
    @Bean
    public SessionManager sessionManager(
            SessionFactory sessionFactory,
            SessionDAO sessionDAO) {
        DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
 		// 禁用Cookie
        webSessionManager.setSessionIdCookieEnabled(false);
        // 禁用URL重写
        webSessionManager.setSessionIdUrlRewritingEnabled(false);
        
        // 既然cookie都禁用了,就没有必要设置它了
        // webSessionManager.setSessionIdCookie(cookieTemplate);
        
        // 自动配置中已经配置了sessionFactory 直接注入进来
        webSessionManager.setSessionFactory(sessionFactory);
        // 使用自定义的ShiroRedisSessionDAO
        webSessionManager.setSessionDAO(sessionDAO);
        // 清理无效的session
        webSessionManager.setDeleteInvalidSessions(true);
        // 开启session定时检查
        webSessionManager.setSessionValidationSchedulerEnabled(true);
        webSessionManager.setSessionValidationScheduler(new ExecutorServiceSessionValidationScheduler());
        return webSessionManager;
    }
	...
}

修改完毕,重启服务后,将redis中保存的会话数据全部清除掉,然后再执行登录:

  • 请求报文:

    POST /login HTTP/1.1
    Host: 127.0.0.1:8080
    User-Agent: Apifox/1.0.0 (https://apifox.com)
    Accept: */*
    Host: 127.0.0.1:8080
    Connection: keep-alive
    Content-Type: application/x-www-form-urlencoded
    
    username=administrator&password=admin
    
  • 响应报文:
    请添加图片描述

可以看到,服务端不会再将SessionID响应回给客户端了。(不必关注那个 Cookie 1,那是工具里遗留的上一次的数据,没有清理掉)

现在在请求Home,返回的就是:

{
    "code": 401,
    "msg": "未登录或登录已过期"
}

因为保持会话的 SessionID丢了。也就是说虽然登录成功了,但是后面再请求服务器的时候,服务器依然不认识这个请求。

3. 保持会话的办法

引用Cookie后,无法保持会话。我们可以将会话ID作为数据响应给客户端程序,客户端程序将这个会话ID临时存储起来,下次发起请求的时候,将它放入到请求头中。(JavaScript 中使用 axios 库,在拦截器中很方便将数据放入到请求头中)。

这里做一个约定: SessionID在服务端依然叫 SessionID, 返回到客户端后,换一个叫法叫做 Access-Token , 请求头的名字也叫 Access-Token ,其实它就是SessionID

如果请求头中加入了自定义的头,这里会引发跨域问题。根据CORS(Cross-Origin Resource Sharing,跨源资源共享)规范,当发起跨域请求时,如果请求包含了自定义请求头(即非简单请求头),浏览器会先发送一个预检(OPTIONS)请求到服务器,询问服务器是否允许实际的请求发生。

这个预检请求会携带Access-Control-Request-Headers头部,列出实际请求打算发送的自定义请求头。服务器需要在响应中通过Access-Control-Allow-Headers头部告知浏览器哪些自定义请求头是可以接受的。只有当服务器确认允许这些自定义请求头之后,浏览器才会发送真实的POST、PUT、DELETE等请求。

简单请求指的是那些方法为GET、HEAD、POST,且满足以下条件之一的请求:

  • Content-Type 是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain。
  • 请求头仅包含Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(满足上述简单请求条件),以及其他若干标准请求头。

只要超出简单请求的范畴,浏览器都会执行预检请求以确保跨域请求的安全性。

不过不用担心,如果我们能确保请求是在同源策略(协议、域名、端口号均相同)下进行的。如果是同源请求,则无论是否有自定义请求头,浏览器都不会发送OPTIONS预检请求。

4.改造DefaultWebSessionManager

DefaultWebSessionManager 默认从Cookie中获取。我们从SessionManager接口开始跟踪,看看它到底是如何获取sessionID的。

先看看类继承图:
在这里插入图片描述
脉络很清晰: DefaultWebSessionManager->DefaultSessionManager->AbstractValidatingSessionManager->AbstractNativeSessionManager->AbstractSessionManager->SessionManger

4.1 找改造点

而SessionManger中哪个方法是获取SessionID的呢?

public interface SessionManager {
    Session start(SessionContext context);
    Session getSession(SessionKey key) throws SessionException;
}

从第一个实现类开始,一次向下查找源代码:

  • AbstractSessionManager : 抽象类,并没有实现SessionManager接口。这个类中定义了默认失效时间为 30分钟,也可以在外部调用 setGlobalSessionTimeout(long globalSessionTimeout) 来设置失效时间

  • AbstractNativeSessionManager 抽象类,它实现了 getSession方法,在这个方法中又调用了 本类中的 lookupSession方法, lookupSession方法调用了本类中的抽象方法 doGetsession。 因为是抽象方法,所以子类中一定会实现这个doGetSession方法。

    这个类中可以set 一个 SessionListener 集合进来,这样就可以监听Session的 onStart, onStop,onExpiration

  • AbstractValidatingSessionManager 抽象类,它实现了 doGetSession方法,在doGetSession方法中,调用了 retrieveSession方法, 而retrieveSession方法又是本类中的一个抽象方法, 继续在子类中找retrieveSession 抽象方法的实现

    这个类中可以设置是否开启 用于定期验证会话的调度,和 调度器对象(SessionValidationScheduler)

  • DefaultSessionManager 类,它实现了 retrieveSession 方法,代码片段如下:

    在 retrieveSession 主要调用了本类中的getSessionId 和 retrieveSessionFromDataSource 两个方法。 getSessionId 方法被子类DefaultWebSessionManager 重写了。

    非Web应用的SessionManager 使用的就是这个类。

    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        // 可以看到 从SessionKey 中获取sessionID是有可能为null的
        // getSessionId 被 子类 DefaultWebSessionManager重写了
        Serializable sessionId = getSessionId(sessionKey);
        if (sessionId == null) {
            LOGGER.debug("Unable to resolve session ID from SessionKey [{}].  Returning null to indicate a "
                         + "session could not be found.", sessionKey);
            return null;
        }
        Session s = retrieveSessionFromDataSource(sessionId);
        if (s == null) {
            //session ID was provided, meaning one is expected to be found, but we couldn't find one:
            String msg = "Could not find session with ID [" + sessionId + "]";
            throw new UnknownSessionException(msg);
        }
        return s;
    }
    // 从sessionKey中获取sessionID, 这个方法是 protected的,子类可以重写
    protected Serializable getSessionId(SessionKey sessionKey) {
        return sessionKey.getSessionId();
    }
    // 从dao中获取sessionID关联的session对象
    protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
        return sessionDAO.readSession(sessionId);
    }
    

    这个类中,可以set进来 sessionDAO, cacheManager和 sessionFactory

  • DefaultWebSessionManager 类,看名字就知道它与Web会话管理有关系,有与cookie,url重写相关的属性。来看看它重写的 getSessionId方法:

    // 重写父类的方法
    @Override
    public Serializable getSessionId(SessionKey key) {
        Serializable id = super.getSessionId(key);
        // 父类方法中没有获取到SessionID,而且是 一个 web key (WebSessionKey类)
        if (id == null && WebUtils.isWeb(key)) {
            ServletRequest request = WebUtils.getRequest(key);
            ServletResponse response = WebUtils.getResponse(key);
            // 调用了下面的方法,最终调用 getReferencedSessionId 方法从cookie中获取sessionID
            id = getSessionId(request, response);
        }
        return id;
    }
    
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        return getReferencedSessionId(request, response);
    }
    // 省略的代码就是在从Cookie中获取 SessionID
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        	// 省略代码
            ....
            return id;
    }
    

    代码跟踪到这里,就知道了我们该如何做了。

    1. 继承DefaultWebSessionManager

    2. 重写 protected Serializable getSessionId(ServletRequest request, ServletResponse response) 这个方法,在这个方法中从请求头中获取sessionID

    3. 在DefaultWebSessionManager 中,看到了一个方法 private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) 它是私有方法,被 protected void onStart(Session session, SessionContext context) 调用了,这个私有方法主要是创建cookie,并将cookie写回到浏览器。

      所以可以根据自己的需要,如果需要将SessionID响应到客户端,比如写入到响应头上,就可以重写onStart方法,如果没有这个需求则不用管这个方法

4.2 扩展DefaultWebSessionManager

下面我们写一个 AccessTokenWebSessionManager类,继承 DefaultWebSessionManager,并重写getSessionId 方法,从请求头上获取SessionID

package com.qinyeit.shirojwt.demos.shiro.session;
...
@Slf4j
public class AccessTokenWebSessionManager extends DefaultWebSessionManager {
    public AccessTokenWebSessionManager() {
        // 禁用Cookie
        super.setSessionIdCookieEnabled(false);
        // 禁用URL重写
        super.setSessionIdUrlRewritingEnabled(false);
        // 因为已经禁用了cookie,所以没有必要有这个配置了.
        // super.setSessionIdCookie(cookieTemplate);
        // 清理无效的session
        super.setDeleteInvalidSessions(true);
        // 开启session定时检查
        super.setSessionValidationSchedulerEnabled(true);
        super.setSessionValidationScheduler(new ExecutorServiceSessionValidationScheduler());
    }

 	//从请求头 X-Access-Token 获取SessionID
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String sessionId = WebUtils.toHttp(request).getHeader("X-Access-Token");
        if (sessionId != null) {
            return sessionId;
        }
        return super.getSessionId(request, response);
    }
}

4.3 改写Controller登录方法

原来登录成功后,是将sessionID,放入了cookie中响应给浏览器,浏览器下次请求的时候,只要cookie没有过期就会自动发送包含了sessionID的cookie。

现在cookie被禁用了,我们需要在登录成功后,将sessionID作为数据返回给客户端,客户端收到后存储起来,下次发送请求的时候,将它放入到请求头X-Access-Token上 .

package com.qinyeit.shirojwt.demos.controller;
@RestController
@Slf4j
public class AuthenticateController {
   ...
    @PostMapping("/login")
    public Map<String, String> login(HttpServletRequest req) {
        Subject             subject = SecurityUtils.getSubject();
        Map<String, String> map     = new HashMap<>();
        if (subject.isAuthenticated()) {
            // 主体的标识,可以有多个,但是需要具备唯一性。比如:用户名,手机号,邮箱等。
            PrincipalCollection principalCollection = subject.getPrincipals();
            log.info("是否认证:{},当前登录用户主体信息:{}", subject.isAuthenticated(), principalCollection.getPrimaryPrincipal());
            map.put("name", principalCollection.getPrimaryPrincipal().toString());
            // 将sessionID作为数据返回给客户端。
            map.put("accessToken", subject.getSession().getId().toString());
            map.put("message", "登录成功");
        } else {
            ...
        }
        return map;
    }
    ...
}

4.3 配置SessionManager

自定义的AccessTokenWebSessionManager 需要配置成SpringBean:

@Configuration
@Slf4j
public class ShiroConfiguration {
    ...
    // sessionManager配置 AccessTokenWebSessionManager
    @Bean
    public SessionManager sessionManager(
            SessionFactory sessionFactory,
            SessionDAO sessionDAO) {
        AccessTokenWebSessionManager webSessionManager = new AccessTokenWebSessionManager();
        // 自动配置中已经配置了sessionFactory 直接注入进来
        webSessionManager.setSessionFactory(sessionFactory);
        // 使用自定义的ShiroRedisSessionDAO
        webSessionManager.setSessionDAO(sessionDAO);
        return webSessionManager;
    }
	...
}

5. 测试

程序启动后,先登录:

请求报文:

POST /login HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Apifox/1.0.0 (https://apifox.com)
Accept: */*
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded

username=administrator&password=admin

响应结果:

{
    "name": "SystemAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt=55ae2b2c63ddd6d4763e0c57bda9078e)",
    "accessToken": "eb6490d0-7562-457c-b98a-69af27b8d6bc",
    "message": "登录成功"
}

可以看到,sessionID作为数据响应回来了。这里没有客户端程序(JavaScript) ,就手动在Api fox 中添加请求头 X-Access-Token, 将 accessToken设置到工具中,然后发送请求到 home
在这里插入图片描述
请求报文:

GET / HTTP/1.1
Host: 127.0.0.1:8080
X-Access-Token: 312cc7ce-38e5-4f89-914d-4d452bb130e5
User-Agent: Apifox/1.0.0 (https://apifox.com)
Accept: */*
Host: 127.0.0.1:8080
Connection: keep-alive

响应结果:

{
    "sessionKeys": "[org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY, org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY]",
    "name": "SystemAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt=55ae2b2c63ddd6d4763e0c57bda9078e)"
}

6. 总结

  1. 可以在服务端应用中禁用Cookie,配置SessionManager中的 sessionIdCookieEnabled和sessionIdUrlRewritingEnabled
  2. Cookie一旦被禁用,SessionID无法传递,无法保持会话。我们可以在每个请求发出前,在请求报文中加入自定义请求头如: X-Access-Token ,将SessionID放入到这个头上
  3. 自定义一个SessionManager,继承DefaultWebSessionManager,并重写getSessionId 方法从请求头中获取SessionID

代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 5_springboot_shiro_jwt_多端认证鉴权_禁用Cookie 分支上.

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

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

相关文章

字符串分割(C++)

经常碰到字符串分割的问题&#xff0c;这里总结下&#xff0c;也方便我以后使用。 一、用strtok函数进行字符串分割 原型&#xff1a; char *strtok(char *str, const char *delim); 功能&#xff1a;分解字符串为一组字符串。 参数说明&#xff1a;str为要分解的字符串&am…

LeetCode每日一题 将有序数组转换为二叉搜索树(分治)

题目描述 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵平衡二叉搜索树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9] 输出&#xff1a;[0,-3,9,-10,null,5] 解释&#xff1a;[0,-10,5,null,-3,null,9] 也将被视…

[RAM] RAM 突发传输(Burst ,Burst size, length) | Burst 读写过程与时序 精讲

主页&#xff1a; 元存储博客 文章目录 前言1. Burst 基本概念含义Burst Width &Burst Length 2. CPU Burst mode3. 总线 burst mode总线的仲裁总线突发传输时序 4. Burst Chop (突发终止)5. Burst Mode 应用什么时候用突发模式 总结 前言 在DMA&#xff08;直接内存访问&…

MD5算法:密码学中的传奇

title: MD5算法&#xff1a;密码学中的传奇 date: 2024/3/15 20:08:07 updated: 2024/3/15 20:08:07 tags: MD5起源算法原理安全分析优缺点比较技术改进示例代码应用趋势 MD5算法起源&#xff1a; MD5&#xff08;Message Digest Algorithm 5&#xff09;算法是由MIT的计算机…

Web框架盘点:好用又实用的技术解析

​随着2024年的临近&#xff0c;我们满怀热情地为新的一年制定计划&#xff0c;探索未来一年可以学习或实现的目标。此时是探索未来一年值得学习的框架、理解其功能和特点的最佳时机。我们将以2023年JavaScript新星为指南&#xff0c;力求保持客观公正的态度。对于每个值得关注…

调皮的String及多种玩法(下部)

&#x1f468;‍&#x1f4bb;作者简介&#xff1a;&#x1f468;&#x1f3fb;‍&#x1f393;告别&#xff0c;今天 &#x1f4d4;高质量专栏 &#xff1a;☕java趣味之旅 欢迎&#x1f64f;点赞&#x1f5e3;️评论&#x1f4e5;收藏&#x1f493;关注 &#x1f496;衷心的希…

修复 error Delete `␍` prettier/prettier 错误

修复 error Delete ␍ prettier/prettier 错误 问题背景报错信息报错原因解决办法修改CRLF----针对单个文件yarn run lint --fix 一键修复&#xff08;官方提供&#xff09; 问题背景 今天在使用 openapi 自动生成前端接口代码的时候&#xff0c;爆了一个类似 eslint 规范的错…

C/C++炸弹人游戏

参考书籍《啊哈&#xff0c;算法》&#xff0c;很有意思的一本算法书&#xff0c;小白也可以看懂&#xff0c;详细见书&#xff0c;这里只提供代码和运行结果。 这里用到的是枚举思想&#xff0c;还有更好地搜索做法。 如果大家有看不懂的地方或提出建议&#xff0c;欢迎评论区…

外包干了9天,技术退步明显。。。。。

先说一下自己的情况&#xff0c;本科生&#xff0c;2018年我通过校招踏入了南京一家软件公司&#xff0c;开始了我的职业生涯。那时的我&#xff0c;满怀热血和憧憬&#xff0c;期待着在这个行业中闯出一片天地。然而&#xff0c;随着时间的推移&#xff0c;我发现自己逐渐陷入…

Django之Cookie

Django之Cookie 目录 Django之Cookie介绍Django操作Cookie设置Cookie浏览器查看Cookie 获取Cookie设置超时Cookie注销Cookie 模拟登录验证登录验证装饰器登录验证装饰器-升级版 介绍 当我们上网使用社交媒体或者购物时&#xff0c;浏览器需要通过一种方式来记住我们。想象一下…

Java宝典-异常

目录 1. 异常的分类1.1 运行时异常1.2 编译时异常 2. 异常的抛出2.1 throw2.2 throws 3. 异常的捕获3.1 try-catch3.2 finally 4. 异常执行的过程5. 自定义异常 在Java中&#xff0c;异常(Exception)是指程序发生不正常的行为&#xff0c;异常其实就是一个一个的类。 1. 异常的…

算法-贪心-122. 糖果传递

题目 有 n个小朋友坐成一圈&#xff0c;每人有 a[i]个糖果。 每人只能给左右两人传递糖果。 每人每次传递一个糖果代价为 1。 求使所有人获得均等糖果的最小代价。 输入格式 第一行输入一个正整数 n&#xff0c;表示小朋友的个数。 接下来 n 行&#xff0c;每行一个整数…

数据结构 之 优先级队列(堆) (PriorityQueue)

&#x1f389;欢迎大家观看AUGENSTERN_dc的文章(o゜▽゜)o☆✨✨ &#x1f389;感谢各位读者在百忙之中抽出时间来垂阅我的文章&#xff0c;我会尽我所能向的大家分享我的知识和经验&#x1f4d6; &#x1f389;希望我们在一篇篇的文章中能够共同进步&#xff01;&#xff01;&…

langchain学习(十二)

Chat Messages | &#x1f99c;️&#x1f517; Langchain ChatMessageHistory&#xff1a;基类&#xff0c;保存HumanMessages、AIMessages from langchain.memory import ChatMessageHistory history ChatMessageHistory() history.add_user_message("hi!") his…

使用docker-compose管理freeswitch容器

概述 之前的文章我们介绍过如何将freeswitch做成docker镜像&#xff0c;也使用命令行模式正常启动了fs的docker容器。 但是当我们需要同时管理多个docker容器的时候&#xff0c;还是使用docker-compose更简单。 环境 CENTOS 7 docker engine&#xff1a;Version 25.0.3 D…

【深度学习与神经网络】MNIST手写数字识别1

简单的全连接层 导入相应库 import torch import numpy as np from torch import nn,optim from torch.autograd import Variable import matplotlib.pyplot as plt from torchvision import datasets, transforms from torch.utils.data import DataLoader读入数据并转为ten…

IDEA中在Service中开启管理多个微服务

问题 : 现在的service窗口里面什么都没有 ; 解决 : 1.没有service情况 : 点击View->Tool Windows -> Services,打开Service 2 . 在Service栏里操作 : 点击Add service &#xff0c; 然后选择第一个 : 然后在出来的选项中找到自己的项目类型 &#xff0c; 点击一下…

贪心算法(算法竞赛、蓝桥杯)--糖果传递

1、B站视频链接&#xff1a;A31 贪心算法 P2512 [HAOI2008] 糖果传递_哔哩哔哩_bilibili 题目链接&#xff1a;[HAOI2008] 糖果传递 - 洛谷 #include <bits/stdc.h> using namespace std; const int N1000005; int n,a[N],c[N]; long long b,ans;int main(){scanf(&quo…

Docker Compose基本配置及使用笔记

Docker Compose基本配置及使用笔记 简介 Docker Compose 是一个用于定义和运行多个 Docker 容器应用程序的工具。它使用 YAML 文件来配置应用程序的服务&#xff0c;并通过简单的命令集管理这些服务的生命周期。 1.步骤1 代码如下&#xff1a;docker-compose.yml放在虚拟机roo…

Mac版Jmeter安装与使用模拟分布式环境

Mac版Jmeter安装与使用&模拟分布式环境 1 安装Jmeter 1.1 安装Java环境 国内镜像地址&#xff1a;https://repo.huaweicloud.com/java/jdk/11.0.29/jdk-11.0.2_osx-x64_bin.dmg 下载dmg后&#xff0c;双击进行安装。 配置环境变量&#xff1a; # 1 打开环境变量配置文件…