用户登陆实现前后端JWT鉴权

目录

一、JWT介绍

二、前端配置

三、后端配置

四、实战


一、JWT介绍

1.1 什么是jwt

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间以安全的方式传输信息。JWT 是一种紧凑、自包含的信息载体,可以被解码和验证。它通常用于身份验证和授权服务,特别是在无状态的 Web 应用程序中,比如那些基于 REST 的 API。

1.2 jwt的结构

JWT 由三部分组成,每一部分都由点号(.)分隔开:

  1. 头部 (Header): 包含关于类型和签名算法的信息。例如:

{"alg":"HS256","typ":"JWT"}

这个头部通常表明使用 HMAC SHA-256 算法签名。

  • 负载 (Payload): 也称为“声明”(Claims),包含了要传输的信息。这些信息可以是任意的 JSON 数据,但通常包括一些标准的字段,例如:

{"sub":"1234567890","name":"John Doe","admin":true}

这里 "sub" 是主题(Subject),"name" 是姓名,"admin" 是权限声明。

  • 签名 (Signature): 用于验证数据的完整性和确认发送者的身份。签名是通过一个密钥对头部和负载进行加密得到的。

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

如果使用非对称加密,则密钥可以是公钥或私钥。

1.3 jwt工作流程

  1. 创建和签发: 服务器创建一个 JWT,其中包含用户的身份信息和/或其他数据,然后使用一个秘密密钥或私钥对其进行签名。

  2. 传输: JWT 通过网络发送给客户端,通常作为 HTTP Authorization header 的一部分。

  3. 验证和使用: 当客户端向服务器发送请求时,它将 JWT 作为身份验证的一部分。服务器验证 JWT 的签名,以确保它没有被篡改,并从中读取信息。

  4. 过期: JWT 可以设置一个过期时间,在此之后,它将不再有效。

下面将通过Vue + SpringBoot 实现一个jwt鉴权的项目


二、前端配置

2.1 引入axios

npm install axios

通过添加前端拦截器配置axios

在src下创建一个utils包,再创建一个axios.js文件

import axios from 'axios';

// 创建axios实例
const instance = axios.create();

// 添加请求拦截器
instance.interceptors.request.use(
  function (config) {
    // 在这里添加token到请求头
    const token = localStorage.getItem('token') || ''; // 从本地存储获取token
    if (token) {
      config.headers.Authorization = `${token}`;
    }
    return config;
  },
  function (error) {
    // 请求错误时的处理
    return Promise.reject(error);
  }
);

export default instance;

 在main.js中配置应用axios

import axios from './utils/axios';

Vue.prototype.$axios = axios;

 

2.3 使用axios

在配置全局后,使用axios就并不需要单独引入axios了,直接使用this.$axios即可调用

 this.$axios.get('/api/forum/getAllForumPost', {
                    params: {
                        pageSize: 1,
                        pageNumber: 10
                    }
                }).then((response) => {
                    console.log(response.data.data);
                    
                    this.posts = response.data.data;
                });

创建一个TestView.vue测试发送请求时候是否会携带请求头

<template>
  <div>
 
    <!-- 测试是否会携带请求头 -->
    <button @click="Test"> 发送测试</button>

  </div>
</template>

<script>
export default {
  data() {
    return {
    };
  },
  methods: {
    Test(){
      // 假设有登录成功后的token
      localStorage.setItem('token', '1234567890');

      this.$axios.get('/api/Test').then((response) => {
                    console.log(response.data.data);
                });
    }
  },
  
};
</script>

在控制台的网络中查看是否有对应的请求头

已经成功携带,并且名称为Authorization


三、后端配置

3.1 引入依赖

<!--        JWT依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>

3.3 由于jwt需要三个属性 密钥 有效期 Token的名称

所以需要配置对应的资源类

@Component
@ConfigurationProperties(prefix = "paitool.jwt")
@Data
public class JwtProperties {

    private String SecretKey;
    private long Ttl;
    private String TokenName;


}

application.yml:

paitool:
  jwt:
    secret-key: Alphamilk
    ttl: 10800000
    token-name: Authorization

3.4 创建配置Jwt的工具类 实现快速创建Jwt与解密Jwt方法

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

