SpringSecurity——OAuth2框架鉴权实现源码分析

news2024/11/18 11:24:08

SpringSecurity——OAuth2框架鉴权实现源码分析

  • 一、ManagedFilter迭代过滤器链
    • 1.4 springSecurityFilterChain
      • 1.4.7 OAuth2AuthenticationProcessingFilter
        • ①.OAuth2AuthenticationProcessingFilter.class
        • ②.CookieTokenExtractor.class(我们自己重写的方法)
        • ③.BearerTokenExtractor.class -> extract()
        • ④.返回OAuth2AuthenticationProcessingFilter.class继续如下
        • ⑤.OAuth2AuthenticationDetailsSource.class
        • ⑥.OAuth2AuthenticationManager.class
        • ⑦.DefaultTokenServices.class -> loadAuthentication(String accessTokenValue)
        • ⑧.JwtTokenStore.class -> readAccessToken(String tokenValue)
        • ⑨.OAuth2JwtAccessTokenConverter -> decode(String token) 我们重写的方法,这里略
        • ⑩.JwtAccessTokenConverter.class -> decode(String token)
        • ⑾.DefaultAccessTokenConverter.class -> extractAccessToken()
        • ⑿.DefaultOAuth2AccessToken.class -> isExpired()

一、ManagedFilter迭代过滤器链

ManagedFilter管理这六条过滤器链,如图所示:
其中的第五条过滤器链springSecurityFilterChain是本文要讨论的对象。

0.characterEncodingFilter
1.WebMvcMetricsFilter
2.formContentFilter
3.requestContextFilter
4.springSecurityFilterChain
5.corsFilter

ManagedFilter.class -> doFilter()

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    if (this.servletContext.getDeployment().getDeploymentState() != State.STARTED) {
        throw UndertowServletMessages.MESSAGES.deploymentStopped(this.servletContext.getDeployment().getDeploymentInfo().getDeploymentName());
    } else {
        if (!this.started) {
            this.start();
        }

        this.getFilter().doFilter(request, response, chain);//迭代下一条过滤器链
    }
}

在这里插入图片描述

1.4 springSecurityFilterChain

在springSecurityFilterChain过滤器链中,首先初始化一个FilterChainProxy过滤器链代理对象,在这个过滤器链代理对象中有一个过滤器链集合,每一个过滤器链都有一组过滤器来处理不同的请求。
其中的第八条过滤器链OAuth2AuthenticationProcessingFilter是本文要讨论的对象,用于处理请求的鉴权。

FilterChainProxy.class --> doFilter()

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
    if (this.currentPosition == this.size) {
        if (FilterChainProxy.logger.isDebugEnabled()) {
            FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " reached end of additional filter chain; proceeding with original chain");
        }

        this.firewalledRequest.reset();
        this.originalChain.doFilter(request, response);
    } else {
        ++this.currentPosition;
        Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);//new了一个过滤器链代理对象
        if (FilterChainProxy.logger.isDebugEnabled()) {
            FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'");
        }

        nextFilter.doFilter(request, response, this);	//迭代过滤器
    }

}

在这里插入图片描述

1.4.7 OAuth2AuthenticationProcessingFilter

OAuth2框架鉴权的核心内容是OAuth2AuthenticationProcessingFilter过滤器。这个过滤器下涉及到的类有以下12个类,大致作用是从请求中提取authentication,从authentication中获取token,判断是否为空,是否有效,是否过期。具体主要包含1-66个步骤的跳转和调用,详见代码中的注释。

①.OAuth2AuthenticationProcessingFilter.class

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    boolean debug = logger.isDebugEnabled();
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;

    try {
        //1.从请求中提取authentication,跳到②
        Authentication authentication = this.tokenExtractor.extract(request);
        ......
    }

    chain.doFilter(request, response);
}

②.CookieTokenExtractor.class(我们自己重写的方法)

