Spring Security基本认证(2)

news2024/11/7 5:30:18

前言:
对于安全管理框架而言,认证功能可以说是一切的起点,所以我们要研究Spring Security, 就要从最基本的认证开始。在Spring Security中,对认证功能做了大量的封装,以至于开发者只需要稍微配置一下就能使用认证功能,然而要深刻理解其源码却并非易事。本文从最基本的用法开始讲解,最终再扩展到对源码的理解。

本章涉及的主要知识点有:

  • Spring Security 基本认证。
  • 登录表单配置。
  • 登录用户数据获取。
  • 用户的四种定义方式。

1、Spring Security 基本认证

1.1、快速入门

在Spring Boot项目中使用Spring Security非常方便,创建一个新的Spring Boot项目,我 们只需要引入Web和Spring Security依赖即可,具体代码如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

然后我们在项目中提供一个用于测试的/hello接口,代码如下:

@RestController
public class HelloController {

	@GetMapping("/hello")
    public String hello() {
        return "hello spring security";
    }
    
}

接下来启动项目,/hello接口就已经被自动保护起来了。当用户访问/hello接口时,会自动跳转到登录页面,如图所示,用户登录成功后,才能访问到/hello接口。
在这里插入图片描述

默认的登录用户名是user,登录密码则是一个随机生成的UUID字符串,在项目启动日志中可以看到登录密码(这也意味着项目每次启动时,密码都会发生变化):

Using generated security password: 8ef9c800-17cf-47a3-9984-8ff936db6dd8

输入默认的用户名和密码,就可以成功登录了,这就是Spring Security的强大之处,只需要引入一个依赖,所有的接口就会被自动保护起来。

1.2、流程分析

通过一个简单的流程图来看一下上面案例中的请求流程,如下图所示:
在这里插入图片描述

流程图比较清晰地说明了整个请求过程:

  • 客户端(浏览器)发起请求去访问/hello接口,这个接口默认是需要认证之后才能访问的。
  • 这个请求会走一遍Spring Security中的过滤器链,在最后的FilterSecurityInterceptor 过滤器中被拦截下来,因为系统发现用户未认证。请求拦截下来之后,接下来会抛出 AccessDeniedException 异常。
  • 抛出的 AccessDeniedException 异常在 ExceptionTranslationFilter 过滤器中被捕获, ExceptionTranslationFilter 过滤器通过调用 LoginUrlAuthenticationEntiyPoint#commence 方法给客户端返回302,要求客户端重定向到/login页面。
  • 客户端发送/login请求。
  • /login请求被DefaultLoginPageGeneratingFilter过滤器拦截下来,并在该过滤器中返 回登录页面。所以当用户访问/hello接口时会首先看到登录页面。

在整个过程中,相当于客户端一共发送了两个请求,第一个请求是/hello,服务端收到之 后,返回302,要求客户端重定向到/login,于是客户端又发送了/login请求。现在去理解上面这一个流程图可能还有些困难,等学完后面的内容之后,再回过头来看这个流程图,应该就会比较清晰了。

1.3、原理分析

幵启Spring Security自动化配置,开启后,会自动创建一个名为springSecurityFilterChain 的过滤器,并注入到Spring容器中,这个过滤器将负责所有的安全管理,包括用户的认证、授权、重定向到登录页面等(springSecmityFilterChain实际上代理了 Spring Security中的过滤器链)。

  • 创建一个UserDetailsSeivice实例,UserDetailsService负责提供用户数据,默认的用户数据是基于内存的用户,用户名为user,密码则是随机生成的UUID字符串。
  • 给用户生成一个默认的登录页面。
  • 幵启CSRF攻击防御。
  • 开启会话固定攻击防御。
  • 集成 X-XSS-Protection
  • 集成X-Frame-Options以防止单击劫持。

这里涉及的细节还是非常多的,登录的细节会在后面详细介绍,这里主要分析一下默认用户的生成以及默认登录页面的生成。

1.3.1、默认用户生成

Spring Security中定义了 UserDetails接口来规范开发者自定义的用户对象,这样方便一些旧系统、用户表己经固定的系统集成到Spring Security认证体系中。

UserDetails接口定义如下:

public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

