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

news2024/11/15 9:55:36

目录

一、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中了

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

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


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

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

相关文章

【论文速读】《面向深度学习的联合消息传递与自编码器》,无线AI的挑战和解决思路

这篇文章来自华为的渥太华无线先进系统能力中心和无线技术实验室&#xff0c;作者中有大名鼎鼎的童文。 一、自编码架构的全局收发机面临的主要问题 文章对我比较有启发的地方&#xff0c;是提到自编码架构的全局收发机面临的主要问题&#xff1a; 问题一&#xff1a;基于随…

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

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

深度加速器 为游戏而生

使用深度加速器的基本步骤如下 首先&#xff0c;访问深度加速器的官方网站或授权下载渠道&#xff0c;下载最新版本的深度加速器客户端。 下载完成后&#xff0c;电脑版直接双击打开免安装&#xff0c;将深度加速器安装到您的计算机或移动设备上。 注册与登录&#xff1a; 打…

t-SNE降维可视化并生成excel文件使用其他画图软件美化

t-sne t-SNE&#xff08;t-分布随机邻域嵌入&#xff0c;t-distributed Stochastic Neighbor Embedding&#xff09;是由 Laurens van der Maaten 和 Geoffrey Hinton 于 2008 年提出的一种非线性降维技术。它特别适合用于高维数据的可视化。t-SNE 的主要目标是将高维数据映射…

java《ArrayList篇》--ArrayList全套知识点总结及其配套习题逐语句分析(附带全套源代码)

一、前言 来不及悼念字符串了&#xff0c;接下来登场的是集合&#xff0c;集合和数组的用法差不多&#xff0c;不同之处就在于存储的内容&#xff0c;数组是固定的长度的&#xff0c;集合的长度不固定。学习的过程中可以参照数组 今天已经是学习java的第八天了&#xff0c;接下…

怎么安装Manim库在Windows环境下的Jupyter Notebook上

Manim 是解释性数学视频的动画引擎。 您可以使用它来制作数学视频&#xff08;或其他字段&#xff09;。也许你们会在有有些平台上会看过特别好看的数学动画&#xff0c;例如 3Blue1Brown等。这些动画特别好看&#xff0c;还特别丝滑&#xff0c;基本找不到太大的毛病。 我当初…

BasicSR项目(通用图像超分、修复、增强工具库)介绍

项目地址&#xff1a;https://github.com/XPixelGroup/BasicSR 文档地址&#xff1a;https://github.com/XPixelGroup/BasicSR-docs/releases BasicSR 是一个开源项目&#xff0c;旨在提供一个方便易用的图像、视频的超分、复原、增强的工具箱。BasicSR 代码库从2018年4月20日…

【QT】Qt事件

目录 前置知识 事件概念 常见的事件描述 进入和离开事件 代码示例&#xff1a; 鼠标事件 鼠标点击事件 鼠标释放事件 鼠标双击事件 鼠标滚轮动作 键盘事件 定时器事件 开启定时器事件 窗口相关事件 窗口移动触发事件 窗口大小改变时触发的事件 扩展 前置知识…

知识改变命运 第七集(下):Java中数组的定义与使用

4. 数组练习 4.1 数组转字符串 import java.util.Arrays int[] arr {1,2,3,4,5,6}; String newArr Arrays.toString(arr); System.out.println(newArr); // 执行结果 [1, 2, 3, 4, 5, 6]使用这个方法后续打印数组就更方便一些. Java 中提供了 java.util.Arrays 包, 其中包含…

SwiftUI 截图(snapshot)视频画面的极简方法

功能需求 在 万物皆可截图:SwiftUI 中任意视图(包括List和ScrollView)截图的通用实现 这篇博文中,我们实现了在 SwiftUI 中截图几乎任何视图的功能,不幸的是它对视频截图却无能为力。不过别着急,我们还有妙招。 在上面的演示图片中,我们在 SwiftUI 中可以随心所欲的截图…

【ZooKeeper学习笔记】

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

浪潮信息F-OCC算法夺冠,自动驾驶感知技术再创新高

浪潮信息&#xff0c;作为行业领先的AI技术提供商&#xff0c;其AI团队在近期举办的全球权威CVPR 2024自动驾驶国际挑战赛(Autonomous Grand Challenge)中大放异彩&#xff0c;凭借“F-OCC”算法模型以48.9%的卓越成绩&#xff0c;一举夺得占据栅格和运动估计(Occupancy & …

前端Vue组件化实践:打造仿京东天猫商品属性选择器组件

在前端开发领域&#xff0c;随着业务需求的日益复杂和技术的不断进步&#xff0c;传统的整体式应用开发模式已逐渐显得捉襟见肘。面对日益庞大的系统&#xff0c;每次微小的功能修改或增加都可能导致整个逻辑结构的重构&#xff0c;形成牵一发而动全身的困境。为了解决这一问题…

基于Node.js将个人网站部署到ECS

基于Node.js将个人网站部署到云端ECS 一、如何购买ECS以及如何使用学生认证优惠&#xff1f;1.进入阿里云网站&#xff0c;进行学生认证2.购买学生优惠&#xff0c;免费试用一个月3.重置个人密码 二、服务器的配置以及与宝塔面板的链接1.个人电脑打开终端&#xff08;winR->…

探索性数据分析:使用Python与Pandas库实现数据洞察

探索性数据分析&#xff1a;使用Python与Pandas库实现数据洞察 引言 在当今数据驱动的时代&#xff0c;数据分析已成为决策制定、策略规划和业务优化的关键环节。无论是商业智能、金融分析还是市场研究&#xff0c;数据分析都扮演着至关重要的角色。Pandas库作为Python生态系统…

一文SpringCloud

Springcloud 什么是Springcloud&#xff1f; 官网&#xff1a;Spring Cloud Data Flow Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发&#xff0c;如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控…

Flat Ads:金融APP海外广告投放素材的优化指南

在当今全球化的数字营销环境中,金融APP的海外营销推广已成为众多金融机构与开发者最为关注的环节之一。面对不同地域、文化及用户习惯的挑战,如何优化广告素材,以吸引目标受众的注意并促成有效转化,成为了广告主们亟待解决的问题。 作为领先的全球化营销推广平台,Flat Ads凭借…

树莓派PICO使用INA226测量电流和总线电压(3)

上一篇文章我们讲了如何测试电流&#xff0c;但是INA226有一个非常典型的问题&#xff0c;那就是误差比较大&#xff0c;因为采样电阻非常小&#xff0c;我的开发板用的是100mΩ的采样电阻&#xff0c;在设定中我也用的是这个采样电阻值&#xff0c;但事实上&#xff0c;测试得…

文件内容查阅

cat concatenate files and print on the standard output Linux中一个最简单的且最常用的命令是cat命令。其功能是在终端设备上显示文件内容。 cat命令-n选项用于显示行号。 tac concatenate and print files in reverse tac命令的功能是用于反向显示文件内容&#xff0c;即…

【Qt 基础】绘图

画笔 QPen pen; pen.setWidth(3); // 线条宽度 pen.setColor(Qt::red);// 画笔颜色 pen.setStyle(Qt::DashLine);// 线条样式 pen.setCapStyle(Qt::RoundCap);// 线端样式 pen.setJoinStyle(Qt::BevelJoin);// 连接样式 painter.setPen(pen);线条 线端 连接 画刷 QBrush bru…