3.5  通过ThreadLocal实现后端存储用户信息

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

3.6 配置jwt的拦截器

注意:这里的HandlerMehtod是org.springframework.web.method包下的

@Component
@Slf4j
public class JwtTokenInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {


        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(), token);

            // 获取JWT的过期时间并转换为可读格式

            Date expirationDate = claims.getExpiration();

            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String formattedExpiration = sdf.format(expirationDate);
            log.info("JWT过期时间:{}", formattedExpiration);


            Long userId = Long.valueOf(claims.get("userId").toString());
            log.info("当前用户id:", userId);

            //通过ThreadLocal保存员工id
            BaseContext.setCurrentId(userId);
            //3、通过,放行

            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

3.7 将配置好的拦截器加入到webMvc配置中(由于本次实战通过用户登陆获取token,记得排除用户登陆时候进行校验的过程)

@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurationSupport {


    @Autowired
    private JwtTokenInterceptor jwtTokenInterceptor;


    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/GetCaptcha");



}

四、实战

1.创建User表单

create table paitool.user
(
    id                int auto_increment
        primary key,
    account           varchar(255)                                          not null,
    password          varchar(255)                                          not null,
    phone             varchar(20)                                           null,
    address           varchar(255)                                          null,
    isVip             tinyint(1)                  default 0                 null,
    email             varchar(255)                                          null,
    registration_date datetime                    default CURRENT_TIMESTAMP null,
    last_login        datetime                                              null,
    status            enum ('active', 'inactive') default 'active'          null,
    constraint account_UNIQUE
        unique (account),
    constraint email_UNIQUE
        unique (email),
    constraint phone_UNIQUE
        unique (phone)
);

通过MyBatisPlusX自动生成架构

 2.创建返回结果实体类

//结果类
public class Result<T> {
    // 状态码常量
    public static final int SUCCESS = 200;
    public static final int ERROR = 500;
    
    private int code; // 状态码
    private String message; // 消息
    private T data; // 数据

    // 构造函数,用于创建成功的结果对象
    private Result(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // 成功结果的静态方法
    public static <T> Result<T> success(T data) {
        return new Result<>(SUCCESS, "Success", data);
    }


    // 错误结果的静态方法
    public static <T> Result<T> error(String message) {
        return new Result<>(ERROR, message, null);
    }

    // 错误结果的静态方法,可以传入自定义的状态码
    public static <T> Result<T> error(int code, String message) {
        return new Result<>(code, message, null);
    }

    // 获取状态码
    public int getCode() {
        return code;
    }

    // 设置状态码
    public void setCode(int code) {
        this.code = code;
    }

    // 获取消息
    public String getMessage() {
        return message;
    }

    // 设置消息
    public void setMessage(String message) {
        this.message = message;
    }

    // 获取数据
    public T getData() {
        return data;
    }

    // 设置数据
    public void setData(T data) {
        this.data = data;
    }

    // 用于转换为Map类型的方法,方便序列化为JSON
    public Map<String, Object> toMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("code", code);
        map.put("message", message);
        map.put("data", data);
        return map;
    }
}

3.创建验证码(防止密码爆破)工具类 与 Md5加密与解密工具类(防止数据库密码信息泄露)

public class CaptchaUtil {

    private static final int WIDTH = 200;
    private static final int HEIGHT = 75;
    private static final int FONT_SIZE = 36;
    private static final String DEFAULT_FONT = "Arial";

    /**
     * 生成验证码图像.
     *
     * @param captchaText 验证码原始文本
     * @return Base64编码的图像字符串
     */
    public static String generateCaptchaImage(String captchaText) {
        if (captchaText == null || captchaText.isEmpty()) {
            throw new IllegalArgumentException("Captcha text cannot be null or empty.");
        }

        // 创建图像和图形上下文
        BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = (Graphics2D) image.getGraphics();

        // 设置背景颜色
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, WIDTH, HEIGHT);

        // 绘制验证码文本
        g.setFont(new Font(DEFAULT_FONT, Font.BOLD, FONT_SIZE));
        g.setColor(getRandomColor());
        g.drawString(captchaText, 45, 50);