该接口中一共定义了 7个方法:

  • getAuthorities方法:返回当前账户所具备的权限。
  • getPassword方法:返回当前账户的密码。
  • getUsemame方法:返回当前账户的用户名。
  • isAccountNonExpired方法:返回当前账户是否未过期。
  • isAccountNonLocked方法:返回当前账户是否未锁定。
  • isCredentialsNonExpired方法:返回当前账户凭证(如密码)是否未过期。
  • isEnabled方法:返回当前账户是否可用。

这是用户对象的定义,而负责提供用户数据源的接口是UserDetailsService , UserDetailsService中只有一个查询用户的方法,代码如下:

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsemame有一个参数是username,这是用户在认证时传入的用户名,最常见的就是用户在登录表单中输入的用户名(实际开发时还可能存在其他情况,例如使用CAS单点登录时,username并非表单输入的用户名,而是CAS Server认证成功后回调的用户名参数), 开发者在这里拿到用户名之后,再去数据库中査询用户,最终返回一个UserDetails实例。

在实际项目中,一般需要开发者自定义UserDetailsSerace的实现。如果开发者没有自定义 UserDetailsService 的实现,Spring Security 也为 UserDetailsService 提供了默认实现,如下图:
在这里插入图片描述

  • UserDetailsManager在UserDetailsService的基础上,继续定义了添加用户、更新用户、 删除用户、修改密码以及判断用户是否存在共5种方法。
  • JdbcDaoImpl在UserDetailsService的基础上,通过spring-jdbc实现了从数据库中查询用户的方法。
  • InMemoryUserDetailsManager 实现了 UserDetailsManager 中关于用户的增删改查方法,不过都是基于内存的操作,数据并没有持久化。
  • JdbcUserDetailsManager 继承自 JdbcDaoImp 同时又实现了 UserDetailsManager接口,因此可以通过JdbcUserDetailsManager实现对用户的增删改查操作,这些操作都会持久化到数据库中。不过JdbcUserDetailsManager有一个局限性,就是操作数据库中用户的SQL 都是提前写好的,不够灵活,因此在实际开发中JdbcUserDetailsManager使用并不多。
  • CachingUserDetailsSeivice 的特点是会将 UserDetailsService 缓存起来。
  • UserDetailsSeiviceDelegator 则是提供了 UserDetailsService 的懒加载功能。
  • ReactiveUserDetailsSeiviceAdapter 是 webflux-web-security 模块定义的 UserDetailsService 实现。

当我们使用Spring Security时,如果仅仅只是引入一个Spring Security依赖,则默认使用的用户就是由 InMemoryUserDetailsManager 提供的。

大家知道,Spring Boot之所以能够做到零配置使用Spring Security,就是因为它提供了众多的自动化配置类,其中,针对UserDetailsService的自动化配置类是UserDetailsServiceAuto Configurationr这个类的源码并不长,我们一起来看一下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
		value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class },
		type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
				"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector" })
public class UserDetailsServiceAutoConfiguration {
 
	private static final String NOOP_PASSWORD_PREFIX = "{noop}";
 
	private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
 
	private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);
 
	@Bean
	@ConditionalOnMissingBean(
			type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
	@Lazy
	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
			ObjectProvider<PasswordEncoder> passwordEncoder) {
		SecurityProperties.User user = properties.getUser();
		List<String> roles = user.getRoles();
		return new InMemoryUserDetailsManager(
				User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
						.roles(StringUtils.toStringArray(roles)).build());
	}
 
	private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
		String password = user.getPassword();
		if (user.isPasswordGenerated()) {
			logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
		}
		if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
			return password;
		}
		return NOOP_PASSWORD_PREFIX + password;
	}
}

上述代码中可以看到,有两个比较重要的条件促使系统自动提供一个InMemoryUserDetailsManager 的实例:

  • (1) 当前 classpath 下存在 AuthenticationManager 类。
  • (2) 当前项目中,系统没有提供 AuthenticationManager、AutlienticationProvider、UserDetailsService 以及 ClientRegistrationRepository 实例。

