用户登陆实现前后端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中了

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

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


相关推荐

  1. asp.net core webpi 结合jwt实现登录

    2024-07-14 18:02:02       54 阅读
  2. springboot项目jwt认证(企业级实现方案)

    2024-07-14 18:02:02       30 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-14 18:02:02       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-14 18:02:02       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-14 18:02:02       57 阅读
  4. Python语言-面向对象

    2024-07-14 18:02:02       68 阅读

热门阅读

  1. qiankun子应用vue加载js资源失效问题解决

    2024-07-14 18:02:02       17 阅读
  2. 深入理解C++11中的std::packaged_task

    2024-07-14 18:02:02       21 阅读
  3. 华为 NAT 技术介绍及配置

    2024-07-14 18:02:02       21 阅读
  4. prompt第三讲-PromptTemplate

    2024-07-14 18:02:02       18 阅读
  5. 微信小程序的目录结构

    2024-07-14 18:02:02       26 阅读
  6. Nmap端口扫描工具

    2024-07-14 18:02:02       22 阅读