从0到1开始我的全栈之路(第三天)登录注册功能的设计与实现

1. 登录注册功能设计概要

此次开发中某些C端标准功能并未实现,比如邮箱验证登,目的为了带大家循序渐进进行学习

1.1 前端设计


   - 登录页面作为系统入口(/login),未登录用户访问其他页面将被重定向至登录页。
   - 登录成功后使用Token(如JWT)进行身份验证,Token存储于localStorage、sessionStorage或HttpOnly cookie。
   - 所有需认证API请求需在header中携带Token。
   - 用户提交表单前,前端验证输入有效性,并处理后端返回的错误信息。
   - 实现Token刷新机制,以便Token过期时自动获取新Token。

1.2 后端设计


   - 提供用户认证(/api/auth/login)和注册(/api/auth/register)接口。
   - 实现Token生成和验证机制。
   - 使用BCrypt等算法加密存储密码。
   - 用户信息表至少包含用户名、密码hash、邮箱、创建时间。
   - 管理用户会话状态,确保登录后保持登录状态。
   - 实现API限流机制,防止DDoS攻击和暴力破解。
   - 记录关键操作日志,便于问题追踪和安全审计。

1.3 安全考虑


   - 使用HTTPS传输密码。
   - 密码不可明文存储。
   - 登录失败次数限制,防止暴力破解。
   - 实现CSRF防护机制。
   - 防止XSS攻击。
   - 确保JWT安全性,使用HTTPS传输,设置合理过期时间,使用强密钥。
   - 后端对所有输入进行验证,防止SQL注入、命令注入等攻击。
   - 确保敏感信息不在日志中被记录。

1.4 功能流程


   - 登录流程:用户输入账号密码,前端发送请求,后端验证账号密码,生成JWT Token,返回Token给前端,前端存储Token,跳转至主页面。可增加验证码机制和“记住我”功能。
   - 注册流程:用户填写注册信息,前端表单验证,发送注册请求,后端验证信息,密码加密存储,创建用户记录,返回结果,跳转至登录页。可发送验证邮件或短信,验证用户邮箱或手机号真实性,提供用户协议和隐私政策勾选。

2. 实现登录注册功能

2.1 在后端添加必要的依赖(JWT和数据库支持)

2.2 更新后端的User实体类,添加密码字段

package com.example.entity;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    // 在返回给前端时不包含密码
    @Transient
    private String token;

    // 构造函数
    public User() {}
    
    public User(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }

    // Getter和Setter方法
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(LocalDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
} 

2.3 创建JWT工具类

package com.example.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtUtil {
    private final String secret = "your-secret-key";
    private final long expiration = 86400000L; // 24小时

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
} 

2.4 创建认证控制器

package com.example.controller;

import com.example.entity.User;
import com.example.service.UserService;
import com.example.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody User user) {
        try {
            User registeredUser = userService.register(user);
            return ResponseEntity.ok(registeredUser);
        } catch (Exception e) {
            Map<String, String> response = new HashMap<>();
            response.put("error", e.getMessage());
            return ResponseEntity.badRequest().body(response);
        }
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody Map<String, String> loginRequest) {
        try {
            String username = loginRequest.get("username");
            String password = loginRequest.get("password");
            
            User user = userService.login(username, password);
            String token = jwtUtil.generateToken(username);
            
            Map<String, Object> response = new HashMap<>();
            response.put("token", token);
            response.put("user", user);
            
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            Map<String, String> response = new HashMap<>();
            response.put("error", e.getMessage());
            return ResponseEntity.badRequest().body(response);
        }
    }
} 

2.5 创建UserRepository

package com.example.repository;

import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
} 

2.6 实现UserService

package com.example.service;

import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    public User register(User user) {
        // 验证用户名和邮箱是否已存在
        if (userRepository.existsByUsername(user.getUsername())) {
            throw new RuntimeException("用户名已存在");
        }
        if (userRepository.existsByEmail(user.getEmail())) {
            throw new RuntimeException("邮箱已存在");
        }

        // 加密密码
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        
        // 保存用户
        return userRepository.save(user);
    }

    public User login(String username, String password) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("用户不存在"));

        // 验证密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new RuntimeException("密码错误");
        }

        return user;
    }
} 

2.7 接下来配置数据库连接,更新application.properties

server.port=8081
spring.application.name=sprint-client

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/vue_sprint?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# JPA配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

2.8 创建前端的登录组件

