【动力节点】Springsecurity14-18章JWT+Spring Security+redis+mysql 实现认证

news2024/12/24 3:43:35

15 SpringSecurity 集成thymeleaf

此项目是在springsecurity-12-database-authorization-method 的基础上进行
复制springsecurity-12-database-authorization-method 并重命名为springsecurity-13-thymeleaf

15.1 添加thymeleaf依赖

|

org.springframework.boot

spring-boot-starter-thymeleaf

15.2 修改application.yml

加入thymeleaf的配置

| spring:

thymeleaf:

cache: false # 不使用缓存check-template: true  # 检查thymeleaf模板是否存在

15.3 idea 添加thymeleaf模板

【File】—》【Settings…】

模板名称thymeleaf ,扩展名html,具体内容如下:

|

#[[$Title$]]#       

#[[ E N D END END]]#

简要说明:

#[[ T i t l e Title Title]]# #[[ E N D END END]]# 这两处的作用是,当你新建一个模板页面时,在标签中输入标题内容后,只需要点击回车键,光标就会直接跳到内,省去了你挪动鼠标,或者挪动方向键的步骤,也可以给你节省一点点时间。<strong>新版****idea可能有点问题,实验一下Enable Live Template</strong>

15.4 新建LoginController

| @Controller

@RequestMapping(“/login”)

public class LoginController {

_/**

* 跳转到登陆页面
     */

_@RequestMapping(“/toLogin”)

public String toLogin(){

return “login”;

}

}

15.5 创建thymeleaf文件login.html

在templates下面创建login.html,使用模板创建

|

用户登陆

登录页面

用户名:

密码:

登录

15.7 修改安全配置文件WebSecurityConfig

修改后如下:

| @EnableGlobalMethodSecurity(prePostEnabled = true)

//@Configuration

@Slf4j

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean

public PasswordEncoder passwordEncoder(){

return new BCryptPasswordEncoder();

}

@Override

protected void configure(HttpSecurity http) throws Exception {

//设置登陆方式
        http.formLogin()//使用用户名和密码的登陆方式
                .usernameParameter(“uname”) //页面表单的用户名的name

.passwordParameter(“pwd”)//页面表单的密码的name

.loginPage(“/login/toLogin”) //自己定义登陆页面的地址
                .loginProcessingUrl(“/login/doLogin”)//配置登陆的url

.successForwardUrl(“/index/toIndex”) //登陆成功跳转的页面
                .failureForwardUrl(“/login/toLogin”)//登陆失败跳转的页面
                .permitAll(); //放行和登陆有关的url,别忘了写这个
        //配置退出方式
        http.logout()

.logoutUrl(“/logout”)

.logoutSuccessUrl(“/login/toLogin”)

.permitAll();/放行和退出有关的url,别忘了写这个

//配置路径拦截 的url的匹配规则
        http.authorizeRequests()

//任何路径要求必须认证之后才能访问
                .anyRequest().authenticated();

// 禁用csrf跨站请求攻击  后面可以使用postman工具测试,注意要禁用csrf
        http.csrf().disable();

}

}

15.8 创建IndexController

| @Controller

@RequestMapping(“/index”)

public class IndexController {

_/**

* 登录成功后进入主页
     */

_@RequestMapping(“/toIndex”)

public String toIndex(){

return “main”;

}

}

15.9 创建thymeleaf文件main.html

在templates下面创建main.html

|

系统首页

系统首页

查询学生


添加学生


更新学生


删除学生


导出学生





退出


15.10 修改Studentcontroller

修改后如下:

| @Controller

@Slf4j

@RequestMapping(“/student”)

