Spring Security 06 Rember Me

news2024/11/25 12:51:29

        

目录

基本使用

原理分析 

RememberMeServices

TokenBasedRememberMeServices 

总结

内存令牌 

PersistentTokenBasedRememberMeServices

使用内存中令牌实现

持久化令牌

自定义记住我 

自定义认证类 LoginFilter

自定义 RememberMeService

配置记住我

        RememberMe 是一种服务器端的行为。传统的登录方式基于 Session会话,一旦用户的会话超时过期,就要再次登录,这样太过于烦琐。如果能有一种机制,让用户会话过期之后,还能继续保持认证状态,就会方便很多,RememberMe 就是为了解决这一需求而生的。

实现思路就是通过 Cookie 来记录当前用户身份。当用户登录成功之后,会通过一定算法,将用户信息、时间戳等进行加密,加密完成后,通过响应头带回前端存储在cookie中,当浏览器会话过期之后,如果再次访问该网站,会自动将 Cookie 中的信息发送给服务器,服务器对 Cookie中的信息进行校验分析,进而确定出用户的身份,Cookie中所保存的用户信息也是有时效的,例如三天、一周等。

基本使用

   @Bean
    public UserDetailsService userDetailsService(){
        UserDetails user = User.withUsername("admin").password("{noop}123").roles("ADMIN").build();
        return new InMemoryUserDetailsManager(user);
    }
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().rememberMe() // 开启 记住我
                .userDetailsService(userDetailsService());
        return http.csrf().disable().build();
    }

原理分析 

如果自定义登录页面开启 RememberMe 功能应该多加入一个一样的请求参数就可以啦。该请求会被 RememberMeAuthenticationFilter进行拦截然后自动登录

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            this.logger.debug(LogMessage
                    .of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
                            + SecurityContextHolder.getContext().getAuthentication() + "'"));
            chain.doFilter(request, response);
            return;
        }
        Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
        if (rememberMeAuth != null) {
            // Attempt authenticaton via AuthenticationManager
            try {
                rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
                // Store to SecurityContextHolder
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                context.setAuthentication(rememberMeAuth);
                SecurityContextHolder.setContext(context);
                onSuccessfulAuthentication(request, response, rememberMeAuth);
                this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
                        + SecurityContextHolder.getContext().getAuthentication() + "'"));
                this.securityContextRepository.saveContext(context, request, response);
                if (this.eventPublisher != null) {
                    this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                            SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
                }
                if (this.successHandler != null) {
                    this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
                    return;
                }
            }
            catch (AuthenticationException ex) {
                this.logger.debug(LogMessage
                        .format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
                                + "rejected Authentication returned by RememberMeServices: '%s'; "
                                + "invalidating remember-me token", rememberMeAuth),
                        ex);
                this.rememberMeServices.loginFail(request, response);
                onUnsuccessfulAuthentication(request, response, ex);
            }
        }
        chain.doFilter(request, response);
    }
  • 请求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没值的话表示用户尚未登录,此时调用 autoLogin 方法进行自动登录。
  • 当自动登录成功后返回的rememberMeAuth 不为null 时,表示自动登录成功,此时调用 authenticate 方法对 key 进行校验,并且将登录成功的用户信息保存到 SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含 RememberMeServices 中的 1oginSuccess 方法。
  • 如果自动登录失败,则调用 remenberMeServices.loginFail方法处理登录失败回调。onUnsuccessfulAuthentication 和 onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现这就是 RememberMeAuthenticationFilter 过滤器所做的事情,成功将 RememberMeServices的服务集成进来。

RememberMeServices

public interface RememberMeServices {
​
​
    // 从请求中提取出需要的参数,完成自动登录功能。
    Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
​
    // 自动登录失败的回调
    void loginFail(HttpServletRequest request, HttpServletResponse response);
​
    // 自动登录成功的回调
    void loginSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication successfulAuthentication);
    
}

TokenBasedRememberMeServices 

在开启记住我后如果没有加入额外配置默认实现就是由TokenBasedRememberMeServices进行的实现。查看这个类源码中 processAutoLoginCookie 方法实现:

