当用 Spring Security 框架进行认证时,你可能会遇到这样的问题:
你输入的用户名或密码不管是空还是错误,它的错误信息都是 Bad credentials。
那么如果你想根据不同的情况给出相应的错误提示该怎么办呢?
这个时候我们只有了解 Spring Securiy 认证的流程才能知道如何修改代码。
好啦,来看下面的例子,大部分人的 WebSecurityConfig 的 configure 代码都类似于下:
@Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub http .authorizeRequests() .anyRequest().permitAll() .and() .formLogin().loginPage("/signin") .usernameParameter("username") .passwordParameter("password") .loginProcessingUrl("/signin") .and() .csrf().disable(); }
相信以上代码大家都知道什么意思:任何请求信息都允许,也就是不需要身份认证。
登录页面请求为 /signin,用户名和密码参数的name属性分别是 username,password。登录页面 form 的 action 请求为 /signin。
当然这个 action 不必和登录页面请求一样。最后的那个是禁止跨站请求伪造。
这段代码和登录认证联系较大的应该是从 loginPage() 到 loginProcessingUrl() 里的方法。
咱先从 loginPage 看起,鼠标左键拖动覆盖 loginPage,然后右键 Open Declaration 就进入到了 FormLoginConfigurer 类。
这个类里值得注意的方法有两个:构造方法和 loginPage 方法。
public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), null); usernameParameter("username"); passwordParameter("password"); } public FormLoginConfigurer<H> loginPage(String loginPage) { return super.loginPage(loginPage); }
构造方法中使用了一个用户名密码认证过滤器类,这一看就和认证有关系。
loginPage 方法大家可以自行按照这个步骤查看,现在直接看 UsernamePasswordAuthenticationFilter 类。
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; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
这只是其中一部分代码,其他的可以自己看。该类中定义的两个字符串和构造方法定义了默认的登录方式。
登录 action 请求为以 POST 方式的 /login,用户名及密码分别以 username,password 属性值获取。
该类的父类的父类 GenericFilterBean 实现了 InitializingBean 接口,也就是会初始化为一个 Bean。
当看到 attemptAuthentication 时,就知道他是认证的方法啦。
这里咱直接看到 new UsernamePasswordAuthenticationToken(username, password);
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); }
从这里可以知道,它把用户名和密码分别存在了 principal,credentials 里。
现在我们只需要记住登录信息存在了 authRequest 里。现在来看下setDetails,虽然我不感兴趣。
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); }
它调用了一个 buildDetails 方法,实际上是调用的:(追根溯源可以看到)
/**
* Records the remote address and will also set the session Id if a session already
* exists (it won't create one).
*
* @param request that the authentication request was received from
*/
public WebAuthenticationDetails(HttpServletRequest request) { this.remoteAddress = request.getRemoteAddr(); HttpSession session = request.getSession(false); this.sessionId = (session != null) ? session.getId() : null; }
从源码注释可以看到,它是记录远程地址并且会设置一个会话 ID,这里我们不管它了。
直接看这一句:return this.getAuthenticationManager().authenticate(authRequest);
它调用的是一个实现了 AuthenticationManager 接口的类的 authenticate 方法。
从源码中我们找不到它用的是哪个实现类,网上说是 ProviderManager 类,我们来看一下该类。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. try { result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request } catch (AuthenticationException e) { lastException = parentException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it if (parentException == null) { prepareException(lastException, authentication); } throw lastException; } }
这里我只给出该类的声明和 authenticate 方法,从类的声明可以看出来它也会初始化为一个 Bean,咱找不到很正常对吧。
authenticate 方法会遍历所有的 AuthenticationProvider ,然后调用 provider 的 authenticate 方法。
如果认证结果不为空的话将会保存到 result 中,并且擦除认证信息再返回 result。
为空的话一般是没有提供 AuthenticationProvider,会报 ProviderNotFoundException 错误。
现在我们来看下 provider 的 authenticate 方法。
@Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new CustomAuthenticationProvider(); provider.setMessageSource(messageSource); provider.setUserDetailsService(userService); provider.setPasswordEncoder(new BCryptPasswordEncoder()); return provider; }
这个是我写的一个 AuthenticationProvider,只不过我重写了一个类继承了 DaoAuthenticationProvider。
这里我们来看 DaoAuthenticationProvider 类:(这个类里面并没有发现 authenticate 方法,那先从它的父类找)
父类是 AbstractUserDetailsAuthenticationProvider,它也实现了 InitializingBean 接口,也是初始化为一个 Bean。
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // Determine username String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
在这段代码中可以知道:如果 authentication.getPrincipal() 为空的话,username 将会为 NONE_PROVIDED。
不为空的话将会得到 authentication.getPrincipal(),也就是用户名,只是这种类型不是 String 类型,但可以强制转换。
代码中是 authentication.getName(),这种和上面基本一样,只不过该类型是 String 类型的。
然后定义一个 user,先尝试从缓存中获取 user,没获取到的话就通过 retrieveUser 获取。
该类中 retrieveUser 是一个抽象方法,我们现在来看 DaoAuthenticationProvider 类里的方法。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
从代码中可以看到是通过我们之前写的 UserDetailsService 方法获取用户。
接下来我们看后面的代码,这部分异常代码我们等会再看。
try { preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); }
这两句代码是对用户进行检查的,第一行代码调用的其实是这部分的:
private class DefaultPreAuthenticationChecks implements UserDetailsChecker { public void check(UserDetails user) { if (!user.isAccountNonLocked()) { logger.debug("User account is locked"); throw new LockedException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.locked", "User account is locked")); } if (!user.isEnabled()) { logger.debug("User account is disabled"); throw new DisabledException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); } if (!user.isAccountNonExpired()) { logger.debug("User account is expired"); throw new AccountExpiredException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } }
可以看到并不是检查密码的,只是对用户状态进行检查。那么我们不管它了,看下一行代码:
@SuppressWarnings("deprecation") protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
这里有个获取密码的操作:authentication.getCredentials()。
然后如果密码不为空的话就通过 passwordEncoder.matches(presentedPassword, userDetails.getPassword() 检查是否匹配。
如果匹配成功的话,嗯,这部分结束了,我们回到 AbstractUserDetailsAuthenticationProvider 类里的 authenticate 方法。
return createSuccessAuthentication(principalToReturn, authentication, user);
它会返回一个创建成功认证方法的返回值。这里我们就不管了。
现在我们先回到AbstractUserDetailsAuthenticationProvider 类的错误处理上:
try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } }
这个是用户找不到引起的错误,我们看下 messages.getMessage():
public String getMessage(String code, String defaultMessage) { String msg = this.messageSource.getMessage(code, null, defaultMessage, getDefaultLocale()); return (msg != null ? msg : ""); }
再来看下这个里面的 getMessage():
它是一个接口类里的方法:根据 code 返回 messageSource 里的字符串,如果不存在这个 code,就返回 defaultMessage。
既然是个接口类,那我们看下它的实现类,回到 messageSource,查看一下它:
public class SpringSecurityMessageSource extends ResourceBundleMessageSource { // ~ Constructors // =================================================================================================== public SpringSecurityMessageSource() { setBasename("org.springframework.security.messages"); } // ~ Methods // ======================================================================================================== public static MessageSourceAccessor getAccessor() { return new MessageSourceAccessor(new SpringSecurityMessageSource()); } }
原来是从这个路径里找数据源。
其他的错误处理也是一样,这里就省略了。那我们如何获取错误信息呢?
@Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub http .authorizeRequests() .anyRequest().permitAll() .and() .formLogin().loginPage("/signin") .usernameParameter("username") .passwordParameter("password") .loginProcessingUrl("/signin") .failureHandler(authenticationFailureHandler) .and() .csrf().disable();
看到那个 failureHandler 没,这个是登录失败处理器,这里加上只是看一下里面源码:AbstractAuthenticationFilterConfigurer
/** * Specifies the { @link AuthenticationFailureHandler} to use when authentication * fails. The default is redirecting to "/login?error" using * { @link SimpleUrlAuthenticationFailureHandler} * * @param authenticationFailureHandler the { @link AuthenticationFailureHandler} to use * when authentication fails. * @return the { @link FormLoginConfigurer} for additional customization */ public final T failureHandler( AuthenticationFailureHandler authenticationFailureHandler) { this.failureUrl = null; this.failureHandler = authenticationFailureHandler; return getSelf(); }
从注释中可以看出默认的失败处理器是 SimpleUrlAuthenticationFailureHandler:
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if (defaultFailureUrl == null) { logger.debug("No failure URL set, sending 401 Unauthorized error"); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } else { saveException(request, exception); if (forwardToDestination) { logger.debug("Forwarding to " + defaultFailureUrl); request.getRequestDispatcher(defaultFailureUrl) .forward(request, response); } else { logger.debug("Redirecting to " + defaultFailureUrl); redirectStrategy.sendRedirect(request, response, defaultFailureUrl); } } }
因为默认的 defaultFailureUrl 为 /login?error,从 AbstractAuthenticationFilterConfigurer 类里可以看出来。
登录失败后,会调用 saveException(request, exception); 保存错误信息。
protected final void saveException(HttpServletRequest request, AuthenticationException exception) { if (forwardToDestination) { request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); } else { HttpSession session = request.getSession(false); if (session != null || allowSessionCreation) { request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); } } }
由于该类中 forwardToDestination 为 false,它将执行 else 里的语句。
将错误信息保存到会话的 WebAttributes.AUTHENTICATION_EXCEPTION 属性中:
public static final String AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION";
所有我们可以通过会话的这个属性来获取错误信息。(thymeleaf)
(注意:signin.html 不能放在 static 目录下,不然获取不到错误信息。)
<p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>
好啦,都介绍完了,可以看下我的 CustomAuthenticationProvider:
package security.config; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.util.Assert; public class CustomAuthenticationProvider extends DaoAuthenticationProvider { @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // TODO Auto-generated method stub String presentedPassword = authentication.getCredentials().toString(); if (!getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "UNameOrPwdIsError","Username or Password is not correct")); } } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // TODO Auto-generated method stub Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); if("".equals(authentication.getPrincipal())) { throw new BadCredentialsException(messages.getMessage( "UsernameIsNull","Username cannot be empty")); } if("".equals(authentication.getCredentials())) { throw new BadCredentialsException(messages.getMessage( "PasswordIsNull","Password cannot be empty")); } String username = (String) authentication.getPrincipal(); boolean cacheWasUsed = true; UserDetails user = this.getUserCache().getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "UNameOrPwdIsError","Username or Password is not correct")); } else { throw notFound; } } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { getPreAuthenticationChecks().check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); getPreAuthenticationChecks().check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } getPostAuthenticationChecks().check(user); if (!cacheWasUsed) { this.getUserCache().putUserInCache(user); } Object principalToReturn = user; if (isForcePrincipalAsString()) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); } }
这里值得注意的是 "".equals(authentication.getPrincipal()),"".equals(authentication.getCredentials())
因为如果按照那个 AbstractUserDetailsAuthenticationProvider 类来写的话,发现这一步永不为 null。
我通过加入代码 System.out.println(username); 才知道的,应该是个坑吧。
项目代码可供大家参考:
链接:https://pan.baidu.com/s/1pNWQMyIgZOzX5_rF3Tvd2A
提取码:m585
文章浏览阅读90次。【代码】js-选项卡原理。_选项卡js原理
文章浏览阅读67次。原型模式是一种对象创建型模式,它采用复制原型对象的方法来创建对象的实例。它创建的实例,具有与原型一样的数据结构和值分为深度克隆和浅度克隆。浅度克隆:克隆对象的值类型(基本数据类型),克隆引用类型的地址;深度克隆:克隆对象的值类型,引用类型的对象也复制一份副本。UML图:具体代码:浅度复制:import java.util.List;/*..._prototype 设计模式
文章浏览阅读59次。入选国内首批云计算服务创新发展试点城市的北京、上海、深圳、杭州和无锡起到了很好的示范作用,不仅促进了当地产业的升级换代,而且为国内其他城市发展云计算产业提供了很好的借鉴。据了解,目前国内至少有20个城市确定将云计算作为重点发展的产业。这势必会形成新一轮的云计算基础设施建设的**。由于云计算基础设施建设具有投资规模大,运维成本高,投资回收周期长,地域辐射性强等诸多特点,各地在建...
文章浏览阅读9.4k次,点赞2次,收藏20次。一、功能及目的 在每个STM32的芯片上都有两个管脚BOOT0和BOOT1,这两个管脚在芯片复位时的电平状态决定了芯片复位后从哪个区域开始执行程序。BOOT1=x BOOT0=0 // 从用户闪存启动,这是正常的工作模式。BOOT1=0 BOOT0=1 // 从系统存储器启动,这种模式启动的程序_stm32boot0和boot1作用
文章浏览阅读3.4k次,点赞2次,收藏22次。C语言函数递归调用_c语言函数递归调用
文章浏览阅读410次。明日方舟bilibili服是一款天灾驾到战斗热血的创新二次元废土风塔防手游,精妙的二次元纸片人设计,为宅友们源源不断更新超多的纸片人老婆老公们,玩家将扮演废土正义一方“罗德岛”中的指挥官,与你身边的感染者们并肩作战。与同类塔防手游与众不同的几点,首先你可以在这抽卡轻松获得稀有,同时也可以在战斗体系和敌军走位机制看到不同。明日方舟bilibili服设定:1、起因不明并四处肆虐的天灾,席卷过的土地上出..._明日方舟抽卡模拟器
文章浏览阅读437次。Maven上传Jar到私服报错:ReasonPhrase: Repository version policy: SNAPSHOT does not allow version: xxx_repository version policy snapshot does not all
文章浏览阅读1.2k次。斐波那契数列(Fibonacci Sequence)是由如下形式的一系列数字组成的:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …上述数字序列中反映出来的规律,就是下一个数字是该数字前面两个紧邻数字的和,具体如下所示:示例:比如上述斐波那契数列中的最后两个数,可以推导出34后面的数为21+34=55下面是一个更长一些的斐波那契数列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,_斐波那契日
文章浏览阅读363次。PHP必会面试题1. 基础篇1. 用 PHP 打印出前一天的时间格式是 2017-12-28 22:21:21? //>>1.当前时间减去一天的时间,然后再格式化echo date('Y-m-d H:i:s',time()-3600*24);//>>2.使用strtotime,可以将任何字符串时间转换成时间戳,仅针对英文echo date('Y-m-d H:i:s',str..._//该层循环用来控制每轮 冒出一个数 需要比较的次数
文章浏览阅读1.3k次,点赞26次,收藏26次。windows下用mingw编译opencv貌似不支持cuda,选cuda会报错,我无法解决,所以没选cuda,下面两种编译方式支持。打开cmake gui程序,在下面两个框中分别输入opencv的源文件和编译目录,build-mingw为你创建的目录,可自定义命名。1、如果已经安装Qt,则Qt自带mingw编译器,从Qt安装目录找到编译器所在目录即可。1、如果已经安装Qt,则Qt自带cmake,从Qt安装目录找到cmake所在目录即可。2、若未安装Qt,则安装Mingw即可,参考我的另外一篇文章。_opencv mingw contrib
文章浏览阅读10w+次,点赞42次,收藏309次。今天给大家推荐5个好用且免费的简历模板网站,简洁美观,非常值得收藏!1、菜鸟图库https://www.sucai999.com/search/word/0_242_0.html?v=NTYxMjky网站主要以设计类素材为主,办公类素材也很多,简历模板大部个偏简约风,各种版式都有,而且经常会更新。最重要的是全部都能免费下载。2、个人简历网https://www.gerenjianli.com/moban/这是一个专门提供简历模板的网站,里面有超多模板个类,找起来非常方便,风格也很多样,无须注册就能免费下载,_hoso模板官网
文章浏览阅读142次。你听说过吗?该计划可让您以推广您的产品并在成功销售时支付佣金。它提供了新的营销渠道,使您的产品呈现在更广泛的受众面前并提高品牌知名度。此外,TikTok Shop联盟可以是一种经济高效的产品或服务营销方式。您只需在有人购买时付费,因此不存在在无效广告上浪费金钱的风险。这些诱人的好处是否足以让您想要开始您的TikTok Shop联盟活动?如果是这样,本指南适合您。_tiktok联盟