public class StudentController {

@GetMapping(“/query”)

@PreAuthorize(“hasAuthority(‘student:query’)”)

public String queryInfo(){

return “user/query”;

}

@GetMapping(“/add”)

@PreAuthorize(“hasAuthority(‘student:add’)”)

public String addInfo(){

return “user/add”;

}

@GetMapping(“/update”)

@PreAuthorize(“hasAuthority(‘student:update’)”)

public String updateInfo(){

return “user/update”;

}

@GetMapping(“/delete”)

@PreAuthorize(“hasAuthority(‘student:delete’)”)

public String deleteInfo(){

return “user/delete”;

}

@GetMapping(“/export”)

@PreAuthorize(“hasAuthority(‘student:export’)”)

public String exportInfo(){

return “/user/export”;

}

}

15.11 在templates/user下面创建学生管理的各个页面

创建export.html

|

系统首页-学生管理

系统首页-学生管理-导出

返回


创建query.html

|

系统首页-学生管理

系统首页-学生管理-查询

返回


创建add.html

|

系统首页-学生管理

系统首页-学生管理-新增

返回


创建update.html

|

系统首页-学生管理

系统首页-学生管理-更新

返回


创建delete.html

|

系统首页-学生管理

系统首页-学生管理-删除

返回


15.12 创建403页面

在static/error下面创建403.html

|

403

403:你没有权限访问此页面

去首页

15.13 启动测试

注意:如果出现404问题,一般不出现这个问题

        spring-boot-starter-parent        org.springframework.boot         2.6.13                   

15.14 当用户没有某权限时,页面不展示该按钮(简单看下即可)

上一讲里面我们创建的项目里面是当用户点击页面上的链接请求到后台之后没有权限会跳转到403,那么如果用户没有权限,对应的按钮就不显示出来,这样岂不是更好吗?
我们接着上一个项目来改造
引入下面的依赖

|

org.thymeleaf.extras

thymeleaf-extras-springsecurity5

修改main.html即可

|

xmlns:sec=“http://www.thymeleaf.org/extras/spring-security”>

系统首页

系统首页

查询用户


添加用户


更新用户


删除用户


导出用户





退出


重新启动登录后查看效果

16 springsecurity 集成图片验证码

以前因为我们自己写登陆的方法可以在自己的登陆方法里面去接收页面传过来的code,再和session里面正确的code进行比较 。

16.1 概述

上一讲里面我们集成了thymeleaf实现在页面链接的动态判断是否显示,那么在实际开发中,我们会遇到有验证码的功能,那么如何处理呢?
复制上一个工程springsecurity-13-thymeleaf ,修改名字为springsecurity-14-captcha

16.2 原理、存在问题、解决思路

Springsecurity的过滤器链

我们知道Spring Security是通过过滤器链来完成了,所以它的解决方案是创建一个过滤器放到Security的过滤器链中,在自定义的过滤器中比较验证码

16.3 添加依赖(用于生成验证码)

|

cn.hutool

hutool-all

5.3.9

16.4 添加一个获取验证码的接口

| @Controller

@Slf4j

public class CaptchaController {

@GetMapping(“/code/image”)

public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {

//创建一个验证码
        CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);

//放到session中
        // 为什么要重构?重构的快捷键是啥?
        String captchaCode=circleCaptcha.getCode();

log.info(“生成的验证码为:{}”,captchaCode);

request.getSession().setAttribute(“LOGIN_CAPTCHA_CODE”,captchaCode);

ImageIO.write(circleCaptcha.getImage(),“JPEG”,response.getOutputStream());

}

}

16.5 创建验证码过滤器

| @Component

@Slf4j

public class ValidateCodeFilter extends OncePerRequestFilter {

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

validateCode(request, response,filterChain);
    }