@Override
    protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
            HttpServletResponse response) {
        if (cookieTokens.length != 3) {
            throw new InvalidCookieException(
                    "Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        }
        long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
        if (isTokenExpired(tokenExpiryTime)) {
            throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
                    + "'; current time is '" + new Date() + "')");
        }
        // Check the user exists. Defer lookup until after expiry time checked, to
        // possibly avoid expensive database call.
        UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
        Assert.notNull(userDetails, () -> "UserDetailsService " + getUserDetailsService()
                + " returned null for username " + cookieTokens[0] + ". " + "This is an interface contract violation");
        // Check signature of token matches remaining details. Must do this after user
        // lookup, as we need the DAO-derived password. If efficiency was a major issue,
        // just add in a UserCache implementation, but recall that this method is usually
        // only called once per HttpSession - if the token is valid, it will cause
        // SecurityContextHolder population, whilst if invalid, will cause the cookie to
        // be cancelled.
        String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
                userDetails.getPassword());
        if (!equals(expectedTokenSignature, cookieTokens[2])) {
            throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
                    + "' but expected '" + expectedTokenSignature + "'");
        }
        return userDetails;
    }

processAutoLoginCookie 方法主要用来验证 Cookie 中的令牌信息是否合法:

  1. 首先判断 cookieTokens 长度是否为了,不为了说明格式不对,则直接抛出异常。
  2. 从cookieTokens 数组中提取出第 1项,也就是过期时间,判断令牌是否过期,如果己经过期,则拋出异常。
  3. 根据用户名 (cookieTokens 数组的第。项)查询出当前用户对象。
  4. 调用 makeTokenSignature 方法生成一个签名,签名的生成过程如下:首先将用户名、令牌过期时间、用户密码以及 key 组成一个宇符串,中间用“:”隔开,然后通过 MD5 消息摘要算法对该宇符串进行加密,并将加密结果转为一个字符串返回。
  5. 判断第4 步生成的签名和通过 Cookie 传来的签名是否相等(即 cookieTokens 数组的第2项),如果相等,表示令牌合法,则直接返回用户对象,否则拋出异常。
    @Override
    public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication successfulAuthentication) {
        String username = retrieveUserName(successfulAuthentication);
        String password = retrievePassword(successfulAuthentication);
        // If unable to find a username and password, just abort as
        // TokenBasedRememberMeServices is
        // unable to construct a valid token in this case.
        if (!StringUtils.hasLength(username)) {
            this.logger.debug("Unable to retrieve username");
            return;
        }
        if (!StringUtils.hasLength(password)) {
            UserDetails user = getUserDetailsService().loadUserByUsername(username);
            password = user.getPassword();
            if (!StringUtils.hasLength(password)) {
                this.logger.debug("Unable to obtain password for user: " + username);
                return;
            }
        }
        int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
        long expiryTime = System.currentTimeMillis();
        // SEC-949
        expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
        String signatureValue = makeTokenSignature(expiryTime, username, password);
        setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request,
                response);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(
                    "Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
        }
    }
  1. 在这个回调中,首先获取用户经和密码信息,如果用户密码在用户登录成功后从successfulAuthentication对象中擦除,则从数据库中重新加载出用户密码。
  2. 计算出令牌的过期时间,令牌默认有效期是两周。
  3. 根据令牌的过期时间、用户名以及用户密码,计算出一个签名。
  4. 调用 setCookie 方法设置 Cookie, 第一个参数是一个数组,数组中一共包含三项。用户名、过期时间以及签名,在setCookie 方法中会将数组转为字符串,并进行 Base64编码后响应给前端。

总结

        当用户通过用户名/密码的形式登录成功后,系统会根据用户的用户名、密码以及令牌的过期时间计算出一个签名,这个签名使用 MD5 消息摘要算法生成,是不可逆的。然后再将用户名、令牌过期时间以及签名拼接成一个字符串,中间用“:” 隔开,对拼接好的字符串进行Base64 编码,然后将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。当会话过期之后,访问系统资源时会自动携带上Cookie中的令牌,服务端拿到 Cookie中的令牌后,先进行 Bae64解码,解码后分别提取出令牌中的三项数据:接着根据令牌中的数据判断令牌是否已经过期,如果没有过期,则根据令牌中的用户名查询出用户信息:接着再计算出一个签名和令牌中的签名进行对比,如果一致,表示会牌是合法令牌,自动登录成功,否则自动登录失败。

 

内存令牌 

 

PersistentTokenBasedRememberMeServices

protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        if (cookieTokens.length != 2) {
            throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        } else {
            String presentedSeries = cookieTokens[0];
            String presentedToken = cookieTokens[1];
            PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
            if (token == null) {
                throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
            } else if (!presentedToken.equals(token.getTokenValue())) {
                this.tokenRepository.removeUserTokens(token.getUsername());
                throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
            } else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
                throw new RememberMeAuthenticationException("Remember-me login has expired");
            } else {
                this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'", token.getUsername(), token.getSeries()));
                PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
​
                try {
                    this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
                    this.addCookie(newToken, request, response);
                } catch (Exception var9) {
                    this.logger.error("Failed to update token: ", var9);
                    throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
                }
​
                return this.getUserDetailsService().loadUserByUsername(token.getUsername());
            }
        }
    }
  1. 不同于 TokonBasedRemornberMeServices 中的 processAutologinCookie 方法,这里cookieTokens 数组的长度为2,第一项是series,第二项是 token。
  2. 从cookieTokens数组中分到提取出 series 和 token. 然后根据 series 去内存中查询出一个 PersistentRememberMeToken对象。如果查询出来的对象为null,表示内存中并没有series对应的值,本次自动登录失败。如果查询出来的 token 和从 cookieTokens 中解析出来的token不相同,说明自动登录会牌已经泄漏(恶意用户利用令牌登录后,内存中的token变了),此时移除当前用户的所有自动登录记录并抛出异常。
  3. 根据数据库中查询出来的结果判断令牌是否过期,如果过期就抛出异常。
  4. 生成一个新的 PersistentRememberMeToken 对象,用户名和series 不变,token 重新生成,date 也使用当前时间。newToken 生成后,根据 series 去修改内存中的 token 和 date(即每次自动登录后都会产生新的 token 和 date)
  5. 调用 addCookie 方法添加 Cookie, 在addCookie 方法中,会调用到我们前面所说的setCookie 方法,但是要注意第一个数组参数中只有两项:series 和 token(即返回到前端的令牌是通过对 series 和 token 进行 Base64 编码得到的)
  6. 最后将根据用户名查询用户对象并返回。

使用内存中令牌实现

   @Bean
    public UserDetailsService userDetailsService(){
        UserDetails user = User.withUsername("admin").password("{noop}123").roles("ADMIN").build();
        return new InMemoryUserDetailsManager(user);
    }
​
    @Bean
    public RememberMeServices rememberMeServices() {
        return new PersistentTokenBasedRememberMeServices(
                "key",//参数 1: 自定义一个生成令牌 key 默认 UUID
                userDetailsService(), //参数 2:认证数据源
                new InMemoryTokenRepositoryImpl());//参数 3:令牌存储方式
    }
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().rememberMe()
                .userDetailsService(userDetailsService())
                .rememberMeServices(rememberMeServices());
        return http.csrf().disable().build();
    }

持久化令牌

@Configuration
public class WebSecurityConfig {
​
​
    private final DataSource dataSource;
​
    public WebSecurityConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }
​
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 项目启动时创建表。第一次启动后注释掉即可
        jdbcTokenRepository.setCreateTableOnStartup(true);
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }
​
    @Bean
    public UserDetailsService userDetailsService(){
        UserDetails user = User.withUsername("admin").password("{noop}123").roles("ADMIN").build();
        return new InMemoryUserDetailsManager(user);
    }
​
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().rememberMe()
                .userDetailsService(userDetailsService())
                .tokenRepository(persistentTokenRepository());
        return http.csrf().disable().build();
    }
​
}

即使服务器重新启动,依然可以自动登录。

自定义记住我 

自定义认证类 LoginFilter

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
​
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("========================================");
        // 1. 判断请求方式
        if (!request.getMethod().equals("POST")){
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
​
        // 2.判断是否是 json 格式请求类型
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){
            // 3.从 json 数据中获取用户输入用户名和密码进行认证 {"uname":"xxx","password":"xxx","remember-me":true}
            try {
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                String rememberValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
                if (!ObjectUtils.isEmpty(rememberValue)) {
                    request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue);
                }
                System.out.println("用户名: " + username + " 密码: " + password + " 是否记住我: " + rememberValue);
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}

自定义 RememberMeService

public class RememberMeService extends PersistentTokenBasedRememberMeServices {
​
    public RememberMeService(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
    }
​
    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        String paramValue = request.getAttribute(parameter).toString();
        if (paramValue != null) {
            return paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
                    || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1");
        }
        return false;
    }
}

