spring-security 学习笔记一 --- 基于默认配置

1.前言

本文主要讲解 spring-security 在不做任何配置情况下,它的启动流程和认证过程。

1. 准备工作

这里是基于springboot 2.2.5版本对应 spring-security 5.2.2版本演示的 (按我下面导入即可,版本是它自己匹配的)

  1. 引入依赖

    <properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.2.5.RELEASE</spring-boot.version>
    </properties><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>
    
  2. 编写程序

  • 这里我们直接创建一个springboot 启动程序即可,可以不做任何配置,在导入spring-security 依赖后,它会自动给我们进行了一些默认配置。我在这里简单配置了一下端口号

    # 应用名称
    spring.application.name=spring-security
    # 应用服务 WEB 访问端口
    server.port=8082
    
  • 编写一个controller

    @RestController
    public class HelloController {@GetMapping("/hello")public String hello() {return "hello";}
    }
    
  1. 启动项目
    在这里插入图片描述

2. 访问 /hello

在这里插入图片描述我们访问 http://localhost:8082/hello , 发现它会给我们重定向到 http://localhost:8082/login 这个页面。

-思考:我们只写了一个处理 /hello 的方法

2. 流程分析

  • 我们都知道spring-security 主要是基于 一层层的 Filters 来对web 请求做处理的,完成其中的认证授权等一系列功能。

1. 自定义一个Filter

@Component
public class MyFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("进入自定义的filter");filterChain.doFilter(servletRequest,servletResponse);}
}

我们自定义的过滤器逻辑很简单,只做一个简单的输出 (在这个地方打个断点,方便我们后续调试),然后放行,把它添加到容器中

@Configuration
public class MyConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {super.configure(http);// 这个方法表示把我们自定义的过滤器 加在 UsernamePasswordAuthenticationFilter 这个过滤器之前http.addFilterBefore(new MyFilter(), UsernamePasswordAuthenticationFilter.class);}
}

2. debug模式重启项目,访问 http://localhost:8082/login ,随便输入参数,提交表单

在这里插入图片描述

  • 我们点开filterChain(过滤器链,主要是用来管理过滤器的),我们发现除掉我们自定义的过滤器,它自己给我们添加了十五个 过滤器

至于为什么是这个顺序?

FilterComparator() {FilterComparator.Step order = new FilterComparator.Step(100, 100);this.put(ChannelProcessingFilter.class, order.next());this.put(ConcurrentSessionFilter.class, order.next());this.put(WebAsyncManagerIntegrationFilter.class, order.next());this.put(SecurityContextPersistenceFilter.class, order.next());this.put(HeaderWriterFilter.class, order.next());this.put(CorsFilter.class, order.next());this.put(CsrfFilter.class, order.next());this.put(LogoutFilter.class, order.next());this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", order.next());this.filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter", order.next());this.put(X509AuthenticationFilter.class, order.next());this.put(AbstractPreAuthenticatedProcessingFilter.class, order.next());this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", order.next());this.filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter", order.next());this.put(UsernamePasswordAuthenticationFilter.class, order.next());this.put(ConcurrentSessionFilter.class, order.next());this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());this.put(DefaultLoginPageGeneratingFilter.class, order.next());this.put(DefaultLogoutPageGeneratingFilter.class, order.next());this.put(ConcurrentSessionFilter.class, order.next());this.put(DigestAuthenticationFilter.class, order.next());this.filterToOrder.put("org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next());this.put(BasicAuthenticationFilter.class, order.next());this.put(RequestCacheAwareFilter.class, order.next());this.put(SecurityContextHolderAwareRequestFilter.class, order.next());this.put(JaasApiIntegrationFilter.class, order.next());this.put(RememberMeAuthenticationFilter.class, order.next());this.put(AnonymousAuthenticationFilter.class, order.next());this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", order.next());this.put(SessionManagementFilter.class, order.next());this.put(ExceptionTranslationFilter.class, order.next());this.put(FilterSecurityInterceptor.class, order.next());this.put(SwitchUserFilter.class, order.next());}