// 验证码校验
    private void validateCode(HttpServletRequest request, HttpServletResponse response,FilterChain filterChain) throws IOException, ServletException {

String enterCaptchaCode = request.getParameter(“code”);

HttpSession session = request.getSession();

String captchaCodeInSession = (String) session.getAttribute(“LOGIN_CAPTCHA_CODE”);

log.info(“用户输入的验证码为:{},session中的验证码为:{}”,enterCaptchaCode,captchaCodeInSession);

//移除错误信息
        session.removeAttribute(“captchaCodeErrorMsg”);

if (!StringUtils.hasText(captchaCodeInSession)) {

session.removeAttribute(“LOGIN_CAPTCHA_CODE”);

}

if (!StringUtils.hasText(enterCaptchaCode) || !StringUtils.hasText(captchaCodeInSession) || !enterCaptchaCode.equalsIgnoreCase(captchaCodeInSession)) {

//说明验证码不正确,返回登陆页面
            session.setAttribute(“captchaCodeErrorMsg”, “验证码不正确”);//重定向
            response.sendRedirect(“/login/toLogin”);

}else{

filterChain.doFilter(request,response);

}

}    @Override    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {        //如果不是登陆请求,直接放行,不走过滤器        return !request.getRequestURI().equals(“/login/doLogin”);    }

}

16.6 修改WebSecurityConfig(重点)

| @EnableGlobalMethodSecurity(prePostEnabled = true)

@Slf4j

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource

private ValidateCodeFilter validateCodeFilter;

@Override
    _/**

* Security的http请求配置
     *

* **@param **http

* **@throws **Exception

*/

_@Override

protected void configure(HttpSecurity http) throws Exception {
        //设置登陆方式
        http.formLogin()//使用用户名和密码的登陆方式
                .usernameParameter(“uname”) //页面表单的用户名的name

.passwordParameter(“pwd”)//页面表单的密码的name

.loginPage(“/login/toLogin”) //自己定义登陆页面的地址
                .loginProcessingUrl(“/login/doLogin”)//配置登陆的url

.successForwardUrl(“/index/toIndex”) //登陆成功跳转的页面
                .failureForwardUrl(“/login/toLogin”)//登陆失败跳转的页面
                .permitAll(); // 这个不要忘了
        //配置退出方式
        http.logout()

.logoutUrl(“/logout”)

.logoutSuccessUrl(“/login/toLogin”)

.permitAll();

//配置路径拦截 的url的匹配规则 ,放行请求获取验证码的路径
     http.authorizeRequests().antMatchers(“/code/image”).permitAll()

//任何路径要求必须认证之后才能访问
                .anyRequest().authenticated();

// 禁用csrf跨站请求,注意不要写错了,因为前端页面没有传token,所以要禁用
        http.csrf().disable();

// 配置登录之前添加一个验证码的过滤器      http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);

}_
    _@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

}

16.7 修改login.html

添加验证码表单元素和图片

|

用户登陆

登录页面

用户名:

密码:

验证码:

username

登录

16.8 测试登录

故意输入错误验证码

16.9 使用debug模式,查看一下自定义过滤器执行流程

17 Base64 和JWT学习

见《base64及jwt学习文档.doc》

18 JWT+Spring Security+redis+mysql 实现认证

18.1 新建工程

复制工程springsecurity-12-database-authorization-method,改名字为
springsecurity-16-jwt-authentication
注意这个工程已经有认证功能和基于方法授权的功能了。
下面咱们看下如何设置使用jwt进行认证登录。

18.2 添加jwt依赖

|

com.auth0

java-jwt

3.11.0

18.3 application.yml 中配置密钥

| jwt:

secretKey: mykey

18.4 jwt功能类

com.powernode.util包下创建

| @Component

@Slf4j