@Override
public Authentication extract(HttpServletRequest request) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null) {//2.从SecurityContextHolder中提取authentication
        return authentication;//3.如果拿到了,直接返回提取到的authentication
    }
    //4.如果没拿到,调用父类extract(request)方法,跳到③
    return super.extract(request);
    //22.带着token跳到④
}
@Override
protected String extractToken(HttpServletRequest request) {
    String result;
    //6.从cookie中提取authentication
    Cookie accessTokenCookie = OAuth2CookieHelper.getAccessTokenCookie(request);
    //7.如果拿到了,直接返回提取到的authentication
    if (accessTokenCookie != null) {
        result = accessTokenCookie.getValue();
    } else {
        //8.如果没拿到,调用父类extractToken(request)方法,跳到③
        result = super.extractToken(request);
    }
    //19.带着token,跳到C中的extract(HttpServletRequest request)
    return result;
}

③.BearerTokenExtractor.class -> extract()

public Authentication extract(HttpServletRequest request) {
    //5.调用子类中的extractToken(request),从请求中获取token
    String tokenValue = this.extractToken(request);
    //20.如果获取到了,包装token
    if (tokenValue != null) {
        PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
        //21.返回包装后的authentication,跳到②
        return authentication;
    } else {
        return null;
    }
}

protected String extractToken(HttpServletRequest request) {
    //9.调用本类中extractHeaderToken(request),从请求中获取token
    String token = this.extractHeaderToken(request);
    if (token == null) {
        logger.debug("Token not found in headers. Trying request parameters.");
        token = request.getParameter("access_token");
        if (token == null) {
            logger.debug("Token not found in request parameters.  Not an OAuth2 request.");
        } else {
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer");
        }
    }
	//18.带着token,跳到②
    return token;
}

protected String extractHeaderToken(HttpServletRequest request) {
    //10.通过请求头的key【Authorization】获取对应的值
    Enumeration<String> headers = request.getHeaders("Authorization");

    String value;
    do {//11.遍历获取到的值
        if (!headers.hasMoreElements()) {
            return null;
        }

        value = (String)headers.nextElement();
        //12.找到一个以"Bearer"开头的value,把这个value转换成全小写字母
    } while(!value.toLowerCase().startsWith("Bearer".toLowerCase()));
	//13.把这个value前面的"Bearer"裁掉并且去空格处理,存储到authHeaderValue中
    String authHeaderValue = value.substring("Bearer".length()).trim();
    //14.把OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE作为key,"Bearer"作为value存到request的attributte中
    request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim());
    //15.找到数字44在authHeaderValue中的下标
    int commaIndex = authHeaderValue.indexOf(44);
    if (commaIndex > 0) {
        //16.如果下标>0,也就是存在这个数字,就剪切开头到下标44所在的位置的值,重新赋给authHeaderValue
        authHeaderValue = authHeaderValue.substring(0, commaIndex);
    }
	//17.否则直接返回authHeaderValue,跳到本类中的extractToken(HttpServletRequest request)
    return authHeaderValue;
}

④.返回OAuth2AuthenticationProcessingFilter.class继续如下

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    boolean debug = logger.isDebugEnabled();
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;

    try {
        Authentication authentication = this.tokenExtractor.extract(request);
        //23.判断authentication是否为空,如果为空,也就是这个请求未携带token
        if (authentication == null) {
            //24.如果this.stateless等于true,同时	SecurityContextHolder.getContext().getAuthentication()中的authentication不为空而且不属于匿名用户的authentication
            if (this.stateless && this.isAuthenticated()) {
                if (debug) {
                    logger.debug("Clearing security context.");
                }
                SecurityContextHolder.clearContext();
            }

            if (debug) {
                logger.debug("No token in request, will continue chain.");
            }
        //25.如果请求中提取的authentication不为空
        } else {
          //26.把OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE作为key,authentication.getPrincipal()作为value存到request的attributte中
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
            //27.判断authentication的对象是否是AbstractAuthenticationToken的实例
            if (authentication instanceof AbstractAuthenticationToken) {
                //28.如果是,Authentication authentication向下转型为AbstractAuthenticationToken needsDetails
                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
                //29.为needsDetails的details属性赋值,跳到⑤
                needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
            }
			//31.跳到⑥,由authenticationManager把authentication包装后返回authResult,从⑥返回这里并开始下一行
            Authentication authResult = this.authenticationManager.authenticate(authentication);
            if (debug) {
                logger.debug("Authentication success: " + authResult);
            }
			//64.将鉴权通过的事件进行广播
            this.eventPublisher.publishAuthenticationSuccess(authResult);
            //65.将authResult存到SecurityContextHolder的context属性中
            SecurityContextHolder.getContext().setAuthentication(authResult);
        }
    } catch (OAuth2Exception var9) {
        SecurityContextHolder.clearContext();
        if (debug) {
            logger.debug("Authentication request failed: " + var9);
        }

        this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
        this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
        return;
    }
	//66.放行
    chain.doFilter(request, response);
}