配置记住我

@Configuration
public class WebSecurityConfig {
​
    private final DataSource dataSource;
​
    public WebSecurityConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }
​
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 项目启动时创建表。第一次启动后注释掉即可
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }
​
​
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername("admin").password("{noop}123").roles("ADMIN").build();
        return new InMemoryUserDetailsManager(user);
    }
​
​
    @Bean
    public RememberMeServices rememberMeServices() {
        return new RememberMeService(UUID.randomUUID().toString(), userDetailsService(), persistentTokenRepository());
    }
​
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
​
    @Bean
    public LoginFilter loginFilter(AuthenticationManager authenticationManager) {
        LoginFilter filter = new LoginFilter();
        filter.setUsernameParameter("username");
        filter.setPasswordParameter("password");
        filter.setFilterProcessesUrl("/doLogin");
​
        filter.setAuthenticationManager(authenticationManager);
        filter.setRememberMeServices(rememberMeServices());
​
        filter.setAuthenticationFailureHandler(new LoginFailureHandler());
        filter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
​
        return filter;
    }
​
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().exceptionHandling().authenticationEntryPoint(new UnAuthenticationHandler())
                .and().logout().logoutSuccessHandler(new LogoutHandler())
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository()) // 配置token持久化仓库
                .userDetailsService(userDetailsService())
                .and().addFilterBefore(loginFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class);
        return http.csrf().disable().build();
    }
​
}

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

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

相关文章

使用多线程执行任务,并获取返回结果,附异步实现