public class JwtUtils {

//算法密钥
    @Value(“${jwt.secretKey}”)

private String jwtSecretKey;

_/**

* 创建jwt

*

* **@param **userInfo 用户信息
     * **@param **authList 用户权限列表
     * **@return **返回jwt(JSON WEB TOKEN)
     */

_public String createToken(String userInfo, List authList) {

//创建时间
        Date currentTime = new Date();

//过期时间,5分钟后过期
        Date expireTime = new Date(currentTime.getTime() + (1000 * 60 * 5));

//jwt 的header信息
        Map<String, Object> headerClaims = new HashMap<>();

headerClaims.put(“type”, “JWT”);

headerClaims.put(“alg”, “HS256”);

//创建jwt

return JWT.create()

.withHeader(headerClaims) // 头部
                .withIssuedAt(currentTime) //已注册声明:签发日期,发行日期
                .withExpiresAt(expireTime) //已注册声明 过期时间
                .withIssuer(“thomas”)  //已注册声明,签发人
                .withClaim(“userInfo”, userInfo) //私有声明,可以自己定义
                .withClaim(“authList”, authList) //私有声明,可以自定义
                .sign(Algorithm.HMAC256(jwtSecretKey)); // 签名,使用HS256算法签名,并使用密钥
//        HS256是一种对称算法,这意味着只有一个密钥,在双方之间共享。 使用相同的密钥生成签名并对其进行验证。 应特别注意钥匙是否保密。
    }

_/**

* 验证jwt的签名,简称验签
     *

* **@param **token 需要验签的jwt

* **@return **验签结果
     */

_public boolean verifyToken(String token) {

//获取验签类对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();

try {

//验签,如果不报错,则说明jwt是合法的,而且也没有过期
            DecodedJWT decodedJWT = jwtVerifier.verify(token);

return true;

} catch (JWTVerificationException e) {

//如果报错说明jwt 为非法的,或者已过期(已过期也属于非法的)
            log.error(“验签失败:{}”, token);

e.printStackTrace();

}

return false;

}

_/**

* 获取用户id

*

* **@param **token jwt

* **@return **用户id

*/

_public String getUserInfo(String token) {

//创建jwt验签对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();

try {

//验签
            DecodedJWT decodedJWT = jwtVerifier.verify(token);

//获取payload中userInfo的值,并返回
            return decodedJWT.getClaim(“userInfo”).asString();

} catch (JWTVerificationException e) {

e.printStackTrace();

}

return null;

}

_/**

* 获取用户权限
     *

* **@param **token

* **@return

***/

_public List getUserAuth(String token) {

//创建jwt验签对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();

try {

//验签
            DecodedJWT decodedJWT = jwtVerifier.verify(token);

//获取payload中的自定义数据authList(权限列表),并返回
            return decodedJWT.getClaim(“authList”).asList(String.class);

} catch (JWTVerificationException e) {

e.printStackTrace();

}

return null;

}

}

18.5 添加响应类

com.powernode.vo包中

| @Data

@AllArgsConstructor

@NoArgsConstructor

@Builder

public class HttpResult implements Serializable {

private Integer code; //响应码
    private String msg; //响应消息
    private Object data; //响应对象

}

18.6 修改SecurityUser类

加入一个获取SysUser的方法,后面会用到这个方法

| public SysUser getSysUser() {

return sysUser;

}

18.7 新建认证成功处理器

| _/**

* 认证成功处理器,当用户登录成功后,会执行此处理器
 */

_@Component

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

//使用此工具类进行序列化
    @Resource

private ObjectMapper objectMapper;

@Resource

private JwtUtils jwtUtils;

@Override

public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

response.setCharacterEncoding(“UTF-8”);

response.setContentType(“text/html;charset=utf-8”);

//从认证对象中获取认证用户信息
        SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();

String userInfo=objectMapper.writeValueAsString(securityUser.getSysUser());

List authorities = (List) securityUser.getAuthorities();

//_这可以改成stream流
        _List authList=new ArrayList<>();

for (SimpleGrantedAuthority authority : authorities) {

authList.add(authority.getAuthority());

}//使用stream流1,使用lambda表达式
List test = authorities.stream().map(

a -> {

return a.getAuthority();

}

).collect(Collectors.toList());

System.out.println("test = " + test);

//使用stream流2

List test111 = authorities.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors.toList());

System.out.println("test111 = " + test111);
        // 创建jwt

String token = jwtUtils.createToken(userInfo,authList);

//返回给前端token

