Shiro框架:Shiro登录认证流程源码解析

目录

1.用户登录认证流程

1.1 生成认证Token

1.2 用户登录认证

1.2.1  SecurityManager login流程解析

1.2.1.1 authenticate方法进行登录认证

1.2.1.1.1 单Realm认证

1.2.1.2 认证通过后创建登录用户对象

1.2.1.2.1 复制SubjectContext

1.2.1.2.2 对subjectContext设置securityManager

1.2.1.2.3 对subjectContext设置session

1.2.1.2.4 对subjectContext设置Principals

1.2.1.2.5 根据subjectContext创建Subject

1.2.1.2.6 保存Subject 


Shiro作为一款比较流行的登录认证、访问控制安全框架,被广泛应用在程序员社区;Shiro登录验证、访问控制、Session管理等流程内部都是委托给SecurityManager安全管理器来完成的,SecurityManager安全管理器上篇文章已经进行了详细解析,详见:Shiro框架:Shiro SecurityManager安全管理器解析-CSDN博客,在此基础上,本篇文章继续对Shiro关联链路处理流程之一---登录认证流程 进行解析;

想要深入了解Shiro框架整体原理,可移步:

Shiro框架:ShiroFilterFactoryBean过滤器源码解析-CSDN博客、

Shiro框架:Shiro内置过滤器源码解析-CSDN博客

1.用户登录认证流程