3. WebAsyncManagerIntegrationFilte

  • GenericFilterBean 子类,将Security上下文与Spring Web中用于处理异步请求映射的 WebAsyncManager(spring 中用于管理异步请求的核心类) 进行集成。

4. SecurityContextPersistenceFilter

  • GenericFilterBean 子类,在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信息存储到一个仓储中,然后将SecurityContextHolder中的信息清除
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;// FILTER_APPLIED : 如果有值,说明当前请求已经被这个 Filter 拦截过,直接放行if (request.getAttribute(FILTER_APPLIED) != null) {// ensure that filter is only applied once per requestchain.doFilter(request, response);return;}final boolean debug = logger.isDebugEnabled();// 给 FILTER_APPLIED 设置一个值,表示当前请求已被 当前Filter 处理过 request.setAttribute(FILTER_APPLIED, Boolean.TRUE);// private boolean forceEagerSessionCreation = false;// 默认是false ,  日志记录一下 早起创建的sessionif (forceEagerSessionCreation) {HttpSession session = request.getSession();if (debug && session.isNew()) {logger.debug("Eagerly created session: " + session.getId());}}// 创建一个 HttpRequestResponseHolder HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,response);// private SecurityContextRepository repo; 默认实现是 : HttpSessionSecurityContextRepository// 初次进入的时候,它会先尝试从session 中获取,第一次获取不到,它会创建一个默认的没有权限认证的  SecurityContext// 认证通过后 它会保存在sesion 中,下次进入可以直接取出来SecurityContext contextBeforeChainExecution = repo.loadContext(holder);try {// 把从对应的 SecurityContextRepository  获取的securityContext存入SecurityContextHolder中 SecurityContextHolder.setContext(contextBeforeChainExecution);// 放行chain.doFilter(holder.getRequest(), holder.getResponse());}// 这里代码 是当当前请求被所有过滤器处理完毕后,才会执行的finally {// 从 SecurityContextHolder 中获取 所有Filter执行完毕后的 SecurityContext SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();// Crucial removal of SecurityContextHolder contents - do this before anything// else.// 清除 SecurityContextHolder 中的 SecurityContextSecurityContextHolder.clearContext();// 把 前面获取的 SecurityContext  保存到 SecurityContextRepository reporepo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());// 清除掉这个标记,下次进入还会拦截		request.removeAttribute(FILTER_APPLIED);if (debug) {logger.debug("SecurityContextHolder now cleared, as request processing completed");}}}

5. HeaderWriterFilter

GenericFilterBean 子类,主要是处理请求头信息的

