# 用户登录模块 **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 ``` ### **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 ```