        // 添加随机线条作为干扰
        addNoiseLines(g);

        // 关闭图形上下文
        g.dispose();

        // 将图像转换为Base64编码的字符串
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            ImageIO.write(image, "png", baos);
            return Base64.getEncoder().encodeToString(baos.toByteArray());
        } catch (Exception e) {
            throw new RuntimeException("Error generating captcha image", e);
        }
    }

    private static void addNoiseLines(Graphics2D g) {
        for (int i = 0; i < 5; i++) {
            g.setColor(getRandomColor());
            g.drawLine(
                    getRandomNumber(WIDTH),
                    getRandomNumber(HEIGHT),
                    getRandomNumber(WIDTH),
                    getRandomNumber(HEIGHT)
            );
        }
    }

    private static Color getRandomColor() {
        return new Color((int) (Math.random() * 255),
                         (int) (Math.random() * 255),
                         (int) (Math.random() * 255));
    }

    private static int getRandomNumber(int bound) {
        return (int) (Math.random() * bound);
    }
}
public final class MD5Util {

    /**
     * 使用MD5算法对字符串进行加密。
     *
     * @param input 待加密的字符串
     * @return 加密后的MD5散列值字符串
     */
    public static String encryptToMD5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hashInBytes = md.digest(input.getBytes());

            // 将字节数组转换成十六进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : hashInBytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5 algorithm not found", e);
        }
    }

    public static void main(String[] args) {
        String originalString = "Hello World";
        String encryptedString = encryptToMD5(originalString);
        System.out.println("Original: " + originalString);
        System.out.println("Encrypted: " + encryptedString);
    }
}

 4.创建数据传输与视图的实体类

登陆时候,前端传入数据

@Data
public class LoginDTO {

    private String account;

    private String password;

//    验证码
    private String captcha;


}

 验证通过后传给前端的数据

@Data
public class loginVo {

    private Integer id;

    private String account;

    private Integer isvip;

    private Object status;

    private String token;


}

4.UserController实现登陆功能

@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;


    @Autowired
    private JwtProperties jwtProperties;

    // 登陆时候获取验证码
    @ApiOperation("获取验证码功能")
    @GetMapping("/GetCaptcha")
    public String GetCaptcha(HttpSession session) {

//        随机生成四位验证码原始数据
        String allowedChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        String randomString = generateRandomString(allowedChars, 4);
        System.out.println("captchaCode " + randomString);

        // 将验证码保存到session中
        session.setAttribute("captcha", randomString); // 使用方法参数session
        String ImageByBase64 = CaptchaUtil.generateCaptchaImage(randomString);
        return ImageByBase64;
    }


    // 实现登陆功能
    @ApiOperation("用户登陆功能")
    @PostMapping("/login")
    public Result<loginVo> Login(@RequestBody LoginDTO loginDTO, HttpSession session) { // 使用同一个HttpSession参数

        String captcha = (String) session.getAttribute("captcha");

        log.info("用户调用login方法");
        if (loginDTO.getCaptcha() == null || !loginDTO.getCaptcha().equalsIgnoreCase(captcha)) {
            session.removeAttribute("captcha");
            return Result.error("验证码出错了噢!");
        }

        // 对密码进行md5加密
        String encryptToMD5 = MD5Util.encryptToMD5(loginDTO.getPassword());

        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getAccount, loginDTO.getAccount())
                .eq(User::getPassword, encryptToMD5);

        User user = userService.getOne(lambdaQueryWrapper);
        if (user == null) {
            return Result.error("很抱歉,查不到此用户");
        }
        loginVo loginVo = new loginVo();
        BeanUtils.copyProperties(user,loginVo);
        Map<String,Object> claims = new HashMap<>();
        claims.put("userId",user.getId());
        String token = JwtUtil.createJWT(jwtProperties.getSecretKey(), jwtProperties.getTtl(), claims);
        loginVo.setToken(token);

        return Result.success(loginVo);
    }
}

前端账户操作View.vue:

<template>
  <div id="Header">
    <h3>--PaiTool--</h3>

    <div class="header-avatar">
      <el-popover placement="bottom" :visible-arrow="false" :visible.sync="showUserInfo">
        <div class="userInfo">
          <p>用户名:{{ account }}</p>
          <p>邮箱:{{ email }}</p>
          <p>是否是vip: {{ isVip }}</p>
          <p>账号状态:{{ status }}</p>

          <!-- 登录按钮 -->
          <el-button type="primary" @click="showDialog">登录/注册</el-button>
          <!-- 退出按钮 -->
          <el-button type="text" @click="confirmQuit">退出</el-button>

          <!-- 登录对话框 -->
          <el-dialog title="登录与注册" :visible.sync="dialogLoginVisible" width="30%" @close="resetLoginForm" append-to-body
            :modal-append-to-body="false">

            <el-tabs v-model="activeName" @tab-click="handleClick">
              
              <el-tab-pane label="登陆" name="first">
                <el-form :model="loginForm" ref="loginFormRef" label-width="80px">
                  <el-form-item label="用户名:">
                    <el-input v-model="loginForm.account"></el-input>
                  </el-form-item>
                  <el-form-item label="密码:">
                    <el-input v-model="loginForm.password" show-password></el-input>
                  </el-form-item>
                  <el-form-item label="验证码">
                    <el-input v-model="loginForm.captcha" style="width: 20%;"></el-input>
                    <img :src="captchaImageUrl" alt="验证码" @click="refreshCaptcha" id="captchaImage">
                  </el-form-item>
                </el-form>
              </el-tab-pane>

              <el-tab-pane label="注册" name="second">
                <el-form :model="loginForm" ref="registerFormRef" label-width="80px">
                  <el-form-item label="注册用户:">
                    <el-input v-model="registerFormRef.account"></el-input>
                  </el-form-item>
                  <el-form-item label="注册密码:">
                    <el-input v-model="registerFormRef.password" show-password></el-input>
                  </el-form-item>
                  <el-form-item label="验证码">
                    <el-input v-model="registerFormRef.captcha" style="width: 20%;"></el-input>
                    <img :src="captchaImageUrl" alt="验证码" @click="refreshCaptcha" id="captchaImage">
                  </el-form-item>
                </el-form>


              </el-tab-pane>

            </el-tabs>
            <span slot="footer" class="dialog-footer">
              <el-button @click="dialogLoginVisible = false">取消</el-button>
              <el-button type="primary" @click="submitLogin">登录|注册</el-button>
            </span>


          </el-dialog>

          <!-- 退出确认对话框 -->
          <el-dialog title="确认退出" :visible.sync="dialogConfirmVisible" width="30%" @close="dialogConfirmVisible = false"
            append-to-body :modal-append-to-body="false">
            <span>您确定要退出吗?</span>
            <span slot="footer" class="dialog-footer">
              <el-button @click="dialogConfirmVisible = false">取消</el-button>
              <el-button type="primary" @click="quit">确定退出</el-button>
            </span>

          </el-dialog>
        </div>
        <el-avatar slot="reference" :src="circleUrl" :size="40" class="clickable-avatar"></el-avatar>
      </el-popover>
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import Cookies from 'js-cookie';