private boolean isAuthenticated() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    return authentication != null && !(authentication instanceof AnonymousAuthenticationToken);
}

⑤.OAuth2AuthenticationDetailsSource.class

@Deprecated
public class OAuth2AuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, OAuth2AuthenticationDetails> {
    public OAuth2AuthenticationDetailsSource() {
    }

    public OAuth2AuthenticationDetails buildDetails(HttpServletRequest context) {
        //30.new 了一个OAuth2AuthenticationDetails对象,把request中的context存放进去,然后返回④
        return new OAuth2AuthenticationDetails(context);
    }
}

⑥.OAuth2AuthenticationManager.class

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    //32.判断authentication是否为空,为空就抛异常
    if (authentication == null) {
        throw new InvalidTokenException("Invalid token (token not found)");
    } else {
        //33.从authentication中获取token
        String token = (String)authentication.getPrincipal();
        //34.校验token的有效性和是否过期,跳到⑦
        OAuth2Authentication auth = this.tokenServices.loadAuthentication(token);
        if (auth == null) {
            throw new InvalidTokenException("Invalid token: " + token);
        } else {
            Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
            if (this.resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(this.resourceId)) {
                throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + this.resourceId + ")");
            } else {
                //61.调用本类中checkClientDetails(auth)方法
                this.checkClientDetails(auth);
                if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
                    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
                    if (!details.equals(auth.getDetails())) {
                        details.setDecodedDetails(auth.getDetails());
                    }
                }

                auth.setDetails(authentication.getDetails());
                auth.setAuthenticated(true);
                //63.返回处理之后的auth,跳到④
                return auth;
            }
        }
    }
}

private void checkClientDetails(OAuth2Authentication auth) {
    //62.暂时不知道干啥用的,啥也没干就返回了,跳回上面方法
    if (this.clientDetailsService != null) {
        ClientDetails client;
        try {
            client = this.clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
        } catch (ClientRegistrationException var6) {
            throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
        }

        Set<String> allowed = client.getScope();
        Iterator var4 = auth.getOAuth2Request().getScope().iterator();

        while(var4.hasNext()) {
            String scope = (String)var4.next();
            if (!allowed.contains(scope)) {
                throw new OAuth2AccessDeniedException("Invalid token contains disallowed scope (" + scope + ") for this client");
            }
        }
    }

}

⑦.DefaultTokenServices.class -> loadAuthentication(String accessTokenValue)

public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
    //35.从tokenStore中读取token,跳到⑧
    OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(accessTokenValue);
    //51.判断token是否为空,为空就抛异常
    if (accessToken == null) {
        throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        //53.判断token是否过期,过期就从tokenStore中移除token并且抛异常,跳到⑿
    } else if (accessToken.isExpired()) {
        this.tokenStore.removeAccessToken(accessToken);
        throw new InvalidTokenException("Access token expired: " + accessTokenValue);
    } else {
        //55.从tokenStore中解析token,跳到⑧
        OAuth2Authentication result = this.tokenStore.readAuthentication(accessToken);
        //59.结果为空就抛异常
        if (result == null) {
            throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        } else {
            if (this.clientDetailsService != null) {
                String clientId = result.getOAuth2Request().getClientId();

                try {
                    this.clientDetailsService.loadClientByClientId(clientId);
                } catch (ClientRegistrationException var6) {
                    throw new InvalidTokenException("Client not valid: " + clientId, var6);
                }
            }
			//60.把解析后的token返回⑥
            return result;
        }
    }
}