默认情况下,上面的条件都会满足,此时Spring Security会提供一个InMemoryUserDetailsManager实例。从InMemoryUserDetailsManager方法中可以看到,用户数据源自 SecurityProperties#getUser 方法:

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
	public static final int BASIC_AUTH_ORDER = Ordered.LOWEST_PRECEDENCE - 5;
	public static final int IGNORED_ORDER = Ordered.HIGHEST_PRECEDENCE;
	public static final int DEFAULT_FILTER_ORDER = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100;
	private final Filter filter = new Filter();
	private User user = new User();
	
	public User getUser() {
		return this.user;
	}
	
	public Filter getFilter() {
		return this.filter;
	}
	
	public static class Filter {
		private int order = DEFAULT_FILTER_ORDER;
		private Set<DispatcherType> dispatcherTypes = new HashSet<>(
				Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
		public int getOrder() {
			return this.order;
		}
		public void setOrder(int order) {
			this.order = order;
		}
		public Set<DispatcherType> getDispatcherTypes() {
			return this.dispatcherTypes;
		}
		public void setDispatcherTypes(Set<DispatcherType> dispatcherTypes) {
			this.dispatcherTypes = dispatcherTypes;
		}
	}
	
	public static class User {
		private String name = "user";
		private String password = UUID.randomUUID().toString();
		private List<String> roles = new ArrayList<>();
		private boolean passwordGenerated = true;
		public String getName() {
			return this.name;
		}
		public void setName(String name) {
			this.name = name;
		}
		public String getPassword() {
			return this.password;
		}
		public void setPassword(String password) {
			if (!StringUtils.hasLength(password)) {
				return;
			}
			this.passwordGenerated = false;
			this.password = password;
		}
		public List<String> getRoles() {
			return this.roles;
		}
		public void setRoles(List<String> roles) {
			this.roles = new ArrayList<>(roles);
		}
		public boolean isPasswordGenerated() {
			return this.passwordGenerated;
		}
	}
	
}

从SecurityProperties.User类中,我们就可以看到默认的用户名是user,默认的密码是一个 UUID字符串。

再回到 InMemoryUserDetailsManager方法中,构造 InMemoryUserDetailsManager实例时需要一个 User对象。这里的 User对象不是SecurityProperties.User ,而是 org.springframework.security.core.userdetails.User, 这是 Spring Security 提供的一个实现了UserDetails接口的用户类,该类提供了相应的静态方法,用来构造一个默认的Uset实例。同时,默认的用户密码还在getOrDeducePassword方法中进行了二次处理,由于默认的encoder 为null,所以密码的二次处理只是给密码加了一个前缀{noop},表示密码是明文存储的(关于 {noop}将在后续密码加密中做详细介绍)。

经过以上的源码梳理,相信大家已经明白了 Spnng Security 默认的用户名/密码是来自哪里了!另外,当看了 Security Properties的源码后,只要对Spring Boot中properties属性的加载机制有一点了解,就会明白,只要我们在项目的application.properties配置文件中添加如下配置, 就能定制SecurityProperties.User类中各属性的值:

spring.security.user.name=username
spring.security.user.password=123
spring.security.user.roles=admin, user

配置完成后,重启项目,此时登录的用户名就是javaboy,登录密码就是123 ,登录成功后用户具备admin和user两个角色。

1.3.2、默认页面生成

在上面的案例中,一共存在两个默认页面,一个就是默认的登录页面,另外一个则是注销登录页面。当用户登录成功之后,在浏览器中输入http://localhost:8080/logout就可以看到注销登录页面,如图所示:
在这里插入图片描述

那么这两个页面是从哪里来的呢?这里剖析一下,

在前面我们介绍了 Spring Security中常见的过滤器,在这些常见的过滤器中就包含两个和页面相关的过滤器:DefaultLoginPageGeneratingFilter 和 DefaultLogoutPage Genera tingFilter;

通过过滤器的名字就可以分辨出DefaultLoginPageGeneratingFilter过滤器用来生成默认的登录页面,DefaultLogoutPageGeneratingFilter过滤器则用来生成默认的注销页面。

先来看 DefaultLoginPageGeneratingFilter 作为 Spring Security 过滤器链中的一员,在第一次请求/hello接口的时候,就会经过DefaultLoginPageGeneratingFilter过滤器,但是由于/hello 接口和登录无关,因此DefaultLoginPageGeneratingFilter过滤器并未干涉/hello接口,等到第二次重定向到/login页面的时候,这个时候就和DefaultLoginPageGeneratingFilter有关系了,此时请求就会在DefaultLoginPageGeneratingFilter中进行处理,生成登录页面返回给客户端。

我们来看一下DefaultLoginPageGeneratingFilter的源码,源码比较长,这里仅列出核心部分:

public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		boolean loginError = isErrorPage(request);
		boolean logoutSuccess = isLogoutSuccess(request);
		if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
			String loginPageHtml = generateLoginPageHtml(request, loginError,
					logoutSuccess);
			response.setContentType("text/html;charset=UTF-8");
			response.setContentLength(loginPageHtml.length());
			response.getWriter().write(loginPageHtml);
			return;
		}
		chain.doFilter(request, response);
	}
	
	private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
			boolean logoutSuccess) {
		String errorMsg = "none";
		if (loginError) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				AuthenticationException ex = (AuthenticationException) session
						.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
				errorMsg = ex != null ? ex.getMessage() : "none";
			}
		}
		StringBuilder sb = new StringBuilder();
		sb.append("<html><head><title>Login Page</title></head>");
		if (formLoginEnabled) {
			sb.append("<body οnlοad='document.f.").append(usernameParameter)
					.append(".focus();'>\n");
		}
		if (loginError) {
			sb.append("<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
			sb.append(errorMsg);
			sb.append("</p>");
		}
		if (logoutSuccess) {
			sb.append("<p style='color:green;'>You have been logged out</p>");
		}
		if (formLoginEnabled) {
			sb.append("<h3>Login with Username and Password</h3>");
			sb.append("<form name='f' action='").append(request.getContextPath())
					.append(authenticationUrl).append("' method='POST'>\n");
			sb.append("<table>\n");
			sb.append("	<tr><td>User:</td><td><input type='text' name='");
			sb.append(usernameParameter).append("' value='").append("'></td></tr>\n");
			sb.append("	<tr><td>Password:</td><td><input type='password' name='")
					.append(passwordParameter).append("'/></td></tr>\n");
			if (rememberMeParameter != null) {
				sb.append("	<tr><td><input type='checkbox' name='")
						.append(rememberMeParameter)
						.append("'/></td><td>Remember me on this computer.</td></tr>\n");
			}
			sb.append("	<tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
			renderHiddenInputs(sb, request);
			sb.append("</table>\n");
			sb.append("</form>");
		}
		if (openIdEnabled) {
			sb.append("<h3>Login with OpenID Identity</h3>");
			sb.append("<form name='oidf' action='").append(request.getContextPath())
					.append(openIDauthenticationUrl).append("' method='POST'>\n");
			sb.append("<table>\n");
			sb.append("	<tr><td>Identity:</td><td><input type='text' size='30' name='");
			sb.append(openIDusernameParameter).append("'/></td></tr>\n");
			if (openIDrememberMeParameter != null) {
				sb.append("	<tr><td><input type='checkbox' name='")
						.append(openIDrememberMeParameter)
						.append("'></td><td>Remember me on this computer.</td></tr>\n");
			}
			sb.append("	<tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
			sb.append("</table>\n");
			renderHiddenInputs(sb, request);
			sb.append("</form>");
		}
		if (oauth2LoginEnabled) {
			sb.append("<h3>Login with OAuth 2.0</h3>");
			sb.append("<table>\n");
			for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
				sb.append(" <tr><td>");
				sb.append("<a href=\"").append(request.getContextPath()).append(clientAuthenticationUrlToClientName.getKey()).append("\">");
				sb.append(clientAuthenticationUrlToClientName.getValue());
				sb.append("</a>");
				sb.append("</td></tr>\n");
			}
			sb.append("</table>\n");
		}
		sb.append("</body></html>");
		return sb.toString();
	}
	
}

DefaultLoginPageGeneratingFliter的源码执行流程还是非常清晰的,我们梳理一下:

  • 在doFilter方法中,首先判断出当前请求是否为登录出错请求、注销成功请求或者登录请求,如果是这三种请求中的任意一个,就会在DefaultLoginPageGeneratingFilter过滤器中生成登录页面并返回,否则请求继续往下走,执行下一个过滤器(这就是一开始的/hello请 求为什么没有被DefaultLoginPageGeneratingFilter拦截下来的原因)。
  • 如果当前请求是登录出错请求、注销成功请求或者登录请求中的任意一个,就会调用generateLoginPageHtml方法去生成登录页面。在该方法中,如果有异常信息就把异常信息 取出来一同返回给前端,然后根据不同的登录场景,生成不同的登录页面。生成过程其实就是字符串拼接,拼接岀不同的登录表单。
  • 登录页面生成后,接下来通过HttpServletResponse将登录页面写回到前端,然后调 用return方法跳出过滤器链。

