后端架构token授权认证机制:spring security JSON Web Token(JWT)简例

news2024/11/17 9:36:10

后端架构token授权认证机制:spring security JSON Web Token(JWT)简例

在基于token的客户端-服务器端认证授权以前,前端到服务器端的认证-授权通常是基于session,自从token机制出现并流行起来后,基于token的客户端-服务器端认证-授权访问机制变得越来越主要,token机制从某种意义上讲是过去传统session会话机制的另外一种解决方案,并尤其适用于当前的大规模微服务、分布式体系架构。

客户端(浏览器web前端、app移动端等)与后端服务基于token的认证-授权访问流程一般情况是这样的:

(1)用户侧发送用户名和密码到服务器端。
(2)服务器端收到用户名后验证用户有效性。
(3)服务器端验证通过后,发送给用户一个token。
(4)客户端存储token,并在每次请求服务器端时附带上这个token。
(5)后续,每一层客户端的请求到达服务器端后,服务器验证token的有效性(合法性),若通过验证,返回客户端所需数据。

JWT可以认为是一种特殊编码格式的token。普通oauth2颁发是一串随机hash字符串,本身无意义,而JWT的token有特定含义,分为三部分:

(1)头部Header
(2)载荷Payload
(3)签名Signature

每一部分用 . 分隔开。

典型的应用场景是api鉴权。比如移动应用的app开发,用api,从远程服务器端拉取数据,每次的http访问,均带上token。

JWT-token此类鉴权认证机制不太适用的场景:

(1)传统session的会话保持业务逻辑。因为token无状态、分布式,在token有效期内,原则上只要使用这个token,仍然可以访问系统。
(2)token续签。围绕token续签设计系统架构会增加额外的复杂度。可以用redis记录token状态,在用户访问后更新状态,但这就是token机制用“歪”了,JWT-token的无状态此时变成有状态了,而这恰恰就是传统session+cookie机制可以覆盖住的业务场景。考虑系统的拓展和高可用,可以考虑使用成熟的spring session框架(B/S应用场景)。

现在写一个例子,代码如下:

 其中最关键的有三个类,JwtAuthenticationController,JwtRequestFilter,JwtWebSecurityConfigurerAdapter

这三个类涉及到spring boot对于token生成和验证的逻辑流程。结合上面的逻辑代码,简单总结一下spring+jwt这种组合框架是如何验证-生成授权token。

如果不使用spring security,那么spring里面实现接口访问即是常规的框架编程。引入spring security后,并在spring security中实现基于JWT的token授权-验证,那么首先需要对于访问用户发放token。所以在JwtAuthenticationController的createAuthenticationToken实现对于用户token的生成和返回。

当用户访问localhost:8080/auth后(注意是POST方法,需要写入用户名和密码),JwtAuthenticationController在createAuthenticationToken里面提取用户名和密码,构造UsernamePasswordAuthenticationToken,并将UsernamePasswordAuthenticationToken记入到上下文中的authenticationManager,如果用户名和密码均正确,spring security就“记忆”当前访问的用户,并通过jwtInMemoryUserDetailsService加载用户信息,然后jwtTokenUtil生成token返回给用户。

此后,客户端用户每一次访问受保护的资源时候,加入token。token是写到header里面的,token的key为Authorization,value按照协议规范,需要以字符串Bearer 开头,注意Bearer后面要带上空格。

JwtRequestFilter类目的是配置spring的http请求,把JwtRequestFilter写完后,加入到JwtWebSecurityConfigurerAdapter类的configure(HttpSecurity http)函数的http里面:

http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

加入后,spring接受的http请求,会被JwtRequestFilter过滤一次,JwtRequestFilter的过滤核心是doFilterInternal函数。doFilterInternal对于每一次的http请求,均会提取http头部header字段里面的key为Authorization对应的值,如果对应的值非空,且以Bearer 开头,则意味是token认证,那么就走token认证逻辑。代码将会根据传入的token逆向的通过工具类jwtTokenUtil反向找出用户名,然后根据用户名判断当前用户是否已被授权,如果没有被授权,jwtTokenUtil验证客户端传入的token和后端系统的token信息是否一致,一致则把具有该token的客户端记录进spring security授权记录中,从此,凡是具有该token的访问,均放行通过。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;


@RestController
public class JwtAuthenticationController {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private JwtUserDetailsService jwtInMemoryUserDetailsService;

    /**
     * 验证用户名和密码。
     * 如果验证通过,创建token并将其返回给客户端
     *
     * @param request
     * @return
     */
    @RequestMapping(value = "/auth", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest request) {
        String username = request.getUsername();
        String password = request.getPassword();
        System.out.println("用户名:" + username);
        System.out.println("密码:" + password);

        UsernamePasswordAuthenticationToken authentication=new UsernamePasswordAuthenticationToken(username, password);
        authenticationManager.authenticate(authentication);

        UserDetails userDetails = jwtInMemoryUserDetailsService.loadUserByUsername(request.getUsername());
        String token = jwtTokenUtil.generateToken(userDetails);
        return ResponseEntity.ok(new JwtResponse(token));
    }