export default {
  data() {
    return {
      showUserInfo: false, // 控制个人信息弹窗的显示状态
      circleUrl: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png",
      isVip: '否',
      account: '未登录',
      status: '正常',
      email: 'none',
      activeName: 'first',
      loginOrRegistFlag: true,

      dialogLoginVisible: false,
      dialogConfirmVisible: false,
      loginForm: {
        username: '',
        password: '',
      },
      registerFormRef: {
        username: '',
        password: '',
      },
      captchaImageUrl: '', // 初始化为一个空字符串

    }
  },
  mounted() {
    this.loadUserDataFromCookie();
  },
  methods: {
    loadUserDataFromCookie() {
      // 从cookie中读取account
      const account = Cookies.get('account');
      if (account) {
        this.account = account;
      }


      // 从cookie中读取isVip
      const isVip = Cookies.get('isVip');
      if (isVip !== undefined) {
        // 注意:从cookie读取的数据是字符串类型,需要转换成布尔型
        this.isVip = isVip === 'true';
      }


      // 从cookie中读取status
      const status = Cookies.get('status');
      if (status) {
        this.status = status;
      }


      // 从cookie中读取email
      const email = Cookies.get('email');
      if (email) {
        this.email = email;
      }

    },
    // 打开登录对话框
    open() {
      this.dialogLoginVisible = true;
    },

    resetLoginForm() {
      this.$refs.loginFormRef.resetFields();
    },
    // 提交登录
    submitLogin() {

      // 判断是注册还是登录
      if (this.loginOrRegistFlag == true) {

        // 这里添加验证逻辑(如果需要)
        console.log('登录表单提交:', this.loginForm);

        this.dialogLoginVisible = false;

        // 将this.loginForm作为参数上传
        axios.post("/api/user/login", this.loginForm)
          .then(response => {
            console.log(response.data);
            if (response.data.code === 500) {
              // 重新获取验证码
              this.refreshCaptcha();

              this.$message.error(response.data.message);
            } else if (response.data.code === 200) {
              this.$message({
                showClose: true,
                message: '登陆成功!',
                type: 'success'
              });

              // 设置cookie,可以设置过期时间
              Cookies.set('account', response.data.data.account, { expires: 7 });
              Cookies.set('isVip', response.data.data.isVip, { expires: 7 });
              Cookies.set('status', response.data.data.status, { expires: 7 });
              Cookies.set('email', response.data.data.email, { expires: 7 });
              Cookies.set('userId', response.data.data.id, { expires: 7 })
              
              localStorage.setItem('token', response.data.data.token);


              this.account = response.data.data.account;
              this.isVip = response.data.data.isVip;
              this.status = response.data.data.status;
              this.email = response.data.data.email;
            }

          })
          .catch(error => {
            // 处理错误响应
            console.error('登录失败:', error);
            this.$message.error('登陆错了哦,这是一条错误消息')
          });


      } else {

        axios.post('/api/user/register', this.registerFormRef).then(response => {
          if (response.data.code === 200) {
            this.$message({
              showClose: true,
              message: '注册成功!',
              type: 'success'
            });

            this.dialogLoginVisible = false;
          } else {
            this.$message.error(response.data.message);
          }

        });

      }

    },
    // 打开退出确认对话框
    confirmQuit() {
      this.dialogConfirmVisible = true;
    },
    // 执行退出操作
    quit() {
      // 这里执行实际的退出逻辑
      console.log('执行退出操作');
      this.dialogConfirmVisible = false;

      // 将Cookie所有字段删除
      Cookies.remove('account');
      Cookies.remove('isVip');
      Cookies.remove('status');
      Cookies.remove('email');
      Cookies.remove('userId');
      this.account = '未登录';
      this.isVip = '否';
      this.status = '离线';
      this.email = 'none';

      this.$message({
        showClose: true,
        message: '退出成功!',
        type: 'success'
      });

    },


    // 刷新验证码的示例函数
    refreshCaptcha() {
      // 实现刷新验证码的逻辑
      console.log('刷新验证码');
      this.fetchCaptcha();
    },
    fetchCaptcha() {
      axios.get('/api/user/GetCaptcha')
        .then(response => {

          this.captchaImageUrl = 'data:image/png;base64,' + response.data;
        })
        .catch(error => {
          console.error('获取验证码失败:', error);
        });
    },
    showDialog() {
      this.fetchCaptcha(); // 先获取验证码
      this.dialogLoginVisible = true; // 然后显示登录对话框
    },
    handleClick(tab) {

      if (tab.name === 'first') {
        this.loginOrRegistFlag = true;
      } else {
        this.loginOrRegistFlag = false;
      }

    }

  }
}
</script>

<style scoped>
h3 {
  color: #E9EEF3;
  float: left;
  width: 1307px;
  height: 60px;
  margin-left: 15%;
}

.header-avatar {
  position: relative;
  /* 为绝对定位的子元素提供上下文 */
  float: right;
  z-index: 1000;
  /* 设置一个较高的 z-index 值以确保其位于其他元素之上 */
  margin-top: 10px;
}

.clickable-avatar {
  /* 添加点击手势效果 */
  cursor: pointer;
}

.userInfo {
  text-align: left;
  padding: 10px;
}