在Shiro框架:Shiro内置过滤器源码解析-CSDN博客内置过滤器分析中,我们知道用户执行登录的认证操作是在过滤器FormAuthenticationFilter中执行的,如下:

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {if (isLoginRequest(request, response)) {if (isLoginSubmission(request, response)) {if (log.isTraceEnabled()) {log.trace("Login submission detected.  Attempting to execute login.");}return executeLogin(request, response);} else {if (log.isTraceEnabled()) {log.trace("Login page view.");}//allow them to see the login page ;)return true;}} else {if (log.isTraceEnabled()) {log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +"Authentication url [" + getLoginUrl() + "]");}saveRequestAndRedirectToLogin(request, response);return false;}}

登录认证操作是在executeLogin方法中完成的:

    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {AuthenticationToken token = createToken(request, response);if (token == null) {String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +"must be created in order to execute a login attempt.";throw new IllegalStateException(msg);}try {Subject subject = getSubject(request, response);subject.login(token);return onLoginSuccess(token, subject, request, response);} catch (AuthenticationException e) {return onLoginFailure(token, e, request, response);}}

上述源码的执行过程表示为时序图会更直观,时序图如下:

该认证过程主要包含以下几个部分:

  1. 根据用户名密码等生成认证Token
  2. 获取当前登录用户Subject
  3. 调用Subject的login方法进行登录认证
  4. 其它的登录成功、或登录失败的拦截方法

下面主要对生成认证Token和用户登录认证实现进行详细说明;

1.1 生成认证Token

createToken的实现在FormAuthenticationFilter内,如下:

这里用户名和密码是通过request请求对象获取的:

    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {String username = getUsername(request);String password = getPassword(request);return createToken(username, password, request, response);}

在父类AuthenticatingFilter中进一步实现如下,这里构造了UsernamePasswordToken类,表示采用用户名密码的认证方式;

    protected AuthenticationToken createToken(String username, String password,ServletRequest request, ServletResponse response) {boolean rememberMe = isRememberMe(request);String host = getHost(request);return createToken(username, password, rememberMe, host);}protected AuthenticationToken createToken(String username, String password,boolean rememberMe, String host) {return new UsernamePasswordToken(username, password, rememberMe, host);}

图示UsernamePasswordToken的继承结构:

1.2 用户登录认证

在Subject的login的具体实现如下:

  • 实际的login是委托给SecurityManager完成的
  • 登录成功后设置当前登录对象为认证成功

下面主要对SecurityManager的login方法进行具体分析; 

1.2.1  SecurityManager login流程解析

login方法具体实现如下:

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {AuthenticationInfo info;try {info = authenticate(token);} catch (AuthenticationException ae) {try {onFailedLogin(token, ae, subject);} catch (Exception e) {if (log.isInfoEnabled()) {log.info("onFailedLogin method threw an " +"exception.  Logging and propagating original AuthenticationException.", e);}}throw ae; //propagate}Subject loggedIn = createSubject(token, info, subject);onSuccessfulLogin(token, info, loggedIn);return loggedIn;}

上述登录流程主要包含2部分内容: 

  1. 通过authenticate方法进行登录认证
  2. 认证通过后创建登录用户对象

下面分别进行展开分析;

1.2.1.1 authenticate方法进行登录认证

具体的authenticate实现内部又委托给了认证器Authenticator来实现,具体的认证器为ModularRealmAuthenticator,其继承结构如下:

authenticate方法具体实现是在父类AbstractAuthenticator中,如下:

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {if (token == null) {throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");}log.trace("Authentication attempt received for token [{}]", token);AuthenticationInfo info;try {info = doAuthenticate(token);if (info == null) {String msg = "No account information found for authentication token [" + token + "] by this " +"Authenticator instance.  Please check that it is configured correctly.";throw new AuthenticationException(msg);}} catch (Throwable t) {AuthenticationException ae = null;if (t instanceof AuthenticationException) {ae = (AuthenticationException) t;}if (ae == null) {//Exception thrown was not an expected AuthenticationException.  Therefore it is probably a little more//severe or unexpected.  So, wrap in an AuthenticationException, log to warn, and propagate:String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " +"error? (Typical or expected login exceptions should extend from AuthenticationException).";ae = new AuthenticationException(msg, t);if (log.isWarnEnabled())log.warn(msg, t);}try {notifyFailure(token, ae);} catch (Throwable t2) {if (log.isWarnEnabled()) {String msg = "Unable to send notification for failed authentication attempt - listener error?.  " +"Please check your AuthenticationListener implementation(s).  Logging sending exception " +"and propagating original AuthenticationException instead...";log.warn(msg, t2);}}throw ae;}log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);notifySuccess(token, info);return info;}

通过方法doAuthenticate对认证Token进行认证,通过notifySuccess、notifyFailure监听认证通过或失败的事件并调用注册的监听器;

方法doAuthenticate的具体实现是在子类完成的,如下:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {assertRealmsConfigured();Collection<Realm> realms = getRealms();if (realms.size() == 1) {return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);} else {return doMultiRealmAuthentication(realms, authenticationToken);}}

这里首先获取已注册的安全组件Realm,其注册过程是在RealmSecurityManager初始化的过程中完成的(对SecurityManager安全管理器的处理过程感兴趣可以参见:Shiro框架:Shiro SecurityManager安全管理器解析-CSDN博客)

然后根据Realm的个数分别执行单Realm认证或多Realm认证,这里已单Realm认证为例进行说明,多Realm认证类似;

1.2.1.1.1 单Realm认证

通过doSingleRealmAuthentication方法完成单Realm认证,实现如下:

    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {if (!realm.supports(token)) {String msg = "Realm [" + realm + "] does not support authentication token [" +token + "].  Please ensure that the appropriate Realm implementation is " +"configured correctly or that the realm accepts AuthenticationTokens of this type.";throw new UnsupportedTokenException(msg);}AuthenticationInfo info = realm.getAuthenticationInfo(token);if (info == null) {String msg = "Realm [" + realm + "] was unable to find account data for the " +"submitted AuthenticationToken [" + token + "].";throw new UnknownAccountException(msg);}return info;}

这里Reaml接口包含一整套的继承层次实现,如下,这里不过多展开解析;

getAuthenticationInfo是在AuthenticatingRealm中完成的,如下:

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {AuthenticationInfo info = getCachedAuthenticationInfo(token);if (info == null) {//otherwise not cached, perform the lookup:info = doGetAuthenticationInfo(token);log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);if (token != null && info != null) {cacheAuthenticationInfoIfPossible(token, info);}} else {log.debug("Using cached authentication info [{}] to perform credentials matching.", info);}if (info != null) {assertCredentialsMatch(token, info);} else {log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);}return info;}

 这里首先获取AuthenticationInfo对象,要么从缓存中获取,要么通过引入的抽象方法doGetAuthenticationInfo获取(交由应用层子类具体实现);

获取到AuthenticationInfo后,将其余用户录入的登录Token进行比对,这部分具体是方法assertCredentialsMatch完成的,具体如下:

    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {CredentialsMatcher cm = getCredentialsMatcher();if (cm != null) {if (!cm.doCredentialsMatch(token, info)) {//not successful - throw an exception to indicate this:String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";throw new IncorrectCredentialsException(msg);}} else {throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +"credentials during authentication.  If you do not wish for credentials to be examined, you " +"can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");}}

这里获取了默认的匹配器SimpleCredentialsMatcher,并调用doCredentialsMatch方法进行匹配,匹配实现如下:

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {Object tokenCredentials = getCredentials(token);Object accountCredentials = getCredentials(info);return equals(tokenCredentials, accountCredentials);}

首先获取Credentials,也即用户密码,然后调用equals方法进行匹配,如下通过字节流的方式进行比对(主要是为了安全考虑,系统处理时采用非明文形式)。

    protected boolean equals(Object tokenCredentials, Object accountCredentials) {if (log.isDebugEnabled()) {log.debug("Performing credentials equality check for tokenCredentials of type [" +tokenCredentials.getClass().getName() + " and accountCredentials of type [" +accountCredentials.getClass().getName() + "]");}if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) {if (log.isDebugEnabled()) {log.debug("Both credentials arguments can be easily converted to byte arrays.  Performing " +"array equals comparison");}byte[] tokenBytes = toBytes(tokenCredentials);byte[] accountBytes = toBytes(accountCredentials);return MessageDigest.isEqual(tokenBytes, accountBytes);} else {return accountCredentials.equals(tokenCredentials);}}

 至此,用户账号密码匹配完成,匹配完成后,会重新创建用户登录对象,并更新用户状态等,下面具体分析;

1.2.1.2 认证通过后创建登录用户对象

认证通过后创建Subject的实现如下:

    protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {SubjectContext context = createSubjectContext();context.setAuthenticated(true);context.setAuthenticationToken(token);context.setAuthenticationInfo(info);if (existing != null) {context.setSubject(existing);}return createSubject(context);}@Overrideprotected SubjectContext createSubjectContext() {return new DefaultWebSubjectContext();}

这里创建了DefaultWebSubjectContext,用户Subject创建,其中设置了认证状态、认证Token、已认证信息,然后调用createSubject方法继续进行构造,如下:

    public Subject createSubject(SubjectContext subjectContext) {//create a copy so we don't modify the argument's backing map:SubjectContext context = copy(subjectContext);//ensure that the context has a SecurityManager instance, and if not, add one:context = ensureSecurityManager(context);//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before//sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the//process is often environment specific - better to shield the SF from these details:context = resolveSession(context);//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first//if possible before handing off to the SubjectFactory:context = resolvePrincipals(context);Subject subject = doCreateSubject(context);//save this subject for future reference if necessary://(this is needed here in case rememberMe principals were resolved and they need to be stored in the//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).//Added in 1.2:save(subject);return subject;}

如上创建过程包含了以下几部分,分别进行分析;

1.2.1.2.1 复制SubjectContext

对subjectContext调用复制构造方法进行复制,subjectContext底层通过backingMap保存上下文信息:

    public MapContext(Map<String, Object> map) {this();if (!CollectionUtils.isEmpty(map)) {this.backingMap.putAll(map);}}
1.2.1.2.2 对subjectContext设置securityManager

获取securityManager的顺序如下:

  1. 首先从subjectContext中获取
  2. 若无,则从当前线程上下文中获取
  3. 否则将当前securityManager设置到subjectContext中
    protected SubjectContext ensureSecurityManager(SubjectContext context) {if (context.resolveSecurityManager() != null) {log.trace("Context already contains a SecurityManager instance.  Returning.");return context;}log.trace("No SecurityManager found in context.  Adding self reference.");context.setSecurityManager(this);return context;}
    public SecurityManager resolveSecurityManager() {SecurityManager securityManager = getSecurityManager();if (securityManager == null) {if (log.isDebugEnabled()) {log.debug("No SecurityManager available in subject context map.  " +"Falling back to SecurityUtils.getSecurityManager() lookup.");}try {securityManager = SecurityUtils.getSecurityManager();} catch (UnavailableSecurityManagerException e) {if (log.isDebugEnabled()) {log.debug("No SecurityManager available via SecurityUtils.  Heuristics exhausted.", e);}}}return securityManager;}
1.2.1.2.3 对subjectContext设置session

解析session方法如下,其中session的解析顺序为:

  1. 首先从subjectContext获取session,判断是否已设置session(见源码2)
  2. 否则,通过subjectContext中保存的Subject对象获取关联的session(见源码2)
  3. 其次,通过sessionManager Session管理器获取(见源码3)
  4. Web服务中,具体的Session管理器为ServletContainerSessionManager,尝试从request请求中获取Servlet管理的Session;(见源码4)

源码1: 

    protected SubjectContext resolveSession(SubjectContext context) {if (context.resolveSession() != null) {log.debug("Context already contains a session.  Returning.");return context;}try {//Context couldn't resolve it directly, let's see if we can since we have direct access to //the session manager:Session session = resolveContextSession(context);if (session != null) {context.setSession(session);}} catch (InvalidSessionException e) {log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +"(session-less) Subject instance.", e);}return context;}

源码2: 

    public Session resolveSession() {Session session = getSession();if (session == null) {//try the Subject if it exists:Subject existingSubject = getSubject();if (existingSubject != null) {session = existingSubject.getSession(false);}}return session;}

源码3: 

    protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {SessionKey key = getSessionKey(context);if (key != null) {return getSession(key);}return null;}public Session getSession(SessionKey key) throws SessionException {return this.sessionManager.getSession(key);}

源码4: ServletContainerSessionManager获取Session

    public Session getSession(SessionKey key) throws SessionException {if (!WebUtils.isHttp(key)) {String msg = "SessionKey must be an HTTP compatible implementation.";throw new IllegalArgumentException(msg);}HttpServletRequest request = WebUtils.getHttpRequest(key);Session session = null;HttpSession httpSession = request.getSession(false);if (httpSession != null) {session = createSession(httpSession, request.getRemoteHost());}return session;}
1.2.1.2.4 对subjectContext设置Principals

resolvePrincipals方法实现如下,这里获取Principals的顺序为:

  1. 先从SubjectContext中直接获取Principals(见源码2)
  2. 否则,通过SubjectContext中已认证的AuthenticationInfo获取(见源码2)
  3. 其次,通过SubjectContext中的Subject获取(见源码2)
  4. 再次,通过SubjectContext中的Session获取(见源码2)
  5. 最后,通过RememberMeManager中的Cookie中获取

源码1:

    protected SubjectContext resolvePrincipals(SubjectContext context) {PrincipalCollection principals = context.resolvePrincipals();if (isEmpty(principals)) {log.trace("No identity (PrincipalCollection) found in the context.  Looking for a remembered identity.");principals = getRememberedIdentity(context);if (!isEmpty(principals)) {log.debug("Found remembered PrincipalCollection.  Adding to the context to be used " +"for subject construction by the SubjectFactory.");context.setPrincipals(principals);} else {log.trace("No remembered identity found.  Returning original context.");}}return context;}

源码2:从SubjectContext获取Principals

    public PrincipalCollection resolvePrincipals() {PrincipalCollection principals = getPrincipals();if (isEmpty(principals)) {//check to see if they were just authenticated:AuthenticationInfo info = getAuthenticationInfo();if (info != null) {principals = info.getPrincipals();}}if (isEmpty(principals)) {Subject subject = getSubject();if (subject != null) {principals = subject.getPrincipals();}}if (isEmpty(principals)) {//try the session:Session session = resolveSession();if (session != null) {principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);}}return principals;}
1.2.1.2.5 根据subjectContext创建Subject

如下,通过createSubject创建了WebDelegatingSubject对象;

    public Subject createSubject(SubjectContext context) {if (!(context instanceof WebSubjectContext)) {return super.createSubject(context);}WebSubjectContext wsc = (WebSubjectContext) context;SecurityManager securityManager = wsc.resolveSecurityManager();Session session = wsc.resolveSession();boolean sessionEnabled = wsc.isSessionCreationEnabled();PrincipalCollection principals = wsc.resolvePrincipals();boolean authenticated = wsc.resolveAuthenticated();String host = wsc.resolveHost();ServletRequest request = wsc.resolveServletRequest();ServletResponse response = wsc.resolveServletResponse();return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,request, response, securityManager);}
1.2.1.2.6 保存Subject 

 Subject保存是通过DefaultSubjectDAO完成的,

如下,通过saveToSession方法保存Principals和认证状态到Session中;

    protected void saveToSession(Subject subject) {//performs merge logic, only updating the Subject's session if it does not match the current state:mergePrincipals(subject);mergeAuthenticationState(subject);}

这里在保存Principals的过程中,如果Principals不为空,且非Servlet Session的条件下,这里会调用subject.getSession()方法创建shiro管理的native Session对象:

subject.getSession()方法实现如下:

    public Session getSession() {return getSession(true);}public Session getSession(boolean create) {if (log.isTraceEnabled()) {log.trace("attempting to get session; create = " + create +"; session is null = " + (this.session == null) +"; session has id = " + (this.session != null && session.getId() != null));}if (this.session == null && create) {//added in 1.2:if (!isSessionCreationEnabled()) {String msg = "Session creation has been disabled for the current subject.  This exception indicates " +"that there is either a programming error (using a session when it should never be " +"used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +"for the current Subject.  See the " + DisabledSessionException.class.getName() + " JavaDoc " +"for more.";throw new DisabledSessionException(msg);}log.trace("Starting session for host {}", getHost());SessionContext sessionContext = createSessionContext();Session session = this.securityManager.start(sessionContext);this.session = decorate(session);}return this.session;}

如上,通过securityManager的start方法创建Session对象,start具体实现为 :

    public Session start(SessionContext context) throws AuthorizationException {return this.sessionManager.start(context);}

这里又委托给了Session管理器完成session创建,这里的Session管理器实现为AbstractNativeSessionManager,start方法实现如下:

    public Session start(SessionContext context) {Session session = createSession(context);applyGlobalSessionTimeout(session);onStart(session, context);notifyStart(session);//Don't expose the EIS-tier Session object to the client-tier:return createExposedSession(session, context);}

上述主要完成了以下几部分功能:

1. 通过createSession方法创建SimpleSession对象,其中包括了Session创建、Session保存到            sessionDAO中(比如保存到内存中的子类MemorySessionDAO,或者保存在数据库中的子类        EnterpriseCacheSessionDAO等),SessionId生成等操作;

    图示sessionDAO的整体继承结构:

    

2. Session全局超时时间配置(默认超时时间为30min)

3. onStart将SessionId存储到Cookie中,如下:    

    private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {if (currentId == null) {String msg = "sessionId cannot be null when persisting for subsequent requests.";throw new IllegalArgumentException(msg);}Cookie template = getSessionIdCookie();Cookie cookie = new SimpleCookie(template);String idString = currentId.toString();cookie.setValue(idString);cookie.saveTo(request, response);log.trace("Set session ID cookie for session with id {}", idString);}

至此,用户登录认证过程完成了生成认证Token、通过Realm从应用中获取用户认证信息、用户认证Token匹配,以及认证成功后创建用户Subject对象、保存Subject到Session、SessionId保存到Cookie等操作。

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

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

相关文章

二、MySQL安装

目录 1、双击mysql8的安装向导 2、分为首次安装和再安装 1&#xff09;、首次安装 &#xff08;1&#xff09;如果是首次安装mysql系列的产品&#xff0c;需要先安装mysql产品的安装向导 &#xff08;2&#xff09;选择安装模式 2&#xff09;、不是首次安装 &#xff0…

学会这个技巧,制作电子杂志SOEASY

​电子杂志是一种非常流行的传播方式&#xff0c;它能够以更加生动、直观的方式展示你的品牌和产品。通过电子杂志&#xff0c;你可以将文字、图片、视频等多种元素有机地结合起来&#xff0c;创造出令人难忘的视觉效果。 如果你想制作一本电子杂志&#xff0c;但不知道从何入…

POSIX API与网络协议栈

本文介绍linux中与tcp网络通信相关的POSIX API&#xff0c;在每次调用的时候&#xff0c;网络协议栈会进行的操作与记录。 POSIX API Posix API&#xff0c;提供了统一的接口&#xff0c;使程序能得以在不同的系统上运行。简单来说不同的操作系统进行同一个活动&#xff0c;比…

QT上位机开发(进度条操作)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 进度条是一个比较常见的控件。如果某个操作需要很长的时间才能完成&#xff0c;那么这个时候最好有一个进度条提示&#xff0c;这样比较容易平复一…

18 串口通讯

文章目录 18.0 前言18.1 串口通讯协议简介18.1.1 物理层 18.2 RT1052 的 LPUART 简介18.3 UART 功能框图18.3.1 中断控制 18.4 UART 初始化结构体详解18.4.1 baudRate_Bps18.4.2 parityMode18.4.3 dataBitsCount18.4.4 isMsb18.4.5 stopBitCount18.4.6 txFifoWatermark与rxFifo…

JAVA开发入门

文章目录 计算机基本概念DOS常用命令JAVA语言发展史JDK下载JAVA体系与特点JDK安装JAVA环境变量配置 计算机基本概念 计算机组成原理 计算机组装 计算机&#xff1a;电子计算机&#xff0c;俗称电脑。是一种能够按照程序运行&#xff0c;自动、高速处理海量数据的现代化智能电子…

MATLAB中simulink中scope同时显示两个输入信号

在使用scope时&#xff0c;需要两个输入信号的设置方法 1.点开scope图标 2 点击设置按钮&#xff0c; 然后弹出configuration properties&#xff1a;scope配置图&#xff0c;在Main选项下&#xff0c;在Number of input ports&#xff1a;1这里面更改数字&#xff0c;需要几…

菜狗速递 快人一步

菜狗速递物流管理系统是一款针对网点管理人员开发的系统。 网点管理人员可以在该系统上进行员工信息的录入以及职能分配&#xff0c; 并能对客户的包裹进行一系列的处理&#xff0c;帮助客户查询订单信息&#xff0c;处理问题包裹等。 技术栈 基础框架&#xff1a;SpringBo…

Surface mesh结构学习

CGAL 5.6 - Surface Mesh: User Manual Surface_mesh 类是半边数据结构的实现&#xff0c;可用来表示多面体表面。它是半边数据结构&#xff08;Halfedge Data Structures&#xff09;和三维多面体表面&#xff08;3D Polyhedral Surface&#xff09;这两个 CGAL 软件包的替代品…

(菜鸟自学)搭建虚拟渗透实验室——安装Windows 7 靶机

安装Windows 7 靶机 新建一台虚拟机&#xff0c;并选择Windows 7系统 虚拟机基本配置如下 为虚拟机挂载Windows7的镜像 点击开启虚拟机&#xff0c;将进入安装程序&#xff0c;按如下步骤进行操作&#xff1a; 点击“下一步”》勾选“我接受许可条款”&#xff0c;点击“下…

YOLOv8 Ultralytics:使用Ultralytics框架进行MobileSAM图像分割

YOLOv8 Ultralytics&#xff1a;使用Ultralytics框架进行MobileSAM图像分割 前言相关介绍前提条件实验环境安装环境项目地址LinuxWindows 使用Ultralytics框架进行MobileSAM图像分割参考文献 前言 由于本人水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评改正。更多精彩…

【python】进阶--->MySQL数据库(三)

一、修改列的类型长度及约束 alter table 表名 modify 列名 类型(长度) [约束];修改列名 : alter table 表名 change 旧列名 新列名 类型(长度) [约束];二、数据查询语言 查询表中所有的数据 : select * from 表名; 查询表中部分列数据 : select 列名1, 列名2 from 表名;1. …

进程等待-wait和waitpid

目录 1.为什么要进程等待 2.如何进程等待&#xff1f; 1.wait函数 2.waitpid 1.pid 2.status 3.options/非堵塞轮询 1.为什么要进程等待 1.子进程退出&#xff0c;如果父进程没有退出&#xff0c;那么子进程就会变成僵尸状态。僵尸状态会变得刀枪不入&#xff0c;kiil -…

如何使用vite框架封装一个js库,并发布npm包

目录 前言介绍 一、创建一个vite项目 1、使用创建命令&#xff1a; 2、选择others 3、 模板选择library 4、选择开发语言 ​编辑 二、安装依赖 三、目录介绍 1、vite.config.ts打包文件 2、package.json命令配置文件 三、发布npm 1、注册npm账号 2、设置npm源 3、登…

【模型评估 05】Holdout、交叉检验、自助法

机器学习中&#xff0c;我们通常把样本分为训练集和测试集&#xff0c;训练集用于训练模型&#xff0c;测试集用于评估模型。在样本划分和模型验证的过程中&#xff0c;存在着不同的抽样方法和验证方法。 1. 在模型评估过程中&#xff0c;有哪些主要的验证方法&#xff0c;它们…

Debian(Linux)局域网共享文件-NFS

NFS (Network File system) 是一种客户端-服务器文件系统协议&#xff0c;允许多个系统或用户访问相同的共享文件夹或文件。最新版本是 NFS-V4&#xff0c;共享文件就像存储在本地一样。它提供了中央管理&#xff0c;可以使用防火墙和 Kerberos 身份验证进行保护。 本文将指导…

系列四、Spring Security中的认证 授权(前后端不分离)

一、Spring Security中的认证 & 授权&#xff08;前后端不分离&#xff09; 1.1、MyWebSecurityConfigurerAdapter /*** Author : 一叶浮萍归大海* Date: 2024/1/11 21:50* Description:*/ Configuration public class MyWebSecurityConfigurerAdapter extends WebSecuri…

安卓应用无法拉起部分机型微信支付

错误提示&#xff1a; 2024-01-11 09:01:01.878 11754-11754 MicroMsg.S...ApiImplV10 com.bm.read E register app failed for wechat app signature check failed 2024-01-11 09:01:01.879 11754-11754 MicroMsg.S...ApiImplV10 com.bm.read E s…

建站为什么需要服务器?(Web服务器与计算机对比)

​  在部署网站时&#xff0c;底层基础设施在确保最佳性能、可靠性和可扩展性方面发挥着至关重要的作用。虽然大多数人都熟悉个人计算机 (PC) 作为日常工作和个人任务的设备&#xff0c;但 PC 和 Web 服务器之间存在显著差异。在这篇文章中&#xff0c;我们将讨论这些差异是什…

CentOS:docker容器日志清理

1.先查看磁盘空间 df -h 2.找到容器的containerId-json.log文件,并清理 find /var/lib/docker/containers/ -name *-json.log |xargs du -sh 3、可以根据需求清理对应日志也可以清理数据大的日志 $ cat /dev/null > /var/lib/docker/containers/dbaee0746cc6adad3768b4ef…