⑧.JwtTokenStore.class -> readAccessToken(String tokenValue)

public OAuth2AccessToken readAccessToken(String tokenValue) {
    //36.从本类中调用convertAccessToken(tokenValue)读取
    OAuth2AccessToken accessToken = this.convertAccessToken(tokenValue);
    //48.接收到返回的accessToken,调用this.jwtTokenEnhancer.isRefreshToken(accessToken)方法,跳到10
    if (this.jwtTokenEnhancer.isRefreshToken(accessToken)) {
        throw new InvalidTokenException("Encoded token is a refresh token");
    } else {
        //50.将token返回⑦
        return accessToken;
    }
}

private OAuth2AccessToken convertAccessToken(String tokenValue) {
    //37.调用this.jwtTokenEnhancer.decode(tokenValue)方法用公钥解码token,跳到⑨
    //41.调用this.jwtTokenEnhancer.extractAccessToken()方法,跳到⑩
    return this.jwtTokenEnhancer.extractAccessToken(tokenValue, this.jwtTokenEnhancer.decode(tokenValue));
    //47.接收到返回的token,并将其返回到上面的方法
    
}

public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
    //56.跳到下面的方法
    return this.readAuthentication(token.getValue());
    //58.带着token返回⑦
}

public OAuth2Authentication readAuthentication(String token) {
    //57.调用this.jwtTokenEnhancer.decode(token)方法,跳到⑨,并且把步骤38-47再走一遍,返回一个token,到上面的方法
    return this.jwtTokenEnhancer.extractAuthentication(this.jwtTokenEnhancer.decode(token));
}

⑨.OAuth2JwtAccessTokenConverter -> decode(String token) 我们重写的方法,这里略

@Override
protected Map<String, Object> decode(String token) {
    ......
    //38.调用父类的解码方法,跳到⑩
    return super.decode(token);
    //40.从10带着解析后的token返回⑧
	......
}

⑩.JwtAccessTokenConverter.class -> decode(String token)

protected Map<String, Object> decode(String token) {
    //39.把token解析为可以阅读的键值对并且返回⑨
    try {
        Jwt jwt = JwtHelper.decodeAndVerify(token, this.verifier);
        String claimsStr = jwt.getClaims();
        Map<String, Object> claims = this.objectMapper.parseMap(claimsStr);
        if (claims.containsKey("exp") && claims.get("exp") instanceof Integer) {
            Integer intValue = (Integer)claims.get("exp");
            claims.put("exp", new Long((long)intValue));
        }

        this.getJwtClaimsSetVerifier().verify(claims);
        return claims;
    } catch (Exception var6) {
        throw new InvalidTokenException("Cannot convert access token to JSON", var6);
    }
}

public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
    //42.跳到⑾
    return this.tokenConverter.extractAccessToken(value, map);
    //46.接收到返回的token,并将其返回到⑧
}

public boolean isRefreshToken(OAuth2AccessToken token) {
    //49.判断token的additionalInformation属性中是否包含叫做"ati"的key,并将结果返回8
    return token.getAdditionalInformation().containsKey("ati");
}

⑾.DefaultAccessTokenConverter.class -> extractAccessToken()

public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
    DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(value);
    Map<String, Object> info = new HashMap(map);
    info.remove("exp");
    info.remove("aud");
    info.remove(this.clientIdAttribute);
    info.remove(this.scopeAttribute);
    //43.把exp字段的数据(毫秒值)转换成日期类型数据
    if (map.containsKey("exp")) {
        token.setExpiration(new Date((Long)map.get("exp") * 1000L));
    }

    if (map.containsKey("jti")) {
        info.put("jti", map.get("jti"));
    }

    token.setScope(this.extractScope(map));
    //44.把解析后的token键值对存到token的additionalInformation属性中
    token.setAdditionalInformation(info);
    //45.返回token到⑩
    return token;
}

⑿.DefaultOAuth2AccessToken.class -> isExpired()