<template>
  <div class="gradient-bg">
    <div class="login-box">
      <div class="login-header">{{ isLogin ? '请你登录' : '用户注册' }}</div>
      <div class="form-content">
        <input 
          type="text" 
          v-model="form.username" 
          placeholder="账号" 
          class="input-field"
        >
        <input 
          type="password" 
          v-model="form.password" 
          placeholder="密码" 
          class="input-field"
        >
        <input 
          v-if="!isLogin"
          type="email" 
          v-model="form.email" 
          placeholder="邮箱" 
          class="input-field"
        >
        <div class="login-button" @click="handleSubmit">
          {{ isLogin ? '登录' : '注册' }}
        </div>
      </div>
      <div class="message">
        {{ isLogin ? '如果没有账户?' : '已有账户?' }}
        <a href="#" @click.prevent="isLogin = !isLogin">
          {{ isLogin ? '请先注册' : '去登录' }}
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import request from '@/utils/request'

export default {
  name: 'Login',
  data() {
    return {
      isLogin: true,
      form: {
        username: '',
        password: '',
        email: ''
      }
    }
  },
  methods: {
    async handleSubmit() {
      try {
        const url = this.isLogin ? '/api/auth/login' : '/api/auth/register'
        const response = await request.post(url, this.form)
        
        if (this.isLogin) {
          // 存储token
          localStorage.setItem('token', response.token)
          localStorage.setItem('user', JSON.stringify(response.user))
          // 跳转到用户管理页面
          this.$router.push('/users')
        } else {
          // 注册成功后切换到登录
          this.isLogin = true
          this.form = {
            username: '',
            password: '',
            email: ''
          }
        }
      } catch (error) {
        alert(error.response?.data?.error || '操作失败')
      }
    }
  }
}
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}

html {
  height: 100%;
}

body {
  height: 100%;
}

.gradient-bg {
  height: 100vh;
  background-image: linear-gradient(to right, #fbc2eb, #a6c1ee);
}

.login-box {
  background-color: #fff;
  width: 358px;
  height: 588px;
  border-radius: 15px;
  padding: 0 50px;
  position: relative;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

.login-header {
  font-size: 38px;
  font-weight: bold;
  text-align: center;
  line-height: 200px;
}

.input-field {
  display: block;
  width: 100%;
  margin-bottom: 20px;
  border: 0;
  padding: 10px;
  border-bottom: 1px solid rgb(128, 125, 125);
  font-size: 15px;
  outline: none;
}

.input-field::placeholder {
  text-transform: uppercase;
}

.login-button {
  text-align: center;
  padding: 10px;
  width: 100%;
  margin-top: 40px;
  background-image: linear-gradient(to right, #a6c1ee, #fbc2eb);
  color: #fff;
  border-radius: 8px;
  cursor: pointer;
}

.message {
  text-align: center;
  line-height: 88px;
}

a {
  text-decoration-line: none;
  color: #abc1ee;
  cursor: pointer;
}
</style> 

2.9 更新路由配置和请求拦截器

import { createRouter, createWebHistory } from 'vue-router'
import UserManagement from '@/components/UserManagement.vue'
import LoginPage from '@/views/Login.vue'

const routes = [
  {
    path: '/',
    redirect: '/users'
  },
  {
    path: '/login',
    name: 'LoginPage',
    component: LoginPage
  },
  {
    path: '/users',
    name: 'UserManagement',
    component: UserManagement,
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!token) {
      next('/login')
    } else {
      next()
    }
  } else {
    next()
  }
})

export default router 

2.10 更新请求拦截器,添加token

import axios from 'axios'
import router from '@/router'

const request = axios.create({
    baseURL: 'http://localhost:8081',
    timeout: 5000
})

// 请求拦截器
request.interceptors.request.use(
    config => {
        const token = localStorage.getItem('token')
        if (token) {
            config.headers['Authorization'] = `Bearer ${token}`
        }
        return config
    },
    error => {
        return Promise.reject(error)
    }
)

// 响应拦截器
request.interceptors.response.use(
    response => {
        return response.data
    },
    error => {
        if (error.response?.status === 401) {
            localStorage.removeItem('token')
            localStorage.removeItem('user')
            router.push('/login')
        }
        return Promise.reject(error)
    }
)

export default request 

2.11 创建数据库

使用的数据库为mysql稳定版本5.7

CREATE DATABASE vue_sprint DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

2.12 修改ESLint 的规则

module.exports = {
  root: true,
  env: {
    node: true,
    es6: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended'
  ],
  parserOptions: {
    parser: '@babel/eslint-parser',
    sourceType: 'module'
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'vue/multi-word-component-names': 'off'
  }
} 

2.13 确保前端安装了必要的依赖,支持ES6语法。在 vue_web 目录下运行

npm install --save-dev @babel/core @babel/eslint-parser eslint-plugin-vue

2.14 idea中启动后端服务

2.15 命令行中进入vue_web路径下,启动前端项目

npm run serve

3. 查看我们的登录页面并测试功能

3.1 login页面完整性测试

3.2 注册页面完整性验证

4. 有需要帮忙修改简历,提供就业指导,学习方向和做毕业设计的小伙伴可以私信我

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值