    @RequestMapping(value="/api")
    public Map<String, String> api() {
        Map<String,String> map=new HashMap<String,String>();
        map.put("page", "api");
        return map;
    }

    @RequestMapping(value="/index")
    public Map<String, String> index() {
        Map<String,String> map=new HashMap<String,String>();
        map.put("page", "index");
        return map;
    }

    @RequestMapping(value="/home")
    public Map<String, String> home() {
        Map<String,String> map=new HashMap<String,String>();
        map.put("page", "home");
        return map;
    }
}

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 拒绝每个没有通过身份验证的请求并发送错误代码401。
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        System.out.println("未获得授权");
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;


/**
 * 注意,此处是明文,实际场景时候要加密
 */
@Component
public class JwtPasswordEncoder extends BCryptPasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword为空!");
        }

        if (encodedPassword == null || encodedPassword.length() == 0) {
            throw new IllegalArgumentException("encodedPassword为空");
        }

        return encodedPassword.equals(rawPassword);
    }
}

import java.io.Serializable;

public class JwtRequest implements Serializable {
    private String username;
    private String password;

    //JSON Parsing
    public JwtRequest() {

    }

    public JwtRequest(String username, String password) {
        this.setUsername(username);
        this.setPassword(password);
    }

    public String getUsername() {
        return this.username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return this.password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


/**
 * 对于任何一个传入的请求,都会执行doFilterInternal。
 * 检查请求是否具有有效的token。
 * 如果token有效,在上下文中设置Authentication,
 * 表明当前用户通过身份验证。
 */
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        String username = null;
        String token = null;

        // token一般又称之为"Bearer token",以Bearer开头
        // 截取纯粹的token
        String BEARER = "Bearer ";
        if (header != null && header.startsWith(BEARER)) {
            token = header.substring(BEARER.length());//从Bearer 之后开始截取
            username = jwtTokenUtil.getUsernameFromToken(token);
            System.out.println(username + " token:" + token);
        }

        //拿到token后验证
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username);

            // 如果token有效,配置Spring Security授权
            if (jwtTokenUtil.validateToken(token, userDetails)) {
                System.out.println(username + " token验证通过");
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                //当前用户已经授权,授权认证信息传递给Spring Security配置.
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            System.out.println(username + " 已授权");
        }

        filterChain.doFilter(request, response);
    }
}

public class JwtResponse {
    private String token;

    public JwtResponse(String token) {
        this.token = token;
    }

    public String getToken() {
        return token;
    }
}

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenUtil implements Serializable {
    public static final long TOKEN_VALIDITY = 60 * 60 * 5; //token有效期
    private String secret = "zhangphil_secret";

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getIssuedAtDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getIssuedAt);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private Boolean ignoreTokenExpiration(String token) {
        return false;
    }

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    private String doGenerateToken(Map<String, Object> claims, String subject) {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY * 1000)).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    public Boolean canTokenBeRefreshed(String token) {
        return (!isTokenExpired(token) || ignoreTokenExpiration(token));
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

@Service
public class JwtUserDetailsService implements UserDetailsService {
    //正常情况下,当loadUserByUsername传入用户名后,
    //应该连接数据库从数据库中根据用户名把该用户的信息取出来,
    //本例出于简单演示的目的,不再额外的引入数据库,
    //假设已经知道用户名和密码,硬编码写死了用户名和密码
    public static final String USER_NAME = "zhangphil";
    public static final String USER_PASSWORD = "12345678";

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println(username + " 加载信息");
        if (USER_NAME.equals(username)) {
            return new User(USER_NAME, USER_PASSWORD, new ArrayList<>());
        } else {
            throw new UsernameNotFoundException("用户不存在:" + username);
        }
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    /**
     * 密码管理
     */
    @Autowired
    private JwtPasswordEncoder jwtPasswordEncoder;

    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 本例不需要CSRF
        http.csrf().disable()
                // 排除/auth。
                // 对于请求授权的/auth不需要授权,放行。
                .authorizeRequests()
                .antMatchers("/auth").permitAll()
                //其余的所有请求均需要认证授权
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()//错误处理
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and()
                //本例不需要维护有状态的session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 对于任何一个传入的请求加入一个token过滤器,验证。
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 配置AuthenticationManager使其知道从那里加载用户认证信息
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) {
        try {
            auth.userDetailsService(jwtUserDetailsService)
                    .passwordEncoder(jwtPasswordEncoder);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 假设这一部分接口是公开开放的,不需要token即可访问。
     * 这部分客户端http请求不拦截
     * 排除。
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring()
                .antMatchers(
                        "/index**",
                        "/home/**");
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringJwtApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringJwtApplication.class, args);
    }
}