这就是DefaultLoginPageGeneratingFilter的工作过程。这里重点搞明白为什么/hello请求没有被拦截,而/login请求却被拦截了,其他都很好懂。

理解了 DefaultLoginPageGeneratingFilter,再来看 DefaultLogoutPageGeneratingFilter 就更容易了,DefaultLogoutPageGeneratingFilter 部分核心源码如下 :

public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter {
    private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET");
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (this.matcher.matches(request)) {
            this.renderLogout(request, response);
        } else {
            filterChain.doFilter(request, response);
        }
    }
    private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String page = "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Confirm Log Out?</title>\n    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n  </head>\n  <body>\n     <div class=\"container\">\n      <form class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n        <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n" + this.renderHiddenInputs(request) + "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n      </form>\n    </div>\n  </body>\n</html>";
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(page);
    }
}

从上述源码中可以看出,请求到来之后,会先判断是否是注销请求/logout,如果是/logout 请求,则渲染一个注销请求的页面返回给客户端,渲染过程和前面登录页而的渲染过程类似, 也是字符串拼接(这里省略了字符串拼接,读者可以参考DefaultLogoutPageGeneratingFilter 的源码);否则请求继续往下走,执行下一个过滤器。

通过前面的分析,相信大家对这个简单的案例己经有所了解,看似只是加了一个依赖, 但实际上Spring Security和Spring Boot在背后都默默做了很多事情,当然还有很多没有介绍 到的,将在后面和大家一起继续深究。

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

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

相关文章

VBA_MF系列技术资料1-207

MF系列VBA技术资料 为了让广大学员在VBA编程中有切实可行的思路及有效的提高自己的编程技巧&#xff0c;我参考大量的资料&#xff0c;并结合自己的经验总结了这份MF系列VBA技术综合资料&#xff0c;而且开放源码&#xff08;MF04除外&#xff09;&#xff0c;其中MF01-04属于定…

TCP通信实战:模拟BS系统

1、之前的客户端都是什么样的 其实就是CS架构&#xff0c;客户端是需要我们自己开发实现的 2、BS结构是什么样的&#xff0c;需要开发客户端吗&#xff1f; 浏览器访问服务端&#xff0c;不需要开发客户端 注意&#xff1a;服务端必须给浏览器响应HTTP协议格式的数据&#xff0…

CNN——卷积神经网络

文章目录 多层感知机&#xff08;MLP&#xff0c;Multilayer Perceptron&#xff09;神经网络定义MLP与神经网络的异同相同之处&#xff1a;不同之处&#xff1a;总结 为什么要使用神经网络CNN卷积层&#xff1a;池化层&#xff1a;全连接层&#xff1a; 卷积神经网络的优势pad…

Java持久层框架:MyBatis介绍

MyBatis 概述 概述 MyBatis&#xff0c;是支持定制化 SQL 、存储过程和高级映射的优秀持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用 XML 或注解来配置和映射原生信息&#xff0c;将接口和 Java 的 POJOs&#xff08;Plain …

NVIDIA NCCL 源码学习(十一)- ring allreduce

之前的章节里我们看到了nccl send/recv通信的过程&#xff0c;本节我们以ring allreduce为例看下集合通信的过程。整体执行流程和send/recv很像&#xff0c;所以对于相似的流程只做简单介绍&#xff0c;主要介绍ring allreduce自己特有内容。 单机 搜索ring 在nccl初始化的过…

一道看似简单却易错的C语言的main函数的参数题目(海康威视的面试题)

一道海康的面试题&#xff0c;C语言的main函数有几个参数&#xff08;多选&#xff09; A&#xff1a;argc B&#xff1a;envp C&#xff1a;main D&#xff1a;argv 正确答案是&#xff1a;ABD 一般情况下&#xff0c;我们会认为是只有两个参数&#xff0c;因为从我们从第一…

JAVA栈、堆、方法区

一、什么是JAVA栈、堆、方法区 我们java程序的运行首先会先将.java的文件编译成.class文件&#xff0c;然后由JVM虚拟机的类加载器加载各个类的字节码文件到内存中进行执行&#xff0c;JVM虚拟机将这些数据加载到内存时会对内存进行划分为几个区域分别为栈、堆和方法区&#xf…

vue 02