#captchaImage {
  cursor: pointer;
  width: 136px;
  height: 45px;
  border: 1px solid black;
  float: right;
  margin-right: 54%;
}
</style>

 数据库创建用户与(123456)加密后的密码

account: admin

password: e10adc3949ba59abbe56e057f20f883e

进入前端并进行登陆

 查看返回结果的token,前端的login函数已经自动存入了token中了

使用其它功能,查看是否有效

这里看到,后端正常识别到并解析出来了。


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

UML/SysML建模工具更新情况(2024年7月)(1)

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 工具最新版本&#xff1a;Enterprise Architect 17.0 BETA 更新时间&#xff1a;2024年7月2日 工具简介 性价比很高&#xff0c;目前最流行的UML建模工具。还包含需求管理、项目估算…

【ZooKeeper学习笔记】

1. ZooKeeper基本概念 Zookeeper官网&#xff1a;https://zookeeper.apache.org/index.html Zookeeper是Apache Hadoop项目中的一个子项目&#xff0c;是一个树形目录服务Zookeeper翻译过来就是动物园管理员&#xff0c;用来管理Hadoop&#xff08;大象&#xff09;、Hive&…

数据恢复篇:适用于 Android 的恢复工具

正在摆弄 Android 设备。突然&#xff0c;您意外删除了一张或多张图片。不用担心&#xff0c;您总能找到一款价格实惠的照片恢复应用。这款先进的软件可帮助 Android 用户从硬盘、安全数字 (SD) 或存储卡以及数码相机中恢复已删除的图片。 Android 上文件被删除的主要原因 在获…

昇思学习打卡-13-LLM原理与实践/解码原理--以MindNLP为例

文章目录 搜索方法集束搜索(beam search)贪心搜索(greedy search) 采样池处理结果 一个文本序列的概率分布可以分解为每个词基于其上文的条件概率的乘积 搜索方法 集束搜索(beam search) Beam search通过在每个时间步保留最可能的 num_beams 个词&#xff0c;并从中最终选择出…

C++·多态

1. 多态的概念 多态通俗讲就是多种形态&#xff0c;就是指去完成某个行为&#xff0c;当不同对象去做时会产生不同的结果或状态。 比如买火车票这个行为&#xff0c;同样是买票的行为&#xff0c;普通成年人买到全价票&#xff0c;学生买到半价票&#xff0c;军人优先买票。这个…

NFT如何解决音乐版权的问题

音乐版权问题一直困扰着音乐产业。传统的音乐版权管理模式存在以下问题。需要注意的是&#xff0c;NFT在音乐版权领域仍处于早期发展阶段&#xff0c;存在一些需要解决的问题&#xff0c;例如技术标准不统一、应用场景有限、法律法规不明朗等。但随着技术的进步和市场的完善&am…

可重入锁深入学习(有码)

【摘要】 ​今天&#xff0c;梳理下java中的常用锁&#xff0c;但在搞清楚这些锁之前&#xff0c;先理解下 “临界区”。临界区在同步的程序设计中&#xff0c;临界区段活称为关键区块&#xff0c;指的是一个访问共享资源&#xff08;例如&#xff1a;共享设备或是共享存储器&a…

路径规划 | 飞蛾扑火算法求解二维栅格路径规划(Matlab)

目录 效果一览基本介绍程序设计参考文献 效果一览 基本介绍 路径规划 | 飞蛾扑火算法求解二维栅格路径规划&#xff08;Matlab&#xff09;。 飞蛾扑火算法&#xff08;Firefly Algorithm&#xff09;是一种基于自然界萤火虫行为的优化算法&#xff0c;在路径规划问题中也可以应…

Nginx入门到精通三(反向代理1)

下面内容整理自bilibili-尚硅谷-Nginx青铜到王者视频教程 Nginx相关文章 Nginx入门到精通一&#xff08;基本概念介绍&#xff09;-CSDN博客 Nginx入门到精通二&#xff08;安装配置&#xff09;-CSDN博客 Nginx入门到精通三&#xff08;Nginx实例1&#xff1a;反向代理&a…

子进程继承父进程文件描述符导致父进程打开设备文件失败