public boolean isExpired() {
    //54.判断accessToken的expiration是否不为空同时小于当前时间,并将结果返回⑦
    return this.expiration != null && this.expiration.before(new Date());
}

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

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

相关文章

瞄准智慧园区 东方恩拓与用友协同开拓新商机

在数字化转型升级浪潮中&#xff0c;传统园区也在寻求新的发展方向&#xff0c;从传统园区向智慧园区甚至未来园区不断演进。随着国家“数字中国”、“中国智造”、“新基建”等战略的部署&#xff0c;智慧园区也迎来了新的发展机遇&#xff0c;园区的数字化、网络化、智能化是…

户外运动耳机选择哪个、最适合户外运动的蓝牙耳机推荐

还有哪个季节比秋天更适合爬山和徒步等户外运动的吗&#xff1f;秋天—没有夏日的骄阳&#xff0c;没有冬天的万物凋零&#xff0c;放眼望去都是墨绿和金黄&#xff0c;上山的话还可以采摘成熟的各种各样的果子…但是一个人的话难免有些落寞&#xff0c;要是有音乐的陪伴则会增…

公网远程连接windows SQL Server数据库【内网穿透】

文章目录1. 本地安装配置SQL Server2. 将本地sqlserver服务暴露至公网2.1 本地安装cpolar内网穿透2.2 创建隧道3. 公网远程连接sqlserver3.1 使用命令行远程连接sqlserver3.2 使用navicat premium图形界面远程连接sqlserver3.3 使用SSMS图形界面远程连接sqlserver疫情当下,居家…

win7电脑怎么录屏?超级简单的2种电脑录屏方法

相信还有不少朋友的电脑系统是win7系统。其实&#xff0c;win7电脑和win10电脑一样都有自带录屏功能。那win7电脑怎么录屏&#xff1f;在win7电脑上开启运行窗口即可&#xff0c;当然也可以通过使用专业的录屏软件来录制屏幕。 下面就由小编给大家介绍一下2个win7电脑录屏的方…

P8 PyTorch WhereGather

前言 这两个函数优点是通过GPU 运算速度快 目录&#xff1a; 1 where 2 Gather 一 where 原理&#xff1a; torch.where(condition,x,y) 输入参数&#xff1a; condition: 判断条件 x,y: Tensor 返回值&#xff1a; 符合条件时: 取x, 不满足取y 优点&#xff1a; 可以使…

关注re:Invent中国巡展,尽享数字时代红利

编辑&#xff5c;阿冒收获固然很甜蜜&#xff0c;但是收获也很辛苦。肯定会有人感觉莫名其妙&#xff0c;既然是收获&#xff0c;必然是甜蜜的&#xff0c;哪来的辛苦啊&#xff1f;且不要着急&#xff0c;容我慢慢分说。一年一度的亚马逊云科技re:Invent全球大会&#xff0c;向…

【操作系统】磁盘调度算法

文章目录影响其访问的时间因素磁盘调度&#xff08;移臂调度&#xff09;常见的磁盘调度算法1、先来先服务算法&#xff08;FCFS&#xff09;2、最短寻道时间优先算法&#xff08;SSTF&#xff09;3、电梯调度算法&#xff08;扫描算法SCAN&#xff09;4、循环扫描算法&#xf…

Microsoft Office 2016 VOL版下载

链接都是VOL版&#xff0c;和零售版功能是一样的&#xff0c;只是激活方便一些&#xff0c;三个下载链接&#xff0c;第一个是Office就是包含了Word、Excel、PPT那些的&#xff0c;另外两个一个是Visio&#xff0c;一个是Project&#xff0c;如果不需要的话&#xff0c;只下载第…

【HTML5】复习(二)

HTML5复习二1.代码一2.代码二3.CSS的引入方式4.选择器5.form表单的一些属性6.内联7. 音频视频8. 滑块、搜索、数字、URL9. 表单补充1.代码一 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head>&…

声明式事务的属性之隔离级别

声明式事务的属性之隔离级别 ①介绍 数据库系统必须具有隔离并发运行各个事务的能力&#xff0c;使它们不会相互影响&#xff0c;避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别&#xff0c;不同隔离级别对应不同的干扰程度&…

