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