@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {// shouldWriteHeadersEagerly : 指示是否需要在请求开始前,写入请求头数据if (this.shouldWriteHeadersEagerly) {// 如果需要的话 就写入请求头放行doHeadersBefore(request, response, filterChain);} else {// 如果不需要,它会把请求包装一下,等所有过滤器链执行完毕后,在写入请求头信息doHeadersAfter(request, response, filterChain);}}private void doHeadersBefore(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {writeHeaders(request, response);filterChain.doFilter(request, response);}private void doHeadersAfter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request,response);HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request,headerWriterResponse);try {filterChain.doFilter(headerWriterRequest, headerWriterResponse);} finally {headerWriterResponse.writeHeaders();}}

6. CsrfFilter

GenericFilterBean 子类,用于处理跨站请求伪造

protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {request.setAttribute(HttpServletResponse.class.getName(), response);// 从token 仓库中查看这个请求是否携带有 csrfToken CsrfToken csrfToken = this.tokenRepository.loadToken(request);final boolean missingToken = csrfToken == null;// 如果没有的话,就给这个 request 创建一个默认的token,uuid随机生成的if (missingToken) {csrfToken = this.tokenRepository.generateToken(request);this.tokenRepository.saveToken(csrfToken, request, response);}// 把token 信息设置到 请求中request.setAttribute(CsrfToken.class.getName(), csrfToken);request.setAttribute(csrfToken.getParameterName(), csrfToken);// 决定策略实现的规则是否与提供的请求匹配。 如果匹配直接放行if (!this.requireCsrfProtectionMatcher.matches(request)) {filterChain.doFilter(request, response);return;}// 根据前面设置的csrfToken ,从请求头中获取名为csrfToken.getHeaderName()的属性值 默认"X-CSRF-TOKEN"String actualToken = request.getHeader(csrfToken.getHeaderName());// 如果为null的话 就会从请求参数中获取if (actualToken == null) {actualToken = request.getParameter(csrfToken.getParameterName());}// 这里会拿 actualToken  和 前面创建的默认的 csrfToken的token 进行比对,如果不相等,说明这里被修改过 打印日志,回显异常if (!csrfToken.getToken().equals(actualToken)) {if (this.logger.isDebugEnabled()) {this.logger.debug("Invalid CSRF token found for "+ UrlUtils.buildFullRequestUrl(request));}if (missingToken) {this.accessDeniedHandler.handle(request, response,new MissingCsrfTokenException(actualToken));}else {this.accessDeniedHandler.handle(request, response,new InvalidCsrfTokenException(csrfToken, actualToken));}return;}// 走到这里说明校验通过,放行filterChain.doFilter(request, response);}

7. LogoutFilter

主要是用来处理登出的。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;// 确认是否进行登出操作,里面主要是一些路径方法匹配if (requiresLogout(request, response)) {// SecurityContextHolder的容器中的一些权限认证信息Authentication auth = SecurityContextHolder.getContext().getAuthentication();if (logger.isDebugEnabled()) {logger.debug("Logging out user '" + auth+ "' and transferring to logout destination");}// 这个LogoutHandler是一个复合handler 里面维护了一个  List<LogoutHandler> // 遍历执行每一个 LogoutHandler 的登出操作,处理各种认证信息this.handler.logout(request, response, auth);// 默认情况下是一个 SimpleUrlLogoutSuccessHandler,处理重定向信息logoutSuccessHandler.onLogoutSuccess(request, response, auth);return;}// 如果没有登出操作直接放行。chain.doFilter(request, response);}

8. UsernamePasswordAuthenticationFilter

这个应该是我们大家比较了解,也是重写比较多的一个 Filter,用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理post方式“/login”的请求。
从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {// 校验它是 post 方式if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}// 从request 中获取 参数名为 username,和 password 对应的参数String username = obtainUsername(request);String password = obtainPassword(request);// 当它们是null时,给他们一个 ""字符串if (username == null) {username = "";}if (password == null) {password = "";}username = username.trim();// 创建一个 UsernamePasswordAuthenticationToken 这里会保存用户的权限认证等信息UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);// 默认会得到 一个 ProviderManager,调用它的 authenticate(),因为我们没配置他第一次进入是会 // 从 InMemoryUserDetailsManager的一个 map中根据用户名查找用户信息,用户名:user,密码:前面启动项目打印的那个// 拿着这个查找后的用户信息,和我们输入的密码进行比对,相同则认证成功,否则失败return this.getAuthenticationManager().authenticate(authRequest);}

this.getAuthenticationManager()
-----》ProviderManager
----》result = provider.authenticate(authentication)
-----》 AbstractUserDetailsAuthenticationProvider
----》UserDetails user = this.userCache.getUserFromCache(username); 先从缓存中判断没有再往下执行
----》 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
—》DaoAuthenticationProvider
—》UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
—》InMemoryUserDetailsManager
—》UserDetails user = (UserDetails)this.users.get(username.toLowerCase()); 这个users是一个map,通过username 从这里面获取 用户信息
// private final Map<String, MutableUserDetails> users = new HashMap();
—》接着回到 AbstractUserDetailsAuthenticationProvider
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); 检查当前用户密码和从map中查到的信息是否匹配

9. DefaultLoginPageGeneratingFilter

这个是当我们没有配置登录页面时,系统会在启动的时候帮我们加入这个过滤器,帮我们生成一个默认的登录页面。

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.getBytes(StandardCharsets.UTF_8).length);response.getWriter().write(loginPageHtml);return;}

10 DefaultLogoutPageGeneratingFilter

当我们没有配置退出登录页面时,生成一个的用户退出登录页面,默认情况下,当用户请求为GET /logout时,该过滤器会起作用,生成并展示相应的用户退出登录表单页面。