目录简介 什么是渐进式框架&#xff1f; 就是一开始不需要你完全掌握它的全部功能特性&#xff0c;可以后续逐步增加功能。没有多做职责之外的事情。所以VUE的适用面很广&#xff0c;你可以用它代替老项目中的JQuery。也可以在新项目启动初期&#xff0c;有限的使用VUE的功能…

LLM ReAct: 将推理和行为相结合的通用范式 学习记录

LLM ReAct 什么是ReAct? LLM ReAct 是一种将推理和行为相结合的通用范式,可以让大型语言模型(LLM)根据逻辑推理(Reason),构建完整系列行动(Act),从而达成期望目标。LLM ReAct 可以应用于多种语言和决策任务,例如问答、事实验证、交互式决策等,提高了 LLM 的效率、…

论文导读 | 多模态知识图谱的构建

背景介绍 现有的知识图谱大多是以单一的文本的形式表示&#xff0c;而多模态知识图谱会将文本信息和图像等其他模态的信息综合起来。 多模态知识图谱主要分为两种表现形式&#xff0c;其一是将多模态信息作为实体的属性&#xff0c;另一种是将多模态信息作为单独的实体。 多…

uniapp接入萤石微信小程序插件

萤石官方提供了一些适用于uniapp / 小程序的方案 如 小程序半屏 hls rtmp 等 都TM有坑 文档写的依托答辩 本文参考了uniapp小程序插件 以及 萤石微信小程序插件接入文档 效果如下 1. 插件申请 登录您的小程序微信公众平台&#xff0c;点击左侧菜单栏&#xff0c;进入设置页…

QTday04(事件)

今日任务 代码&#xff1a; 头文件&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimerEvent> #include <QTime> #include <QTimer> #include <QMessageBox> #include <QTextToSpeech> #include <QD…

利用特殊反序列化组件攻击原生反序列化入口

目录 前言 本文所述攻击的本质是将上述组件中的类拼接到反序列化利用利用链中&#xff0c;打的是Serilizable入口&#xff0c;而不是特殊反序列化入口 攻击原理 利用链分析 readObject()->任意类toString() HotSwappableTargetSource & XString BadAttributeValue…

数据防泄密软件排行榜

数字化时代&#xff0c;数据已成为企业的重要资产。然而&#xff0c;数据泄露事件却时常发生&#xff0c;给企业带来巨大的经济损失和声誉风险。因此&#xff0c;数据防泄密软件的重要性日益凸显。 数据防泄密软件是什么 它是一种专门用于防止敏感数据泄露的软件工具。它通过对…

Java虚拟机常见面试题总结

梳理Java虚拟机相关的面试题&#xff0c;主要参考《深入理解Java虚拟机 JVM高级特性与最佳实践》(第2版, 周志明 著)一书&#xff0c;其余部分整合网络相关内容。注意&#xff0c;关于Java并发编程的面试题因为内容较多&#xff0c;单独整理。Java基础相关的面试题可以参考Java…

“懒宅经济”崛起,智能家电品牌快收好这份软文推广指南

目前&#xff0c;国内智能家电呈迅猛发展之势&#xff0c;"懒宅经济"崛起使智能小家电市场不断扩展&#xff0c;根据数据显示&#xff0c;目前购买智能家电的消费者大部分目的是为了节省时间&#xff0c;以及对新鲜事物有着强烈的好奇心&#xff0c;由此来看&#xf…

Pytorch从零开始实战06

Pytorch从零开始实战——明星识别 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——明星识别环境准备数据集模型选择开始训练模型可视化模型预测总结 环境准备 本文基于Jupyter notebook&#xff0c;使用Python3.8&#xff0c;Pytorch2.0.1c…

订水商城H5实战教程-01需求分析

目录 1 用户分析2 模块分析3 原型设计3.1 首页3.2 商城3.3 一键订购3.4 我的3.5 确认订单3.6 地址管理3.7 编辑地址3.8 搜索3.9 搜索结果3.10 充值3.11 我的订单3.12 开票信息3.13 优惠券3.14 我的空桶3.15 商品详情3.16 购物车3.17 门店信息3.18 订单详情 总结 生活中&#xf…

2023柏鹭杯 express fs

进去看看&#xff0c;发现有个file的参数 查看源码有个?filecheck.html&#xff0c;我们尝试?file/etc/passwd&#xff0c;发现可以直接访问任意文件&#xff0c;但是访问不到flag,可能被waf禁掉了 实际上node不能像php有伪协议可以绕&#xff0c;也没办法用什么编码绕过等&…