系统启动后,当访问/index或者/home页面时候,不需要token,均正常打开:

当直接访问localhost:8080/api时候,页面返回HTTP ERROR 401错误,此时,需要post用户名和密码:

然后后台返回token:

把token复制出来,在get请求时候,加入token,访问/api接口,注意,需要为token加上头Bearer ,加到header里面,key是Authorization:

后台系统返回正确结果:

与此同时,系统的后台日志输出为:

未获得授权
用户名:zhangphil
密码:12345678
zhangphil 加载信息
zhangphil 加载信息
zhangphil token:eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ6aGFuZ3BoaWwiLCJleHAiOjE2NDQ1MDc3OTcsImlhdCI6MTY0NDQ4OTc5N30.90WpWZ4MBk1bsEd8r7Vmz7MHQ350q_DEISC7mvHmamgU1YNM_ppfyYweUsb0MxRD-qPFdHF7fNBbf-4H0u6wyw
zhangphil 加载信息
zhangphil token验证通过
zhangphil 已授权

如果在授权认证时候,传入错误的密码,比如密码错误的是1234,那么系统认证失败,后台日志输出:

用户名:zhangphil
密码:1234
zhangphil 加载信息
未获得授权

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

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

相关文章

微信小程序详细登录流程(图解+代码流程)

&#x1f482; 个人网站:【紫陌】【笔记分享网】&#x1f485; 想寻找共同学习交流、共同成长的伙伴&#xff0c;请点击【前端学习交流群】微信小程序的登录和web端的登录有一点是不同的&#xff0c;小程序需要和微信的服务通信验证。1.小程序登录流程官网图官网图地址2.认识op…

【node进阶】深度解析Express框架--路由、中间件

✅ 作者简介&#xff1a;一名普通本科大三的学生&#xff0c;致力于提高前端开发能力 ✨ 个人主页&#xff1a;前端小白在前进的主页 &#x1f525; 系列专栏 &#xff1a; node.js学习专栏 ⭐️ 个人社区 : 个人交流社区 &#x1f340; 学习格言: ☀️ 打不倒你的会使你更强&a…

完全卸载vscode

背景 由于插件崩溃、不正确操作等导致vscode出现异常现象&#xff0c;如&#xff1a;代码高亮失效、无法进行转到定义等&#xff0c;此时的vscode就需要重新完全卸载删除再安装&#xff0c;恢复初始化。 步骤 1、卸载vscode&#xff1a; 任何方法都可以&#xff0c;比如win…

vue 遍历数组

1.forEach(): 遍历数组 需要一个回调函数作为参数 回调函数的形参: ①. value: 遍历数组的内容 ②.index: 对应数组的索引 ③.array: 数组本身 forEach() 方法主要是用于调用数组的每个元素&#xff0c;并将元素传递给回调函数。 注:对于空数组不会执行回调函数 语法: ar…

jsoup 框架的使用指南

概述 参考&#xff1a; 官方文档jsoup的使用JSoup教程jsoup 在 GitHub 的开源代码 概念简介 jsoup 是一款基于 Java 的 HTML 解析器&#xff0c;它提供了一套非常省力的 API&#xff0c;不但能直接解析某个 URL 地址、HTML 文本内容&#xff0c;而且还能通过类似于 DOM、CS…

刷题日常计~JS①

作者 : SYFStrive 博客首页 : 点击跳转HomePage &#x1f4dc;&#xff1a; 初编程JavaScript之每天10题 &#x1f449; 从质变到量变&#x1f4aa; &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f5…

Vue常见报错及解决方案

写代码的过程中一定会遇到报错&#xff0c;遇到报错不要担心&#xff0c;认真分析就可以解决报错&#xff0c;同时积累经验&#xff0c;早日成为大牛&#x1f44d; 本文会整理一些在编码过程中遇到的常见报错&#xff0c;共同学习。 一、报错结构 二、常见问题总结及解决方法 …

微信小程序跳转外部链接

微信小程序跳转外部链接 在开发小程序过程中&#xff0c;我们可能会有这样的需求&#xff0c;在小程序中打开H5或者外部链接 实现方法如下&#xff1a; 1、配置业务域名 小程序管理后台——开发(开发管理)——开发设置&#xff1a;新增业务域名 在这里将你需要的外部链接域…

超详细的正则表达式的使用方法,学不会找我

