Bootstrap

简易Token认证系统实现指南(Spring Boot - Vue)

在现代Web应用中,用户认证是一个不可或缺的部分。除了传统的会话/cookie认证方式,Token认证提供了一种无状态、可扩展的认证机制。在本文中,我将向您展示如何在一个Spring Boot应用中实现一个简易的Token认证系统 

什么是Token认证?

Token认证是一种安全机制,通常使用JSON Web Tokens (JWT) 来实现。服务器对用户凭证进行验证后,生成一个包含用户信息和有效期限的Token,随后客户端需要在每个请求中携带这个Token以验证用户身份。

为什么选择Token认证?
  • 无状态:服务器不需要存储会话信息,易于扩展。
  • 跨域:Token可以轻松地在不同域名的服务间传递。
  • 安全性:JWTs可以被签名和验证,确保数据未被篡改。
实现步骤
  1. 添加依赖:在Spring Boot项目的pom.xml中添加Spring Security和JWT的依赖项。

  2. 配置Spring Security:创建一个配置类来配置Spring Security,禁用CSRF保护,并设置无状态会话。

  3. 创建Token工具类:编写一个工具类来生成和解析JWT。

  4. 实现登录逻辑:创建一个登录控制器,接收用户名和密码,验证后返回Token。

  5. 手动验证Token:在需要保护的API中手动验证Token。

示例代码

以下是一些关键代码片段,展示了如何实现上述步骤。

后端
1. 添加依赖
<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot Starter Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- 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.1</version>
    </dependency>
</dependencies>
2. Token工具类
package com.example.javaee.config;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class JwtUtil {
    private static final String SECRET_KEY = "your_secret_key";

    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))                 
                 // 10 hours
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

}
3. 实现登录逻辑
import com.example.javaee.entity.User;
import com.example.javaee.entity.Response;
import com.example.javaee.mapper.UserMapper;

// 其他导入...

public class UserService {
    private UserMapper userMapper; // 假设已注入UserMapper

    public Response login(String account, String password) {
        User existingUser = userMapper.findByUsername(account);
        if (existingUser != null && existingUser.checkPassword(password)) {
            String token = JwtUtil.generateToken(account); // 生成Token
            String permission = existingUser.getPermission();
            return new Response("登录成功", token, permission);
        }
        return new Response("登录失败", null, null);
    }
}
前端
1.router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from '../components/Home.vue';
import Login from '../components/Login.vue';
// 导入其他页面组件,例如 activitypost、superadmin 等

Vue.use(Router);

// 定义路由配置
const routes = [
  {
    path: '/',
    redirect: '/home',
  },
  {
    path: '/home',
    name: 'Home',
    component: Home,
    meta: { requiresAuth: false },
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: { requiresAuth: false },
  },
  // 示例一个受保护的路由
  {
    path: '/activitypost',
    name: 'activitypost',
    component: () => import('../components/activitypost.vue'),
    meta: { requiresAuth: true },
  },
  // ...其他受保护的路由配置
];

const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});


router.beforeEach((to, from, next) => {
  const hasToken = localStorage.getItem(TOKEN_KEY);
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (hasToken) {
      next(); // 有Token,允许进入受保护的路由
    } else {
      next({ path: '/login', query: { redirect: to.fullPath } }); // 无Token,重定向到登录页,并附带redirect查询参数
    }
  } else {
    next(); // 公共路由,直接放行
  }
});

export default router;
2.Login.vue
<template>
  <div id="login">

    <div class="me-login-box me-login-box-radius">
      <h1>登录界面</h1>

      <el-form ref="userForm" :model="userForm" :rules="rules">
        <el-form-item prop="account">
          <div class="my-form1">
            <img src="../assets/img/user.png" id="user-img">
            <input class="my-input" placeholder="用户名" v-model="userForm.account" ref="account" />
          </div>
        </el-form-item>

        <el-form-item prop="password">
          <div class="my-form1">
            <img src="../assets/img/lock.png" id="password-img">
            <input class="my-input" placeholder="密码" type="password" v-model="userForm.password" ref="password"
              @keyup="onHCapitalize($event)" />
            <img src="../assets/img/eyesclosed.png" id="eyes-img" ref="eyes" @click="show()">
          </div>
        </el-form-item>

        <el-form-item size="small" class="me-login-button">
          <el-button type="primary" @click.native.prevent="login('userForm')">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script>
import { login } from '../api/api'
import { TOKEN_KEY } from '../router/index'
export default {
  name: 'Login',
  data() {
    return {
      userForm: {
        account: '',
        password: ''
      },
      rules: {
        account: [
          { required: true, message: '用户名不能为空' }
        ],
        password: [
          { required: true, message: '密码不能为空' },
          { min: 6, message: 'password must be at least 6 characters' }
        ]
      },
      flag: 'false',
      bigChar: 'false'
    }
  },

  methods: {
    login(formName) {
      let that = this;
      this.$refs[formName].validate((valid) => {
        if (valid) {
          if (this.userForm.account.trim() !== "" && this.userForm.password.trim() !== "") {
            let params = new URLSearchParams();
            params.append('account', this.userForm.account);
            params.append('password', this.userForm.password);

            login(params)
              .then(function (response) {
                if (response.data.message === '登录成功') {
                  const token = response.data.token;
                  // 存储 Token 到 localStorage
                  localStorage.setItem('authToken', token);
                  // 根据返回的权限进行路由跳转
                  switch (response.data.permission) {
                    case '超级管理员':
                      that.$router.push({ name: 'superadmin' });
                      break;
                    case '普通管理员':
                      that.$router.push({ name: 'admin' });
                      break;
                    case '注册用户':
                      that.$router.push({ name: 'user' });
                      break;
                    default:
                      console.error('未知权限');
                  }
                } else if (responseData.message === '登录失败') {
                  console.error('登录失败,用户名不存在或者密码错误');
                }
              })
              .catch(function (error) {
                console.error(error);
              });
          } else {
            // 验证账号密码是否为空,并设置输入框边框颜色
            if (this.userForm.account.trim() === "") {
              this.$refs.account.style.borderColor = "red";
            } else {
              this.$refs.account.style.borderColor = "#797979";
            }

            if (this.userForm.password.trim() === "") {
              this.$refs.password.style.borderColor = "red";
            } else {
              this.$refs.password.style.borderColor = "#797979";
            }
            return false;
          }
        }
      });
    },
    show() {
      if (this.flag) {
        this.$refs.eyes.src = "../../static/img/eyes.png";
        this.$refs.password.setAttribute("type", "text");
      } else {
        this.$refs.eyes.src = "../../static/img/eyesclosed.png";
        this.$refs.password.setAttribute("type", "password");
      }
      this.flag = !this.flag;
    },
  }
}
</script>

<style>
/* 您的样式 */
</style>
测试和调试

在实现Token认证后,进行充分的测试是非常重要的。确保在各种情况下Token都能正确生成、验证和失效。

1.当登录成功后,查看控制台是否成功打印Token,并进行路由跳转。

2.删除Token,看看是否还能直接访问页面,如果不能,则说明已经成功。删除Token的方法如图:F12打开控制台后进行如下操作

结论

Token认证是一种强大且灵活的用户认证方式。通过本指南,本人进行了Token最简易最初步的实现,后续继续提高!

;