# 用户登录模块
**Repository Path**: OSABC/user-login-module
## Basic Information
- **Project Name**: 用户登录模块
- **Description**: No description available
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-05-17
- **Last Updated**: 2024-05-17
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## 一. 前端部分
### **1.1. 设置 Vue 项目**
使用以下命令来创建:
```bash
vue create my-admin-project
```
选择`Babel`,`Router`,`Vuex`和`Css Pre-processors`组件。
然后,进入你的项目目录:
```bash
cd my-admin-project
```
**源码目录结构:**
- components:包含了项目中所有的公共组件,例如表格、树形菜单、图表等组件。
- api: 包含了项目中所有的后端接口相关的文件,包括接口的封装和请求方式的定义等。
- router:包含了项目中的前端路由配置文件,用于定义前端路由规则。
- store:包含了项目中的 Vuex 状态管理模块,用于管理应用程序的状态和数据流。
- views:包含了项目中所有的业务组件,例如系统管理、任务调度等组件,每个组件对应一个页面。
- utils:包含了项目中的工具类文件,例如日期处理、字符串处理、请求(axios)封装等工具类文件。
- layout:用于存放系统的基本布局模板。该目录下的文件通常包括顶部导航栏、左侧菜单栏、底部版权信息等。
这些目录和文件的划分和设计,让项目的代码结构更加清晰、模块化、易于扩展和维护。
### **1.2. 安装 Element-UI和 axios**
在项目中安装 Element-UI:
```bash
npm install element-ui --save
npm install axios --save
```
或
```bash
yarn add element-ui --save
yarn add axios --save
```
### **1.3. 配置 Element-UI**
在 `main.js` 中导入 Element-UI 和它的样式:
```javascript
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
```
完整代码如下:
```javascript
import Vue from 'vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
Vue.use(ElementUI);
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
```
### **1.4. 创建Layout通用布局**
- `layout` 目录:用于存放系统的基本布局模板。该目录下的文件通常包括顶部导航栏、左侧菜单栏、底部版权信息等。
**layout/index.vue**:
```vue
```
### **1.5. 配置路由**
**router/index.js**:
```javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
/* Layout */
import Layout from '@/layout'
Vue.use(VueRouter)
const routes = [
{
path: '/',
component: Layout, // 修改为 Layout 组件
children: [
{
path: '',
component: () => import( '../views/HomeView.vue'),
name: 'home'
}
]
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
```
**App.vue**:
```vue
```
**views/HomeView.vue**:
```vue
标签一
标签二
标签三
标签四
标签五
```
### **1.6. 实现用户管理页面**
**views/User/List.vue**:
```vue
```
**views/User/Add.vue**:
```vue
添加用户
男
女
提交
重置
```
**router/index.js**:
```javascript
{
path: '/rbac',
component: Layout,
children: [{
path: 'user/list',
component:() => import( '../views/User/List.vue')
},
{
path: 'user/add',
component:() => import( '../views/User/Add.vue')
}
]
},
```
**防止连续点击多次路由报错:**
```javascript
// 防止连续点击多次路由报错
let routerPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return routerPush.call(this, location).catch(err => err)
}
```
### **1.7. 运行项目**
```bash
npm run serve
```
或者
```bash
yarn serve
```
### **1.8. 后端访问MySQL**
在项目的 pom.xml 文件中添加依赖:
```xml
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
org.projectlombok
lombok
1.18.20
provided
mysql
mysql-connector-java
8.0.27
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
```
在MySQL数据库中创建一个名为user的数据表,并添加一些数据。
```sql
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `user` (`name`, `age`) VALUES ('Tom', 20);
INSERT INTO `user` (`name`, `age`) VALUES ('Jerry', 25);
```
创建一个User实体类,该类映射到user表中。
```java
@Data
public class User {
private Long id;
private String name;
private Integer age;
}
```
application.yml文件中进行配置:
```xml
server:
port: your_port
spring:
datasource:
url: jdbc:mysql://47.100.69.71:3306/www_abc_com
username: www_abc_com
password: hfdM22Ba1
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
```
```java
@Mapper
@Repository
public interface UserMapper {
List selectByName(@Param("name") String name);
}
```
Mapper.xml文件UserMapper.xml:
```xml
```
```java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List findUserByName(String name) {
return userMapper.selectByName(name);
}
}
```
```java
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public List getUserByName(@RequestParam String name) {
return userService.findUserByName(name);
}
}
```
以上代码示例中,使用MyBatis进行数据访问需要定义Mapper接口和Mapper.xml文件,并在Service层中使用Mapper接口的方法进行数据操作。Controller层则通过调用Service层的方法来返回数据。在配置文件中需要配置数据源相关信息和MyBatis的相关属性。需要注意的是,在Spring Boot中使用MyBatis时,需要在配置文件中指定Mapper.xml文件的位置。
http://localhost:28080/api/users?name=Tom
### **1.9. 封装axios请求**
**utils/request.js**:
```javascript
import axios from 'axios';
const service = axios.create({
baseURL: '/api',
timeout: 5000
});
service.interceptors.request.use(
config => {
// 在请求发送之前对请求数据进行处理
// ...
return config;
},
error => {
// 对请求错误做些什么
console.log(error);
return Promise.reject(error);
}
);
service.interceptors.response.use(
response => {
// 对响应数据进行处理
// ...
return response.data;
},
error => {
// 对响应错误做些什么
console.log(error);
return Promise.reject(error);
}
);
export default service;
```
### **1.10. 封装 demo api请求**
**api/demo.js**:
```javascript
import request from '@/utils/request'
// 获取用户数据
export function getMysqlDemo(name) {
return request({
url: `/users`,
method: 'get',
params: { name }
})
}
```
### **1.11. 访问后端Demo接口数据**
**views/HomeView.vue**:
```vue
```
### **1.12. 设置dev proxy**
跨域问题,是因为浏览器的同源策略限制了不同源的站点之间的请求。
**Axios 的基础 URL**:
```javascript
axios.defaults.baseURL = '/api'
```
这里,`axios.defaults.baseURL = '/api'` 是将 Axios 的基础 URL 设置为 '/api'。这意味着所有的 Axios 请求都会自动在 URL 前面加上 '/api' 前缀。
**配置 devServer**:
在 `vue.config.js` 文件中配置 `devServer` 属性:
```javascript
//其他配置
devServer: {
proxy: {
"/api": {
target: "[实际请求的目标地址]",
changeOrigin: true,
pathRewrite: {
"^/api": "/api"
}
}
}
}
```
- `devServer.proxy` 是一个代理配置,所有的 API 请求都会通过这个代理。
- `"/api"` 指的是当请求 URL 前缀为 '/api' 的时候,代理就会生效。
- `target` 是实际 API 服务器的地址。
- `changeOrigin` 是一个选项,将其设置为 `true` 来代理目标的主机源。
- `pathRewrite` 是一个选项,可以重写请求的路径。`"^/api": ""` 将 URL 中 '/api' 的部分移除。
**重启服务**:
重启server才能使配置生效。
**实际执行过程**:
1. 首先,你的 Axios 请求的 URL 会自动加上 '/api' 前缀。
2. 然后,当你发送一个请求时,devServer 代理会拦截以 '/api' 开头的请求,并将其代理到你在 `target` 中配置的实际 API 服务器。
3. `pathRewrite` 会去除 URL 中的 '/api' 前缀,因为实际的 API 服务器可能不识别这个前缀。
代理配置可以在本地开发环境中解决跨域问题,但需要注意的是,这种配置只在开发环境中有效。在生产环境中,你可能需要在服务器(如Nginx反向代理)处理跨域问题。
至此,开发环境的前后端均已经调试完成。
### **1.13. 后端封装统一的response返回对象**
CommonResult.java
```java
import lombok.Data;
import java.io.Serializable;
@Data
public class CommonResult implements Serializable {
private Integer code;
private String msg;
private T data;
public static CommonResult success(T data) {
CommonResult result = new CommonResult<>();
result.code = 200;
result.data = data;
result.msg = "操作成功";
return result;
}
public static CommonResult error(Integer code, String message) {
CommonResult result = new CommonResult<>();
result.code = code;
result.msg = message;
return result;
}
}
```
返回统一的json数据
```java
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public CommonResult> getUserByName(@RequestParam String name) {
// return userService.findUserByName(name);
return CommonResult.success(userService.findUserByName(name));
}
}
```
## **二. 实现用户登录**
### **2.1. 设计登录页面**
**views/Login.vue**:
```vue
```
### **2.2. 配置路由**
**router/index.js**:
```javascript
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
},
```
### **2.3. 封装 Login api请求**
**api/login.js**:
```javascript
import request from '@/utils/request'
// 用户登录
export function login(username, password) {
const data = {
username,
password
}
return request({
url: '/user/login',
method: 'post',
data: data
})
}
// 获取用户信息
export function getInfo() {
return request({
url: '/user/getInfo',
method: 'get'
})
}
```
### **2.4. 处理登录操作**
**views/login.vue**:
```vue
import {login} from '@/api/login.js'
// 登录处理逻辑中增加网络请求
login(this.loginForm.username,this.loginForm.password).then(res =>{
this.$router.push({ path: '/' })
}).catch(() => {
// 登录失败,显示错误提示
this.$message.error('用户名或密码错误')
this.loading = false
})
```
**views/HomeView.vue**:
```vue
Login Successful!
Welcome, {{ username }}!
```
### **2.5. 后端实现登录接口**
- 登录需要验证用户名和密码,实现login接口
- 实现getInfo接口,返回一个模拟的数据即可
```java
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public CommonResult> login(@RequestBody Map loginData) {
String username = loginData.get("username");
String password = loginData.get("password");
// 模拟用户验证
if ("admin".equals(username) && "123456".equals(password)) {
return CommonResult.success("登录成功");
} else {
return CommonResult.error(401, "用户名或密码错误");
}
}
@GetMapping("/getInfo")
public CommonResult> getInfo() {
// 模拟返回用户信息
Map userInfo = new HashMap<>();
userInfo.put("username", "admin");
return CommonResult.success(userInfo);
}
}
```
## **三. 后端完善登录添加token机制**
### **3.1. Token工具类**
JWT(JSON Web Tokens),Token工具类包括如何生成和验证JWT。
**TokenResponse 类:**
`TokenResponse`类是一个简单的Java类,用于存储访问令牌和刷新令牌。
```java
@Data
public class TokenResponse {
private String accessToken;
private String refreshToken;
public TokenResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
```
**pom依赖:**
引入JWT操作所需的依赖。
```xml
io.jsonwebtoken
jjwt
0.9.1
```
**JWT配置参数:**
配置参数包括用于签名JWT的密钥,以及访问和刷新令牌的过期时间。
```yaml
jwt:
secret: YOUR_SECRET_KEY
access_token:
expiration: 3600 # in seconds
refresh_token:
expiration: 604800 # in seconds
```
其中`secret`使用如下命令生成:
```bash
openssl rand -base64 256
```
下面是一个示例(可以在Demo中使用)。
```xml
secret: gx4SN/4gh6QPvLfVWCW8Aoo4l2n66d2338IwXyu1koDT1W94XS35OZJPYkA2IIMMgmlz96LCHSNc8jLYuzwB1IreKOZz2TZhsWODfjHAS9bYlduniCUSuSPZ5/OP15O63fn1kN1N5w64frpyWcWbTtiCgLMkJpnfjmqFMAr7fgcOGyt2rmunvFYni9T78Q4fn/0gpx3qm8zXw3oBbFb1Ge9Wnh1UCSapXd/EzLau3iaXqp9f+8FHmSCo9vbEaRSRMPHdcpnB4WKHKySE/BCNSsWM+kHmHyrAbvKErPCz2XXMnIalUoAtquq03LnmYjrBHyI230VcYEVzGmzLOTsSJw==
```
**JwtTokenUtil 类:**
`JwtTokenUtil`类包含了生成和验证JWT的方法。
- `generateAccessToken(String username)` 和 `generateRefreshToken(String username)` 方法用于生成访问和刷新令牌。
- `doGenerateToken(Map claims, String username, Long expiration)` 是一个私有方法,用于生成JWT。
- `getUsernameFromToken(String token)` 方法用于从JWT中提取用户名。
- `getExpirationDateFromToken(String token)` 方法用于获取令牌的过期日期。
- `validateToken(String token)` 方法用于验证JWT是否有效和未过期。
```java
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access_token.expiration}")
private Long accessTokenExpiration;
@Value("${jwt.refresh_token.expiration}")
private Long refreshTokenExpiration;
public String generateAccessToken(String username) {
Map claims = new HashMap<>();
return doGenerateToken(claims, username, accessTokenExpiration);
}
public String generateRefreshToken(String username) {
Map claims = new HashMap<>();
return doGenerateToken(claims, username, refreshTokenExpiration);
}
private String doGenerateToken(Map claims, String username, Long expiration) {
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public T getClaimFromToken(String token, Function claimsResolver) {
Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public Boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return !isTokenExpired(token);
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
```
JWT用于身份验证和授权。访问令牌通常具有较短的有效期,而刷新令牌具有较长的有效期。当访问令牌过期时,可以使用刷新令牌获取新的访问令牌。这样可以提高安全性,因为访问令牌的有效期较短,即使被盗也只能在很短的时间内使用。
### **3.2. 完善登录逻辑**
**LoginRequest 类:**
`LoginRequest` 类是一个数据传输对象(DTO),用于接收客户端发送的登录请求。它包含用户名和密码。
```java
@Data
public class LoginRequest {
private String username;
private String password;
}
```
**依赖注入:**
使用 `@Autowired` 注解,Spring会自动将 `JwtTokenUtil` 类的一个实例注入到当前类中,使你能够在代码中使用它来生成和验证JWT。
```java
@Autowired
private JwtTokenUtil jwtTokenUtil;
```
**登录逻辑:**
登录逻辑在 `/login` 端点中实现。当接收到包含用户名和密码的POST请求时,该方法会:
- 根据用户名检索用户。
- 验证用户名和密码。
- 如果验证成功,生成访问和刷新令牌,并返回它们。
```java
@PostMapping("/login")
public CommonResult> login(@RequestBody LoginRequest loginUser) {
User user = userMapper.findByUsername(loginUser.getUsername());
if (user == null) {
return CommonResult.error(50007,"登录失败,账号密码不正确");
}
if (!loginUser.getPassword().equals(user.getPassword())) {
return CommonResult.error(50007,"登录失败,账号密码不正确");
}
String username = loginUser.getUsername();
// 生成访问令牌和刷新令牌
String accessToken = jwtTokenUtil.generateAccessToken(username);
String refreshToken = jwtTokenUtil.generateRefreshToken(username);
TokenResponse token_resp = new TokenResponse(accessToken,refreshToken);
CommonResult result = CommonResult.success(token_resp);
return result;
}
```
**测试:**
你可以使用API测试工具或使用curl命令来测试登录逻辑。以下是一个使用curl的示例:
```bash
curl -X POST \
http://localhost:28080/admin-api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"username":"user01",
"password":"123456"
}'
```
或者,如果你在使用Postman,你可以:
- 设置请求类型为 POST。
- 设置URL为 `http://localhost:28080/admin-api/auth/login`。
- 在 "Headers" 部分中,设置 Content-Type 为 application/json。
- 在 "Body" 部分中,输入JSON数据:
- 点击 "Send" 按钮发送请求。
```json
{
"username":"user01",
"password":"123456"
}
```
成功登录后,你应该会接收到一个包含访问令牌和刷新令牌的响应。
### **3.3. 根据token获取用户基本信息**
**UserController 类:**
这个类是一个REST控制器,它包含用于处理与用户相关的HTTP请求的方法。
**获取用户个人信息的方法:**
该方法 (`getUserProfile`) 处理GET请求,并从"Authorization"请求头中获取JWT令牌,然后它:
- 从JWT令牌中解析出用户名。
- 使用用户名从数据库中检索用户信息。
- 返回找到的用户信息。
```java
@Autowired
private UserMapper userMapper;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@GetMapping("/getInfo")
public CommonResult> getUserProfile(@RequestHeader("Authorization") String authHeader) {
// 解析Authorization请求头中的JWT令牌 Bearer access_token
String token = authHeader.substring(7);
String username = jwtTokenUtil.getUsernameFromToken(token);
User foundUser = userMapper.findByUsername(username);
CommonResult result = CommonResult.success(foundUser);
return result;
}
```
**测试:**
为了测试这个端点,你需要一个有效的JWT令牌。你可以使用之前登录逻辑获取的令牌。测试可以使用Postman或curl命令进行。
- 设置请求类型为 GET。
- 设置URL为 `http://localhost:28080/admin-api/user/profile/get`。
- 在 "Headers" 部分,添加一个名为 "Authorization" 的头,值为 "Bearer YOUR_ACCESS_TOKEN"。替换 `YOUR_ACCESS_TOKEN` 为你实际的访问令牌。
- 点击 "Send" 按钮发送请求。
如果一切正常,你应该会收到一个包含用户个人信息的响应。如果令牌无效或过期,或者其他任何错误,你应该会收到一个相应的错误消息。
## **四. 前端管理登录状态**
### **4.1. token管理**
首先需要在用户登录成功后保存token,用于之后的身份验证和权限检查。
**utils/auth.js:**
包括获取token、设置token和删除token。
```javascript
const AccessTokenKey = 'ACCESS_TOKEN'
// 获取 Token
export function getAccessToken() {
return localStorage.getItem(AccessTokenKey)
}
// 设置 Token
export function setToken(token) {
localStorage.setItem(AccessTokenKey, token)
}
// 删除 Token
export function removeToken() {
localStorage.removeItem(AccessTokenKey)
}
```
**保存token**:
登录成功后,将用户的token保存到localStorage中。
```javascript
import { setToken } from '@/utils/auth'
// 假设res.data.token是从后端接口返回的token
setToken(res.data.token)
```
### **4.2. 每次请求时携带token**
在发送HTTP请求之前,可以配置请求拦截器来修改请求配置。在每个请求的头部添加一个带有token的`Authorization`字段。
**utils/request.js:**
```javascript
import { getAccessToken } from '@/utils/auth'
// ... (其他代码,如axios的导入和实例的创建)
// 请求拦截器
axios.interceptors.request.use(config => {
if (getAccessToken()) {
config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 携带token
}
return config
}, error => {
// 错误处理
return Promise.reject(error)
})
```
这样,每次发送HTTP请求时,都会在请求头中携带token,后端可以通过这个token验证用户的身份。
### **4.3. token检查和路由守卫**
在permission.js文件中,设置全局路由守卫,用于在每次页面跳转时检查用户的token,以确定用户是否有权访问新页面。
**permission.js:**
```javascript
import router from './router'
import { getAccessToken } from '@/utils/auth'
router.beforeEach((to, from, next) => {
if (getAccessToken()) { // 如果有token
if (to.path === '/login') {
next({ path: '/' }) // 如果是去登录页,重定向到首页
} else {
next() // 正常跳转
}
} else { // 没有token
if (to.path === '/login') {
next() // 如果是去登录页,正常跳转
} else {
next('/login') // 否则重定向到登录页
}
}
})
router.afterEach(() => {
// 这里可以添加一些在路由跳转后需要执行的代码
})
```
**main.js:**
在主入口文件中引入permission.js,确保路由守卫在应用加载时生效。
```javascript
import './permission'
```
### **4.4. 增加退出菜单**
**退出菜单:**
在菜单中添加一个退出选项,用户点击后会触发`logout`方法。
```html
退出
```
**清除token:**
当用户选择退出时,`logout`方法会被触发。该方法首先会弹出一个确认对话框。如果用户确认退出,就会从localStorage中移除token,并将用户重定向到登录页面。
```javascript
```