正则表达式的基本使用 前言&#xff1a; 在我们开发过程中&#xff0c;有很多地方需要用到正则表达式。如验证用户登录信息、手机号、邮箱地址等等。那你都会正则表达式的哪几个方法呢&#xff1f; 首先&#xff0c;我们要知道什么是正则表达式。 正则表达式的定义&#xff…

节流还在用JS吗?CSS也可以实现哦

函数节流是一个我们在项目开发中常用的优化手段&#xff0c;可以有效避免函数过于频繁的执行。一般函数节流用在scroll页面滚动&#xff0c;鼠标移动等。 为什么需要节流呢&#xff0c;因为触发一次事件就会执行一次事件&#xff0c;这样就形成了大量操作dom,会出现卡顿的情况…

ES6模块化(默认导入导出、按需导入导出、直接导入)

一、介绍ES6模块化 ES6 模块化规范是浏览器端与服务器端通用的模块化规范&#xff0c;ES6模块化的出现前端开发者不再需要额外的学习其他的模块化规范。 二、ES6 模块化规范中定义&#xff1a; 1.每个 js 文件都是一个独立的模块 2.导入其它模块成员使用 import 关键字 3.向外…

React组件——类组件

一、组件的概念 使用组件的方式进行编程&#xff0c;可以提高开发效率&#xff0c;提高组件的复用性、提高代码的可维护性和可扩展性 React定义组件的方式有两种 类组件:React16.8版本之前几乎React使用的都是类组件 函数组件:React16.8之后&#xff0c;函数式组件使用的越…

2023高频前端面试题(含答案)

一、简单页面 1、CSS选择器样式优先级 2、CSS实现三列布局&#xff08;左右固定宽度&#xff0c;中间自适应&#xff09; &#xff08;1&#xff09;CSS浮动 第一个float:left&#xff0c;第二个float:right&#xff0c;第三个设置margin-left和margin-right &#xff08;2&am…

什么是Mixin?带你了解Vue中的Mixin混入

什么是Mixin&#xff1f;本篇文章带大家了解一下Vue中的Mixin混入&#xff0c;介绍Mixin和Vuex的区别&#xff0c;Mixin的使用方法&#xff0c;希望对大家有所帮助&#xff01; Vue框架现在基本上已经占据了前端的半壁江山&#xff0c;Vue的数据驱动和组件化的思想深入人心。Vu…

web前端-JavaScript中的数组详解

&#x1f41a;作者简介&#xff1a;苏凉&#xff08;专注于网络爬虫&#xff0c;数据分析&#xff0c;正在学习前端的路上&#xff09; &#x1f433;博客主页&#xff1a;苏凉.py的博客 &#x1f310;系列总专栏&#xff1a;web前端基础教程 &#x1f451;名言警句&#xff1a…

我用尽了洪荒之力,解开了ChatGPT 写前端代码的封印,结果...

我用尽了洪荒之力&#xff0c;解开了ChatGPT 写前端代码的封印介绍ChapGPT 听起来好得令人难以置信&#xff0c;所以让我们让它为我们编写一些 JS 代码。我想看看它是否可以解决我作为前端开发人员每天所做的任务。是驴子是马拉出来溜溜&#xff0c;我们还是直接进入主题一探究…

Vite + Vue3 +Vant4构建项目时,按需引入使用Toast组件,引用 showToast 时出现编译报错的解决方案

Vite Vue3 Vant4构建项目时&#xff0c;按需引入使用Toast组件&#xff0c;引用 showToast 时出现编译报错的解决方案 文章目录Vite Vue3 Vant4构建项目时&#xff0c;按需引入使用Toast组件&#xff0c;引用 showToast 时出现编译报错的解决方案一.问题定位二.以下为完整解决…

React+Mobx|基本使用、模块化

欢迎来到我的博客 📔博主是一名大学在读本科生,主要学习方向是前端。 🍭目前已经更新了【Vue】、【React–从基础到实战】、【TypeScript】等等系列专栏 🛠目前正在学习的是🔥 R e a c t 框架 React框架 Reac

【小程序项目开发 -- 京东商城】uni-app 商品分类页面(上)

&#x1f935;‍♂️ 个人主页: 计算机魔术师 &#x1f468;‍&#x1f4bb; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f310; 推荐一款找工作神器网站: 点击跳转牛客网 |笔试题库|面试经验|实习招聘内推| 还没有账户的小伙伴 速速点击链接…

前端 --- HTML

文章目录1. HTML 结构1.1 HTML 文件基本结构1.2 VsCode 中 使用 HTML 的快捷键2. HTML 常见标签2.1 注释标签2.2 标题标签2.3 段落标签2.4 换行标签2.5 格式化标签2.6 图片标签2.7 超链接标签2.8 表格标签2.9 列表标签2.10 表单标签form 标签input 标签① 文本框② 密码框③ 单…