智能家居创意DIY之智能灯泡

一、什么是智能灯 传统的灯泡是通过手动打开和关闭开关来工作。有时&#xff0c;它们可以通过声控、触控、红外等方式进行控制&#xff0c;或者带有调光开关&#xff0c;让用户调暗或调亮灯光。 智能灯泡内置有芯片和通信模块&#xff0c;可与手机、家庭智能助手、或其他智能…

12.29日报

今天完成了数据库TestMrl的增删改查四个接口的开发&#xff0c;测试。 测试接口getQRcodeandscene 遇到的问题及解决 不知道在mapper中的增删改方法返回值int的值&#xff0c;和含义&#xff0c;在调用方法时也没有定义int来接参&#xff0c;都是直接调用。于是我定义int i&…

富丽宝石在港交所招股书再次“失效”,于海洋为控股股东

12月30日&#xff0c;贝多财经从港交所披露易了解到&#xff0c;富丽宝石国际控股有限公司&#xff08;下称“富丽宝石”&#xff09;在港交所的上市申请已经“失效”&#xff0c;目前已无法正常查看或下载。在此之前&#xff0c;富丽宝石先后于2021年6月28日、2022年6月29日在…

ERROR: PostCSS received undefined instead of CSS string

ERROR: PostCSS received undefined instead of CSS string 开发项目a的时候用的node版本比较低&#xff0c;拿到b项目的时候提示版本过低&#xff0c;要升级下node&#xff0c;本来想跟新下node的&#xff0c;后面发现nvm&#xff0c;node版本控制器&#xff0c;简单说就是下载…

Python Django入门

一、路由系统 1、Mac命令行安装django 环境 pip install django3.2 2、创建django项目 选择django项目 不同的py文件功能了解 urls.py views.py 可以使用django命令创建项目 1、Mac命令行安装django 环境 pip install django3.2 2、创建django项目 选择django项目 不通py…

中科院ZJ系列压电参数d33系数特性测试装置设计详细介绍

中科院ZJ系列压电参数d33系数特性测试装置设计详细介绍 中科院ZJ系列压电参数d33系数特性测试装置设计详细介绍 一、前沿分析&#xff1a;目前市场上主流的D33系数测试仪主要是中科院的ZJ-3型精密D33系数测量仪&#xff0c;ZJ-4型宽量程压电D33测量仪和ZJ-6型D33/31/D15型综合…

如何通过企业微信、飞书、钉钉消息通知接收双因子认证动态密码?

使用宁盾双因子认证H5令牌的用户每次登录时要切回到企业微信、飞书、钉钉工作台中&#xff0c;找到H5令牌小程序&#xff0c;点进去看动态码。记住或复制动态码后再切回登录界面输入验证。 路径合理&#xff0c;但实际使用场景下不够便捷。用户体验能否再优化&#xff1f; 这个…

Rockchip RK3566 Camera点亮

一.camera名词解释 在现代移动设备中&#xff0c;常用一种接口用来连接SOC和LCD和Camera,这种接口就是MIPI 其中SOC和LCD连接叫 DSI&#xff08;DisplayCommandSet&#xff09;,SOC和Camera连接叫CSI&#xff08;DisplaySerialInterface&#xff09;。 二.camera数据通路 一般…

collect2.exe: error: ld returned 1 exit status分析与解决

这里写自定义目录标题1、问题描述2、分析3、解决办法4、总结5、码字不易&#xff0c;点赞&#xff01;&#xff01;&#xff01;1、问题描述 Windows下进行网络编程&#xff0c;devc&#xff0c;运行.cpp程序时报如下错&#xff1a; [Error] ld returned 1 exit status报错图…

WebCollector

1.WebCollector简介 WebCollector也是一个基于Java的开源网络爬虫框架&#xff0c;其支持多线程、深度采集、URL维护及结构化数据抽取等。WebCollector项目的源码可以在GitHub上进行下载。相比于Crawler4j&#xff0c;WebCollector的可扩展性和适用性更强&#xff0c;如可以实现…