HttpResult httpResult = HttpResult.builder().code(200).msg(“OK”).data(token).build();

PrintWriter writer = response.getWriter();

writer.write(objectMapper.writeValueAsString(httpResult));

writer.flush();

}

}

18.8 新建jwt过滤器,用于检查token等

com.powernode.filter包中新建类

| _/**

* 定义一次性请求过滤器
 */

_@Component

@Slf4j

public class JwtCheckFilter extends OncePerRequestFilter {

@Resource

private ObjectMapper objectMapper;

@Resource

private JwtUtils jwtUtils;

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

//获取请求头中的Authorization

String authorization = request.getHeader(“Authorization”);

//如果Authorization为空,那么不允许用户访问,直接返回
        if (!StringUtils.hasText(authorization)) {

printFront(response, “没有登录!”);

return;

}

//Authorization 去掉头部的Bearer 信息,获取token值
        String jwtToken = authorization.replace("Bearer ", “”);

//验证签名(简称验签)
        boolean verifyTokenResult = jwtUtils.verifyToken(jwtToken);

//验签不成功
        if (!verifyTokenResult) {

printFront(response, “jwtToken 已过期”);

return;

}

//从payload中获取userInfo

String userInfo = jwtUtils.getUserInfo(jwtToken);

//从payload中获取授权列表
        List userAuth = jwtUtils.getUserAuth(jwtToken);

//创建登录用户
        SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);

SecurityUser securityUser = new SecurityUser(sysUser);

//设置权限
        List authList = userAuth.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());

securityUser.setAuthorityList(authList);

//创建用户名密码token
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToke = new UsernamePasswordAuthenticationToken(securityUser

, null, authList);

//通过安全上下文设置认证信息
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToke);

//继续访问相应的url等
        filterChain.doFilter(request, response);

}

private void printFront(HttpServletResponse response, String message) throws IOException {

response.setCharacterEncoding(“UTF-8”);

response.setContentType(“application/json;charset=utf-8”);

PrintWriter writer = response.getWriter();

HttpResult httpResult =HttpResult.builder().code(-1).msg(message).build();

writer.print(objectMapper.writeValueAsString(httpResult));

writer.flush();

}    @Override    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {        //如果是登陆请求,直接放行,不走过滤器        return request.getRequestURI().equals(“/login”);    }

}

18.9修改 web安全配置类WebSecurityConfig

| @EnableGlobalMethodSecurity(prePostEnabled = true)

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Resource

private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Resource

private JwtCheckFilter jwtCheckFilter;
    @Override