@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {if (this.matcher.matches(request)) {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"+ 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);}

11 BasicAuthenticationFilter

这个拦截器主要是处理请求头上认证信息,如果有,则通过basic64 编码器解密请求头所携带的信息,
封装成一个 UsernamePasswordAuthenticationToken 对象,然后比对认证是否成功,认证成功的话把认证成功的信息放到当前上下文中。

protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain chain)throws IOException, ServletException {final boolean debug = this.logger.isDebugEnabled();try {// 尝试获取请求头封装的信息返回的 UsernamePasswordAuthenticationToken ,若为null,直接放行,详解见下面UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);// 如果没有直接返回if (authRequest == null) {chain.doFilter(request, response);return;}// 获取用户名String username = authRequest.getName();if (debug) {this.logger.debug("Basic Authentication Authorization header found for user '"+ username + "'");}// 仅当用户名与SecurityContextHolder和用户不匹配时或者用户名没经过认证 返回true 需进行验证if (authenticationIsRequired(username)) {// 具体的校验逻辑 可自己重写,认证成功后把它放到容器中即可。Authentication authResult = this.authenticationManager.authenticate(authRequest);if (debug) {this.logger.debug("Authentication success: " + authResult);}SecurityContextHolder.getContext().setAuthentication(authResult);this.rememberMeServices.loginSuccess(request, response, authResult);onSuccessfulAuthentication(request, response, authResult);}}catch (AuthenticationException failed) {SecurityContextHolder.clearContext();if (debug) {this.logger.debug("Authentication request for failed: " + failed);}this.rememberMeServices.loginFail(request, response);onUnsuccessfulAuthentication(request, response, failed);if (this.ignoreFailure) {chain.doFilter(request, response);}else {this.authenticationEntryPoint.commence(request, response, failed);}return;}chain.doFilter(request, response);}public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {//  public static final String AUTHORIZATION = "Authorization";String header = request.getHeader(AUTHORIZATION);// 如果这个 Authorization 请求头为null 直接返回。if (header == null) {return null;}//  获取 Authorization Basic 后面的信息header = header.trim();if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {return null;}// base64 解密请求头认证信息byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);byte[] decoded;try {decoded = Base64.getDecoder().decode(base64Token);}catch (IllegalArgumentException e) {throw new BadCredentialsException("Failed to decode basic authentication token");}// getCredentialsCharset(request) 默认 UTF-8String token = new String(decoded, getCredentialsCharset(request));int delim = token.indexOf(":");if (delim == -1) {throw new BadCredentialsException("Invalid basic authentication token");}// 构造这个对象 UsernamePasswordAuthenticationToken  最后返回UsernamePasswordAuthenticationToken result  = new UsernamePasswordAuthenticationToken(token.substring(0, delim), token.substring(delim + 1));result.setDetails(this.authenticationDetailsSource.buildDetails(request));return result;}

12 RequestCacheAwareFilter

主要作用是用于用户登录成功后,重新恢复因为登录被打断的请求,被打断也是有前提条件的,支持打断后可以被恢复的异常有AuthenticationException、AccessDeniedException,这个操作是ExceptionTranslationFilter中触发的,并且RequestCacheAwareFilter只支持GET方法,而默认TokenEndpoint支持Post获取Token信息,进行登录.

public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {// 匹配有没有需要恢复登录的请求,有的话直接替换到请求路径HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest((HttpServletRequest) request, (HttpServletResponse) response);chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,response);}

13 SecurityContextHolderAwareRequestFilter

Spring Security TokenEndpoint中获取token的请求,有这样一个参数:Principal。 对于一个普通HttpServletRequest,是没有Principal参数类型的。SecurityContextHolderAwareRequestFilter通过HttpServletRequestFactory将HttpServletRequest请求包装成SecurityContextHolderAwareRequestWrapper,它实现了HttpServletRequest,并进行了扩展,添加一些额外的方法,比如:getPrincipal()方法等。这样就可以那些需要Principal等参数的Controller就可以接收到对应参数了。除了这个地方的应用,在其他地方,也可以直接调用request#getUserPrincipal()获取对应信息。

14 AnonymousAuthenticationFilter

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

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

相关文章

SpringBoot-@Transactional注解失效

Transactional注解失效 Transactional失效场景 以下是一些常见导致Transactional注解失效的场景&#xff0c;配合相应的Java代码演示&#xff1a; 1、方法修饰符非公开&#xff08;非public&#xff09; Transactional注解失效的原因在于Spring事务管理器如何实现对事务的代…

【跟马少平老师学AI】-【神经网络是怎么实现的】(七-2)word2vec模型

一句话归纳&#xff1a; 1&#xff09;CBOW模型&#xff1a; 2c个向量是相加&#xff0c;而不是拼接。 2&#xff09;CBOW模型中的哈夫曼树&#xff1a; 从root开始&#xff0c;向左为1&#xff0c;向右为0。叶子结点对应词有中的一个词。每个词对应唯一的编码。词编码不等长。…

Redis基本數據結構 ― String

Redis基本數據結構 ― String 介紹常用命令範例1. 為字串鍵設值/取得字串鍵的值2. 查看字串鍵的過期時間3. 如何為key設置時間?4. 如何刪除指定key?5. 如何增加value的值?6. 獲取value值的長度 介紹 字串鍵是Redis中最基本的鍵值對類型&#xff0c;這種類型的鍵值對會在數據…

Spring - 10 ( 9000 字 Spring 入门级教程 )

一&#xff1a;MyBatis 进阶 动态 SQL 是 Mybatis 的强大特性之⼀&#xff0c;能够完成不同条件下不同的 sql 拼接。 1.1 if 标签 在注册用户的时候&#xff0c;可能会有这样⼀个问题&#xff0c;如下图所示&#xff1a; 注册分为两种字段&#xff1a;必填字段和非必填字段&…

Vue ui 创建vue项目,详细使用攻略。

1.安装及启动 1.1 Vue ui 使用前提是全局安装vue.js 命令如下 npm install vue -g 1.2 安装过Vue.js 之后 随便在自己系统的一个地方打开命令面板 1.3 使用命令启动vue ui面板创建项目 vue ui 如图运行后显示这种就是启动成功&#xff0c;成功之后会弹出页面或者直接访问你的…

伦敦金的交易时间段都适合投资吗?

是所有的交易时间段都适合投资。首先&#xff0c;让我们了解伦敦金的交易时间。伦敦金市场的交易时间分为两个主要时段&#xff1a;亚洲盘和欧美盘。亚洲盘通常在北京时间早晨6点至下午5点半左右&#xff0c;而欧美盘则从北京时间晚上8点半开始&#xff0c;一直到次日早晨4点半…

痉挛性斜颈患者早上运动还是下午运动更合适?选对了让治疗更简单!【北京仁爱堂】

对于痉挛性斜颈患者来说&#xff0c;选择合适的运动时间对于治疗的效果和舒适度至关重要。那么&#xff0c;痉挛性斜颈患者早上运动还是下午运动更合适呢&#xff1f;本文将从多个角度对此进行分析&#xff0c;帮助患者找到最适合自己的运动时间。 首先&#xff0c;我们需要了…

特征提取(Feature Extraction)常见统计特征笔记(三)

统计特征是描述数据集中值的一组量&#xff0c;通常用于了解数据的分布、集中趋势和变异程度。常见的统计特征包括均值、中位数、众数、标准差、方差等。下面会详细解释每个统计特征&#xff0c;并给出相应的Python代码。 1、均值&#xff08;Mean&#xff09;&#xff1a;所有…

Python的使用

1、打印&#xff1a;print&#xff08;‘hello’&#xff09; 2、Python的除法是数学意义上的除法 print&#xff08;2/3&#xff09; 输出&#xff1a;0.6666... 3、a18 a‘hello’ print(a) 可以直接输出 4、**2 表示2的平方 5、打印类型 print&#xff08;type&am…

答案自在你心——不求事事皆如愿,只求件件都无悔

时光荏苒&#xff0c;弹指间&#xff0c;似如梦初醒般又迎来了大学的第二个五一假期。多数人看来&#xff0c;又是一个千载难逢的度假时光&#xff0c;于我而言&#xff0c;心里却一直紧绷一根迟迟难以松弛的弦。也许是习惯了平常忙忙碌碌的生活节奏&#xff0c;突然停下来&…

linux下载压缩包

比如我要下载的压缩包地址为&#xff1a; http://calvin.inf.ed.ac.uk/wp-content/uploads/data/cocostuffdataset/cocostuff-10k-v1.1.zip 1.创建文件夹并切换到这个目录下 2.用wget获取压缩包 压缩包下好了 3.解压 如果是 tar.gz包解压 tar -zxvf 也可以解压到具体的目录…

手撸Mybatis(一)——代理mapper

引言 最近刚写完毕设&#xff0c;闲来无事&#xff0c;看到网上有一个手撸Mybatis的教程&#xff0c;于是想自己实现一个简易版的Mybatis。 本专栏的源码&#xff1a;https://gitee.com/dhi-chen-xiaoyang/yang-mybatis。 创建简单的映射器代理工厂 在使用mybatis的时候&…

Python语言在地球科学中地理、气象、气候变化、水文、生态、传感器等数据可视化到常见数据分析方法的使用

Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;Python能够运行在Linux、Windows、Macintosh、AIX操作系统上及不同平台&#xff08;x86和arm&#xff09;&#xff0c;Python简洁的语法和对动态输入的支持&#xff0c;再加上解释性语言的本质&…

doxygen 辅助阅读代码的神器

简介 Doxygen是一个文档生成工具&#xff0c;主要用于编写编程语言的软件文档。它最初是为C设计的&#xff0c;但后来增加了对C、C#、Java、Objective-C、Python、IDL&#xff08;在某些情况下还有PHP、C#和D&#xff09;的支持。Doxygen可以从一组带有文档注释的源代码文件中…

Mamba3D革新3D点云分析:超越Transformer,提升本地特征提取效率与性能!

DeepVisionary 每日深度学习前沿科技推送&顶会论文分享&#xff0c;与你一起了解前沿深度学习信息&#xff01; Mamba3D革新3D点云分析&#xff1a;超越Transformer&#xff0c;提升本地特征提取效率与性能&#xff01; 引言&#xff1a;3D点云分析的重要性与挑战 3D点云…

软件工程毕业设计选题100例

文章目录 0 简介1 如何选题2 最新软件工程毕设选题3 最后 0 简介 学长搜集分享最新的软件工程业专业毕设选题&#xff0c;难度适中&#xff0c;适合作为毕业设计&#xff0c;大家参考。 学长整理的题目标准&#xff1a; 相对容易工作量达标题目新颖 1 如何选题 最近非常多的…

ubuntu ros noetic 编译 ORB_SLAM2 过程记录

1. 连接 eigen库 sudo ln -s /usr/include/eigen3/Eigen /usr/include/Eigen 2. opencvx 修改 CMakeList.txt 中的 find_package open cv版本 修改 include/orbExtracter.h 文件为&#xff1a; //#include <opencv2/opencv.hpp> #include<opencv2/imgproc/imgpro…

【深入浅出MySQL】「性能调优」高性能查询优化MySQL的SQL语句编写

高性能查询优化MySQL的SQL语句编写准则这里写目录标题 总体优化大纲&#xff08;1&#xff09;优化查询性能&#xff1a;通过索引降低全表扫描频率优化方向案例介绍问题分析解决方案建立复合索引建立单独索引 &#xff08;2&#xff09;优化数据表与查询&#xff1a;合理使用非…

【C语言的完结】:最后的测试题

看到这句话的时候证明&#xff1a; 此刻你我都在努力~ 个人主页&#xff1a; Gu Gu Study ​​ 专栏&#xff1a;语言的起点-----C语言 喜欢的一句话&#xff1a; 常常会回顾努力的自己&#xff0c;所以要为自己的努力留下足迹…

PZK via OWF

参考文献&#xff1a; [SMP88] Santis A, Micali S, Persiano G. Non-Interactive Zero-Knowledge with Preprocessing[C]//Advances in Cryptology—CRYPTO’88.[LS90] Lapidot D, Shamir A. Publicly verifiable non-interactive zero-knowledge proofs[C]//Advances in Cry…