1 获取又返回结果的 需要用到 callable接口 public class TestTask implements Callable<Student> {Overridepublic Student call() throws Exception {Thread.sleep(1500);Student student new Student();student.setAge(10);student.setName("里里");Syste…

Ceph对象存储使用

文章目录 对象存储简介RadosGW简介RadosGW配置RGW使用的存储池配置rgw使用的http端口配置rgw使用https配置rgw高可用 客户端s3cmd测试数据读写创建rgw用户安装s3cmd客户端配置s3cmd访问rgw测试数据读写bucket授权 对象存储简介 对象存储是无层次结构的数据存储方法&#xff0c…

QT+OpenGL反射与折射

文章目录 QTOpenGL反射与折射反射折射 QTOpenGL反射与折射 本篇完整工程见gitee:QtOpenGL 对应点的tag&#xff0c;由turbolove提供技术支持&#xff0c;您可以关注博主或者私信博主 反射 反射这个属性表现为物体(或者物体的一部分)反射它周围的环境&#xff0c;即根据观察者…

【Python入门篇】——Python基础语法(字符串格式化,表达式格式化和数据输入)

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; Python入门&#xff0c;本专栏主要内容为Python的基础语法&#xff0c;Python中的选择循环语句…

STM32CubeMx+HAL库+小熊派+FreeRTOS+EasyLogger+Gitee+手把手教你

文章目录 1、创建工程配置RCC与SYS配置LED配置KEY配置串口生成工程 2、手动移植Freertos获取源码移植include移植portable移植src复制并修改FreeRTOSConfig.hkeil中添加路径与配置 3、移植EasyLogger获取源码复制easylogger文件keil中添加路径修改elog.h修改elog_port.c修改elo…

MacOS自定义安装 Python

Python 下载地址 官网下载太慢&#xff0c;如下是国内的镜像源&#xff0c;各版本都有&#xff1a;Python 国内镜像 下载好后缀是 tgz 的包进行解压 tar -zxvf Python-3.9.16.tgz进入目录并且对进行配置&#xff0c;编译(根据自己的目录进行调整) ./configure --with-opens…

Excel技能之图表,会Excel就能碾压程序员

数据可视化&#xff0c;让数据说话&#xff0c;需要Excel图表的呈现。你越用心&#xff0c;画的图越好看。 同一份数据&#xff0c;用不同的Excel图表展现出来&#xff0c;效果各有千秋。使用正确的图表&#xff0c;从一堆杂乱无章的数据中找出规律。不仅要知道怎么用图表&…

sentinel 随笔 1-流控

0. 想要个半个月的旅游 最近发现算法比较有意思一些&#xff0c;什么企业框架都是看不完的… 书接 FlowSlot 1. FlowRuleChecker.checkFlow() : 配置的规则校验类 sentinel 并没有对这个Checker进行抽象的设计&#xff0c;第一次看有些别扭… package com.alibaba.csp.sent…

矢量绘图UI设计Sketch

Sketch是一款Mac操作系统上常用的矢量图形编辑软件&#xff0c;旨在帮助用户设计和创建高质量的UI和UX界面。 软件安装&#xff1a;Sketch 中文 以下是Sketch软件的一些主要特点&#xff1a; 矢量工具和对象&#xff1a;Sketch提供了多种画线、填充、阴影、文本和形状等矢量工…

Illustrator如何使用图形对象的特殊效果之实例演示?

文章目录 0.引言1.制作毛球小怪物2.制作三维立体图形3.3D剪影球体艺术海报 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对Illustrator进行了学习&#xff0c;本文通过《Illustrator CC2018基础与实战》及其配套素材结合网上相关资料进行学习笔记总结&#xff0c;本…

Unity大面积草地渲染——1、Shader控制一棵草的渲染

大家好&#xff0c;我是阿赵。 这里开始讲大面积草地渲染的第一个部分&#xff0c;一棵草的渲染。按照惯例&#xff0c;完整shader在最后。前面是原理的介绍。 一、准备的资源 这里我自己随便做了一个草的模型&#xff0c;主要是用几个面片搭建的一个简单模型。 然后我准备…

DAP之FLM算法研究

本人所写的博客都为开发之中遇到问题记录的随笔,主要是给自己积累些问题。免日后无印象,如有不当之处敬请指正(欢迎进扣群 24849632 探讨问题); 写在专栏前面https://blog.csdn.net/Junping1982/article/details/129955766 玩过自制DAP工具的一定都知道通过MDK目录的FLM文…

priority_queue

priority_queue&#xff1a;优先队列 头文件还是 < queue> 本质就是堆&#xff1a;完全二叉树 条件&#xff08;任意节点都比其孩子大&#xff08;大根堆&#xff09;&#xff09; priority_queue的默认比较是less&#xff0c;但是建出来的是大根堆&#xff1b;sort排序…

顺序表的基本操作(初始化,增,删,查,改等等)

1.线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串...线性表在逻辑上是线性结构&#xff0c;也就说是连续的一条直线…

智能算法系列之蚁群算法

本博客封面由ChatGPT DALLE 2共同创作而成。 文章目录 前言1. 算法思想2. 算法流程3. 细节梳理4. 算法实现4.1 问题场景4.2 代码实现 代码仓库&#xff1a;IALib[GitHub] 前言 本篇是智能算法(Python复现)专栏的第五篇文章&#xff0c;主要介绍蚁群算法(Ant Colony Optimizati…

cmd@快捷键方式@静默执行命令

文章目录 ref快捷方式执行命令行或打开文件eg:直接打开某个文件 创建快捷方式eg:快捷方式运行命令 ref How can I execute a Windows command line in background? - Super Userstbrenner/SilentCMD: SilentCMD executes a batch file without opening the command prompt wi…

如何用100天时间,让CSDN的粉丝数从0狂飙到10000

2022年10月7日&#xff0c;正式开通了CSDN账号。但因为工作忙的原因&#xff0c;一直没有时间写博客文章&#xff0c;也没有投入精力在CSDN上。理所当然的&#xff0c;我的粉丝数量很稳定&#xff0c;一直保持着0的记录。 2023年春节假期过后&#xff0c;有点空闲时间了&#x…

Tre靶场通关过程(linpeas使用+启动项编辑器提权)

Tre靶场通关 通过信息收集获得到了普通用户账号密码&#xff0c;利用PEASS-ng的linpeas脚本进行提权的信息收集&#xff0c;根据已有信息进行提权。 靶机下载地址&#xff1a; https://download.vulnhub.com/tre/Tre.zip 信息收集 靶机IP探测&#xff1a;192.168.0.129 a…

java多线程下

ThreadLocal ThreadLocal 有什么用&#xff1f;通常情况下&#xff0c;我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢&#xff1f;JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就…

解释表情包This code will never do anything!

目录 解释一下下面这段代码 #include int main(){ while (1) ;} void unreachable(){ std::cout <<"Hello world!"<;}<> 解释一下下面这段代码 $ clang loop.cpp -01 -Wall -o loop $ ./loop Hello world! 解释一下下面这段代码 #include <iostre…