protected void configure(HttpSecurity http) throws Exception {

http.addFilterBefore(jwtCheckFilter, UsernamePasswordAuthenticationFilter.class);

http.formLogin().successHandler(myAuthenticationSuccessHandler).permitAll();
http.authorizeRequests()

.mvcMatchers(“/student/**”).hasAnyAuthority(“student:query”,“student:update”)
                .anyRequest().authenticated(); //任何请求均需要认证(登录成功)才能访问//禁用跨域请求保护
        http.csrf().disable();

//禁用session

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

}

@Bean

public PasswordEncoder passwordEncoder(){

return new BCryptPasswordEncoder();

}

}

18.10 启动测试

先登录系统,获取页面上返回的token,然后使用postman 在请求头中携带token发送请求即可。

使用curl 访问

curl -H “Authorization:Bearer token” localhost:8080/student/query

18.11 测试后的问题

18.11.1 实现用户退出的问题

问题:因为JWT无状态,如果要实现退出功能无法实现。
解决办法:
使用redis
步骤:
① 登陆成功之后把生成JWT存到redis中

keyvalue
logintoken:jwt认证信息authentication

② 用户退出时,从redis中删除该token
③ 用户每次访问时,先校验jwt是否合法,如果合法再从redis里面取出logintoken:jwt判断这个jwt还存不存在,如果不存在就说明用户已经退出来,就返回未登陆。

18.11.2 启动redis并使用客户端工具连接到redis

18.11.3 复制工程

复制springsecurity-16-jwt-authentication 成springsecurity-16-jwt-authentication-redis工程

18.11.4 加入redis依赖

|

org.springframework.boot

spring-boot-starter-data-redis

18.11.5 配置redis信息

| spring:

** ****redis:

host: 192.168.43.33

port: 6379

database: 0

password: 666666**

18.11.6 修改认证成功处理器

添加依赖注入

| @Resource

private StringRedisTemplate stringRedisTemplate;

代码中加入

stringRedisTemplate.opsForValue().set(“logintoken:”+token,objectMapper.writeValueAsString(authentication),30, TimeUnit.MINUTES);

18.11.7 新建用户退出成功处理器

| _/**

* 退出成功处理器,用户退出成功后,执行此处理器
 */

_@Component

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

//使用此工具类的对象进行序列化操作
    @Resource

private ObjectMapper objectMapper;

@Resource

private StringRedisTemplate stringRedisTemplate;

@Override

public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

//从请求头中获取Authorization信息
        String authorization = request.getHeader(“Authorization”);

//如果授权信息为空,返回前端
        if(null==authorization){

response.setCharacterEncoding(“UTF-8”);

response.setContentType(“application/json;charset=utf-8”);

HttpResult httpResult=HttpResult.builder().code(-1).msg(“token不能为空”).build();

PrintWriter writer = response.getWriter();

writer.write(objectMapper.writeValueAsString(httpResult));

writer.flush();

return;

}

//如果Authorization信息不为空,去掉头部的Bearer字符串
        String token = authorization.replace("Bearer ", “”);

//redis中删除token,这是关键点
        stringRedisTemplate.delete(“logintoken:”+token);

response.setCharacterEncoding(“UTF-8”);

response.setContentType(“application/json;charset=utf-8”);

HttpResult httpResult=HttpResult.builder().code(200).msg(“退出成功”).build();

PrintWriter writer = response.getWriter();

writer.write(objectMapper.writeValueAsString(httpResult));

writer.flush();

}

}

配置用户成功退出处理器
修改WebSecurityConfig
依赖注入

| @Resource

private MyLogoutSuccessHandler myLogoutSuccessHandler;

添加代码

http.logout().logoutSuccessHandler(myLogoutSuccessHandler);http.csrf().disable(); //注意:禁用跨域请求保护 要不然logout不能访问

18.11.8 修改jwtcheckfilter

添加依赖注入

| @Resource

private StringRedisTemplate stringRedisTemplate;

代码中加入

| //        从redis中获取token

String tokenInRedis = stringRedisTemplate.opsForValue().get(“logintoken:” + jwtToken);

if(!StringUtils.hasText(tokenInRedis)){

printFront(response, “用户已退出,请重新登录”);

return;

}

18.11.9 启动程序并登录测试

登录后查看redis中是否存储了token

18.11.10 使用jwt token访问/student/query

使用postman测试,发现可以正常访问

18.11.11 使用postman 退出系统


注意携带token,才能退出啊
注意:要禁用跨域请求保护,要不然使用postman无法访问logout端点

http.csrf().disable(); //禁用跨域请求保护

18.11.12 再次使用token访问/student/query

现象:发现已经不能正常访问了
原因:虽然token本身并没有过期,但是redis中已经删除了该token,所以不能正常访问了
使用curl 访问

curl -H “Authorization:Bearer token” localhost:8080/student/query

18.11.13 使用debug模式,理解整个流程

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

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

相关文章

根据vue2数组响应式实现原理找到了一个待优化点

最近学习了相关知识 写出了 vue2数据响应式原理(5) 通过重写函数实现数组响应式监听但也在这里 我发现 数组响应式 一个是通过for遍历监听已有属性 还有就是重写会改变数组的几个内置函数来监听 但 如果 元素之前没有 且 我们不适用这些内置函数去修改呢&#xff1f; 我们创建…