开发过程中有时会遇到需要在程序中执行三方程序或者shell脚本&#xff0c;一般会通过system(), popen(), exec簇来完成该功能。我们知道以上方法会通过fork创建子进程后在子进程中执行相应指令。如图1为某个示例流程&#xff0c;具体的程序执行流程如图2所示&#xff0c;线程my…

使用Python和MediaPipe实现手势控制音量(Win/Mac)

1. 依赖库介绍 OpenCV OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是一个开源的计算机视觉和机器学习软件库。它包含了数百个计算机视觉算法。 MediaPipe MediaPipe是一个跨平台的机器学习解决方案库&#xff0c;可以用于实时人类姿势估计、手势识…

godis源码分析——database存储核心1

前言 redis的核心是数据的快速存储&#xff0c;下面就来分析一下godis的底层存储是如何实现&#xff0c;先分析单机服务。 此文采用抓大放小原则&#xff0c;先大的流程方向&#xff0c;再抓细节。 流程图 源码分析 现在以客户端连接&#xff0c;并发起set key val命令为例…

简单的SQL字符型注入

目录 注入类型 判断字段数 确定回显点 查找数据库名 查找数据库表名 查询字段名 获取想要的数据 以sqli-labs靶场上的简单SQL注入为例 注入类型 判断是数字类型还是字符类型 常见的闭合方式 ?id1、?id1"、?id1)、?id1")等&#xff0c;大多都是单引号…

微分方程的解法(Matlab)

微分方程分为刚性微分方程和非刚性微分方程&#xff0c;在数值解法中的表现和行为特性上存在显著差异。 刚性微分方程&#xff08;Stiffness Equation&#xff09;是指其数值分析的解只有在时间间隔很小时才会稳定&#xff0c;只要时间间隔略大&#xff0c;其解就会不稳定。这…

【BUG】Python3|COPY 指令合并 ts 文件为 mp4 文件时长不对(含三种可执行源代码和解决方法)

文章目录 前言源代码FFmpeg的安装1 下载2 安装 前言 参考&#xff1a; python 合并 ts 视频&#xff08;三种方法&#xff09;使用 FFmpeg 合并多个 ts 视频文件转为 mp4 格式 Windows 平台下&#xff0c;用 Python 合并 ts 文件为 mp4 文件常见的有三种方法&#xff1a; 调用…

项目范围管理-系统架构师(二十九)

1、&#xff08;重点&#xff09;软件设计包括了四个独立又相互联系的活动&#xff0c;高质量的&#xff08;&#xff09;将改善程序结构的模块划分&#xff0c;降低过程复杂度。 A程序设计 B数据设计 C算法设计 D过程设计 解析&#xff1a; 软件设计包含四个&#xff0c;…

博客前端项目学习day01

这里写自定义目录标题 登录创建项目配置环境变量&#xff0c;方便使用登录页面验证码登陆表单 在VScode上写前端&#xff0c;采用vue3。 登录 创建项目 检查node版本 node -v 创建一个新的项目 npm init vitelatest blog-front-admin 中间会弹出询问是否要安装包&#xff0c…

R语言安装devtools包失败过程总结

R语言安装devtools包时&#xff0c;遇到usethis包总是安装失败&#xff0c;现总结如下方法&#xff0c;亲测可有效 一、usethis包及cli包安装问题 首先&#xff0c;Install.packages("usethis")出现如下错误&#xff0c;定位到是这个cli包出现问题 载入需要的程辑包…

Mac和VirtualBox Ubuntu共享文件夹

1、VirtualBox中点击设置->共享文件夹 2、设置共享文件夹路径和名称&#xff08;重点来了&#xff1a;共享文件夹名称&#xff09; 3、保存设置后重启虚拟机&#xff0c;执行下面的命令 sudo mkdir /mnt/share sudo mount -t vboxsf share /mnt/share/ 注&#xff1a;shar…

.快速幂.

按位与&#xff08;Bitwise AND&#xff09;是一种二进制运算&#xff0c;它逐位对两个数的二进制表示进行运算。对于每一位&#xff0c;只有两个相应的位都为1时&#xff0c;结果位才为1&#xff1b;否则&#xff0c;结果位为0。如&#xff1a;十进制9 & 5转化为二进制&am…