报表生成器 FastReport .Net 用户指南(一):基本原理

FastReport .Net是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案&#xff0c;使用FastReport .NET可以创建独立于应用程序的.NET报表。 随着FastReport .Net 2023版的正式发布&#xff0c;厂商也发布了最新版的用户手册&#xff0c;从今天起我们将持续更新2023版的…

苹果手机怎么看生产日期?参考方法在这!

案例&#xff1a;怎么查苹果手机买了几年&#xff1f; 【求助&#xff01;我从别人那里买了一部苹果手机&#xff08;非官方&#xff09;&#xff0c;怎么看这个手机用了几年&#xff1f;】 苹果手机作为一款高端手机&#xff0c;备受用户的喜爱。然而&#xff0c;许多用户不知…

utools - 电脑必装软件

目录 一、超级面板二、计算稿纸三、IP地址查询和正则表达式测试四、浏览器书签五、OCR识别六、Markdown编辑器、Emoji表情输入工具七、总结 简介&#xff1a; 今天我要向大家介绍一款非常实用的电脑软件——utools。 官网&#xff1a;utloos &#x1f433;作为一款集成了多种实…

初识c++语法(一)

我们在C语言的基础之上进行c语言的学习。对于我们的c语言来说&#xff0c;c兼容C语言&#xff0c;所以我们以前编写的C语言的程序在c平台上也是可以运行的。唯一不同的就是我们的c对于我们C语言的部分语法做出了优化以及引入了面向对象的概念。所以在刚开始学习c的时候我们可以…

免费馅饼(牛客刷dp)

免费馅饼 比赛主页 我的提交 时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C 262144K&#xff0c;其他语言524288K 64bit IO Format: %lld 题目描述 SERKOI最新推出了一种叫做“免费馅饼”的游戏:游戏在一个舞台上进行。舞台的宽度为W格&#…

D. Color with Occurrences(Codeforces Round 811 (Div. 3))

https://codeforces.com/contest/1714/problem/D 题目大意 给定一个字符串 t t t 和一组字符串 s 1 , s 2 , … , s n s_1, s_2, \ldots, s_n s1​,s2​,…,sn​。每次操作可以选择其中一个字符串 s i s_i si​&#xff0c;将 t t t 中所有 s i s_i si​ 的出现位置染…

管理后台项目-04-SPU列表-增删改SPU-获取SKU【续】

目录 1-删除spu 2-添加sku 2.1-获取skuForm页面组件的数据 2.2-收集form表单数据 2.3-保存提交数据 3-查看SKU信息和loading效果 上一篇文章管理后台项目-03-SPU列表-增删改SPU-获取SKU_ycmy2017的博客-CSDN博客内容较多&#xff0c;交互逻辑有点复杂&#xff0c;所以分两…

微风·六·JAVA中“==”、hashcode、equals及字符串常量池的区别

vector线程安全的集合 hashset底层为hashmap 文章目录 1 “”和equals的区别是什么?1.1 “”解释1.2 “equals”解释1.3 注意点&#xff1a;equals不能比较基本数据类型1.4 Integter缓存数组1.4.1 引入案例发现问题1.4.2 解释缓存数组 1.5 字符串常量池1.5.1 案例一1.5.2 案例…

视频直播网站开发的最佳实践

随着互联网技术的不断发展&#xff0c;视频直播成为了网络世界中的一股热潮。无论是企业还是个人&#xff0c;都可以通过搭建自己的视频直播网站来实现自己的目标。但是&#xff0c;对于很多企业来说&#xff0c;视频直播网站的开发是一项复杂的任务。因此&#xff0c;本文将介…

响应式开发(HTML5CSS3)实现媒体查询的功能案例

目录 前言 一、媒体查询知识点 二、实现功能的尺寸 三、代码部分 1.不带嵌套的媒体查询功能 1.1.代码段 1.2.运行结果 2.带嵌套的媒体查询功能 2.1.代码段 2.2.运行结果 2.2.3视频效果 前言 1.本文讲解的响应式开发技术&#xff08;HTML5CSS3Bootstrap&#xff09…

C语言开发环境搭建及调试

C简介 可移植 标准C C/C &#xff08;系统硬件操作的接口&#xff0c;windows&#xff0c;Linux不一样&#xff09; 跨平台 Java Python 下载 去官网选择Visual Studio 2019下载 安装过程中勾选使用C的桌面开发 安装好之后点击创建新项目——空项目 位置最好放在根目录下&…

从数据管理到数据资产管理

数据已经与土地、劳动力、资本、技术并称为五种生产要素&#xff0c;数据的价值是毋庸置疑的。数据甚至成为了国家的基础性战略资源&#xff0c;数字经济也正在成为经济增长的强大创新动力。那么—— 数据到底指的是什么&#xff1f; 数据管理又是怎么回事&#xff1f; 数据如何…

Django项目之经济预测平台,应用LSTM、GBDT等算法

一、平台功能与技术点 1.技术点&#xff1a;Python3.9、Django4.1.7&#xff0c; tensorflow2.11.0&#xff0c;keras2.11.0&#xff0c;numpy1.24.2、bootstrap、ajax、MySQL等等 2.功能&#xff1a;正常前后端&#xff0c;前台主要完成经济预测功能&#xff08;特征和标签都…

✨✨✨ ❃ ♕ ꕥXpath解析html获取表情符号,丰富你的文章 ꧁ ꧂꧁ ꧂

✨✨✨ ❃ ♕ ꕥXpath解析html获取表情符号&#xff0c;丰富你的文章 ꧁ ꧂꧁ ꧂ 1. 推荐几个好玩的表情符号网站2. xpath解析html获取表情3. xpath解析html源码3.1 parse_li.py3.2 symbol2.html 参考 1. 推荐几个好玩的表情符号网站 &#x1f495; &#x1f9da; &#x1f6b…

SPDK软件栈基础概念

SPDK SPDK软件架构SSD和SPDK中controller和nsSPDK RPC &#xff08;一&#xff09;driverSPDK virtio-PCIe 和 vhost-user &#xff08;二&#xff09;Storage Services(存储设备)对象存储SPDK BlobStore&BlobFS SPDK 块设备驱动块设备驱动层的其他模块 &#xff08;三&…

KMP字符串匹配算法详解

目录 简单的暴力匹配算法KMP算法next数组next数组的优化 简单的暴力匹配算法 对于字符串的匹配通常是给出一个主串str和一个模式串sub&#xff0c;然后在主串pos位置开始匹配&#xff0c;如果能在str中找到sub那么就返回sub在str中首次出现的首个字符的下标&#xff0c;否则返…

地球系统模式(CESM)

目前通用地球系统模式&#xff08;Community Earth System Model&#xff0c;CESM&#xff09;在研究地球的过去、现在和未来的气候状况中具有越来越普遍的应用。CESM由美国NCAR于2010年07月推出以来&#xff0c;一直受到气候学界的密切关注。近年升级的CESM2.0在大气、陆地、海…

【论文笔记】VideoGPT: Video Generation using VQ-VAE and Transformers

论文标题&#xff1a;VideoGPT: Video Generation using VQ-VAE and Transformers 论文代码&#xff1a;https://wilson1yan. github.io/videogpt/index.html. 论文链接&#xff1a;https://arxiv.org/abs/2104.10157 发表时间&#xff1a; 2021年9月 Abstract 作者提出了…

git 常用命令及遇到问题

自己没事&#xff0c;把git常用命令做个记录总结。方便自己和初学者查看&#xff0c;本文针对初学者&#xff0c;如果你已经是工作多年高手&#xff0c;请跳过。 git的几个区认识&#xff0c;分别为工作区&#xff0c;缓存区&#xff0c;版本库。 工作区&#xff1a;包含.git…