前言:创建一个根目录管理前端和后端的项目 比如 E:\javaStudy\SOFAB2
gitee仓库地址[项目完整代码都在里面了]:SOFAB: 权限管理系统
搭建后端系统架构
1.创建jd-admin项目
2.pom.xml 引入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.jd</groupId> <artifactId>jd-admin</artifactId> <version>0.0.1-SNAPSHOT</version> <name>jd-admin</name> <description>jd-admin</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.2.RELEASE</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!--mp启动器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.40</version> </dependency> <!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.3.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--redis缓存--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--lettuce poll 缓存连接池--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--hutool工具类--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.3</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>
3.在resources目录下创建application.yml文件
server: port: 81 servlet: context-path: / spring: datasource: #mysql数据连接池配置 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/db_admin?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC username: root password: yr redis: #redis配置 host: 192.168.200.130 #ip port: 6379 #端口 password: yr #密码 lettuce: #lettuce redis客户端配置 pool: #连接池配置 max-active: 8 #连接池最大连接数[使用负值表示没有限制] 默认 8 max-wait: 200s #连接池最大阻塞等待事件[使用负值表示没有限制] 默认 -1 max-idle: 8 #连接池中的最大空闲连接 默认 8 min-idle: 0 #连接池中最小空闲连接 默认 0 mybatis-plus: global-config: db-config: id-type: auto #主键自增 configuration: map-underscore-to-camel-case: true #开启下划线到驼峰命名法的自动映射 auto-mapping-behavior: full #置自动映射行为为 full:会自动映射所有的结果集,无论是否有嵌套结果集映射 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql日志输出 mapper-locations: classpath:mapper/*.xml #XXXMapper.xml文件存放路径
4.注意redis要启动 且ip是192.168.200.130[我是把redis安装在了linux虚拟机上 可以使用ip address查看ip]
5.创建数据库和表
# 新建数据库 db_admin
CREATE DATABASE jd_admin;
USE jd_admin;# 新建用户表
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户Id',
username VARCHAR(100) DEFAULT NULL COMMENT '用户名',
`password` VARCHAR(100) DEFAULT NULL COMMENT '密码',
`avatar` VARCHAR(255) DEFAULT 'default.jpg' COMMENT '用户头像',
`email` VARCHAR(100) DEFAULT '' COMMENT '用户邮箱',
`phonenumber` VARCHAR(11) DEFAULT '' COMMENT '手机号码',
`login_date` DATETIME DEFAULT NULL COMMENT '最后登录时间',
`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1禁用)',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注'
)ENGINE=INNODB CHARSET = utf8;#测试数据
INSERT INTO
`sys_user`(`id`,`username`,`password`,`avatar`,`email`,`phonenumber`,`login_date`,`status`,`create_time`,`update_time`,`remark`)
VALUES
(1,'tom','$2a$10$Kib4zuVhTzg3I1CoqJfd0unuY9G9ysI7cfbhyT3fi7k7Z/4pr3bGW','20220727112556000000325.jpg','caofeng4017@126.com','18862857417','2022-08-29 22:10:52','0','2022-06-09 08:47:52','2022-06-22 08:47:54','备注'),
(2,'common','$2a$10$tiArwm0GxChyEP5k0JGzsOuzyY15IKA.ZTl8S2aj3haYlKAfpwfl.','222.jpg','','','2022-08-22 21:34:39','0',NULL,NULL,NULL),
(3,'test','$2a$10$tiArwm0GxChyEP5k0JGzsOuzyY15IKA.ZTl8S2aj3haYlKAfpwfl.','333.jpg','','','2022-07-24 17:36:07','0',NULL,NULL,NULL),
(4,'1','$2a$10$lD0Fx7oMsFFmX9hVkmYy7eJteH8pBaXXro1X9DEMP5sbM.Z6Co55m','default.jpg','','',NULL,'1',NULL,NULL,NULL),
(5,'2',NULL,'default.jpg','','',NULL,'1',NULL,NULL,NULL),
(15,'fdsfs','$2a$10$AQVcp4hQ7REc5o7ztVnI7eX.sJdcYy3d1x2jm5CfrcCoMZMPacfpi','default.jpg','fdfa4@qq.com','18862851414','2022-08-02 02:22:45','1','2022-08-02 02:21:24','2022-08-01 18:23:16','fdfds4'),
(28,'sdfss2','$2a$10$7aNJxwVmefI0XAk64vrzYuOqeeImYJUQnoBrtKP9pLTGTWO2CXQ/y','default.jpg','dfds3@qq.com','18862857413',NULL,'1','2022-08-07 00:42:46','2022-08-06 16:43:04','ddd33'),
(29,'ccc','$2a$10$7cbWeVwDWO9Hh3qbJrvTHOn0E/DLYXxnIZpxZei0jY4ChfQbJuhi.','20220829080150000000341.jpg','3242@qq.com','18862584120','2022-08-29 19:52:27','0','2022-08-29 17:04:58',NULL,'xxx'),
(30,'ccc666','$2a$10$Tmw5VCM/K2vb837AZDYHQOqE3gPiRZKevxLsh/ozndpTSjdwABqaK','20220829100454000000771.jpg','fdafds@qq.com','18865259845','2022-08-29 22:05:18','0','2022-08-29 22:00:39',NULL,'ccc');
6.使用MybatisX插件快速生成 entity、mapper、service
此时便可以看到该库下的表和字段了
好啦生成完毕!
7.创建封装返回json的数据的通用类
package com.yr.java123admin.entity; import lombok.Data; import java.util.HashMap; import java.util.Map; /** * @author java大帝 * @ClassName R * @description 封装返回参数的类,用于统一处理应用程序的响应结果。 * 该类包含了消息、状态码和扩展数据等属性,以及一系列静态方法用于创建不同状态的响应对象。 * @date 2025年01月13日 * @version 1.0 */ @Data public class R { // 消息 private String msg; // 状态码 private Integer code; // 数据 private Map<String, Object> data = new HashMap<>(); /** * 创建一个表示成功的R对象,默认消息为 "success",状态码为 200。 * * @return 表示成功的R对象。 */ public static R success() { R r = new R(); r.setMsg("success"); r.setCode(200); return r; } /** * 创建一个表示成功的R对象,自定义成功消息,状态码为 200。 * * @param msg 自定义的成功消息。 * @return 表示成功的R对象。 */ public static R success(String msg) { R r = new R(); r.setMsg(msg); r.setCode(200); return r; } /** * 创建一个表示成功的R对象,带有扩展数据,默认消息为 "success",状态码为 200。 * * @param extend 包含扩展数据的Map。 * @return 表示成功的R对象。 */ public static R success(Map<String, Object> extend) { R r = new R(); r.setMsg("success"); r.setCode(200); r.setData(extend); return r; } /** * 创建一个表示失败的R对象,默认消息为 "未知异常 请联系管理员!",状态码为 500。 * * @return 表示失败的R对象。 */ public static R fail() { R r = new R(); r.setMsg("未知异常 请联系管理员!"); r.setCode(500); return r; } /** * 创建一个表示失败的R对象,自定义失败消息,状态码为 500。 * * @param msg 自定义的失败消息。 * @return 表示失败的R对象。 */ public static R fail(String msg) { R r = new R(); r.setMsg(msg); r.setCode(500); return r; } public static R fail(Integer code,String msg) { R r = new R(); r.setMsg(msg); r.setCode(code); return r; } public R put(String key, Object val) { this.data.put(key, val); return this; } }
8.创建controlelr.TestController
package com.yr.java123admin.controller; import com.yr.java123admin.entity.R; import com.yr.java123admin.entity.SysUser; import com.yr.java123admin.service.SysUserService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; /** * @author java大帝 * @ClassName TestController * @description: 测试包 * @date 2025年 01月 13日 * @version: 1.0 */ @RestController @RequestMapping("/test") public class TestController { @Resource private SysUserService sysUserService; @RequestMapping("list") public R userList(){ HashMap<String, Object> resultMap = new HashMap<>(); List<SysUser> list = sysUserService.list(); resultMap.put("userList",list); return R.success(resultMap); } }
9.修改一下启动类并使用浏览器测试
package com.jd.jdadmin; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.jd.jdadmin.mapper") //扫描XXXMapper类 public class JdAdminApplication { public static void main(String[] args) { SpringApplication.run(JdAdminApplication.class, args); } }
搭建前端系统架构
1.使用命令创建vue3
2.使用idea打开SOFAB2
3.启动前端项目
使用浏览器访问
4.安装axios和elementPlus依赖
5.测试elementPlus是否安装成功
在main.ts中引入
import ElementPlus from 'element-plus' import 'element-plus/dist/index.css'app.use(ElementPlus)
在App.vue中使用elementPlus的按钮组件
<template> <div> <el-button type="danger">猜猜我是谁</el-button> </div> </template> <script setup lang="ts"> </script> <style scoped> </style>
浏览器访问
JWT前后端交互
Json Web Token(jwt),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)
JWT就是一段字符串,用来进行用户身份认证的凭证,该字符串分成三段[头部、载荷、凭证]
1.创建common.constant包 并创建JwtConstant类
package com.jd.jdadmin.common.constant; /** * @author java大帝 * @ClassName JwtConstant * @description: 常量类 * @date 2025年 01月 13日 * @version: 1.0 */ public class JwtConstant { /*token*/ public static final int JWT_ERRCODE_NULL = 4000; // token不存在 public static final int JWT_ERRCODE_EXPIRE = 4001; // token过期 public static final int JWT_ERRCODE_FAIL = 4002; // 认证不通过 /*jwt*/ public static final String JWT_SECERT = "1ba@842as45*74s?1p41-15a*s/dj;"; // 密钥 public static final long JWT_TTL = 24* 60 * 60 * 1000; // 24小时 }
2.在entity包下创建CheckResult类
package com.jd.jdadmin.entity; import io.jsonwebtoken.Claims; /** * @author java大帝 * @ClassName CheckResult * @description: 认证结果 * @date 2025年 01月 13日 * @version: 1.0 */ public class CheckResult { private int errCode; private boolean success; private Claims claims; public int getErrCode() { return errCode; } public void setErrCode(int errCode) { this.errCode = errCode; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public Claims getClaims() { return claims; } public void setClaims(Claims claims) { this.claims = claims; } }
3.创建util包 并创建JwtUtils类
package com.jd.jdadmin.util; import cn.hutool.core.codec.Base64; import com.jd.jdadmin.common.constant.JwtConstant; import com.jd.jdadmin.entity.CheckResult; import io.jsonwebtoken.*; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Date; /** * @author java大帝 * @ClassName JwtUtils * @description: Jwt工具类 * @date 2025年 01月 13日 * @version: 1.0 */ public class JwtUtils { // 生成JWT public static String createJWT(String id,String subject,long ttlMillis){ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); SecretKey secretKey = generalKey(); JwtBuilder jwtBuilder = Jwts.builder() .setId(id) .setSubject(subject) // 主体 .setIssuer("yr") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey);// 签名算法以及密钥 if (ttlMillis >= 0){ long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); jwtBuilder.setExpiration(expDate); //设置过期时间 } return jwtBuilder.compact(); } // 生成jwt token public static String genJwtToken(String username){return createJWT(username,username,60 * 60 * 1000);} // 验证JWT public static CheckResult validateJWT(String jwtStr){ CheckResult checkResult = new CheckResult(); Claims claims = null; try { claims = parseJWT(jwtStr); checkResult.setSuccess(true); checkResult.setClaims(claims); } catch (ExpiredJwtException e) { checkResult.setErrCode(JwtConstant.JWT_ERRCODE_EXPIRE); checkResult.setSuccess(false); } catch (SignatureException e){ checkResult.setErrCode(JwtConstant.JWT_ERRCODE_FAIL); checkResult.setSuccess(false); } catch (Exception e) { checkResult.setErrCode(JwtConstant.JWT_ERRCODE_FAIL); checkResult.setSuccess(false); } return checkResult; } // 生成加密key public static SecretKey generalKey(){ byte[] encodeKey = Base64.decode(JwtConstant.JWT_SECERT); SecretKeySpec key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES"); return key; } // 解析JWT字符串 public static Claims parseJWT(String jwt){ SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } public static void main(String[] args) { // 小明失效 10s String sc = createJWT("1", "小明", 60 * 60 * 1000); System.out.println(sc); System.out.println(validateJWT(sc).getErrCode()); System.out.println(validateJWT(sc).getClaims().getId()); System.out.println(validateJWT(sc).getClaims().getSubject()); System.out.println(validateJWT(sc).getClaims()); } }
测试一下
3.发请求测试login登录返回token
3.1后端:创建测试类controller.TestController
@RestController @RequestMapping("/test") public class TestController { @Resource private SysUserService sysUserService; @RequestMapping("list") public R userList(@RequestHeader(required = false) String token){ if (StrUtil.isNotEmpty(token)){ HashMap<String, Object> resultMap = new HashMap<>(); List<SysUser> list = sysUserService.list(); resultMap.put("userList",list); return R.success(resultMap); } return R.fail(401,"无权访问"); } @RequestMapping("login") public R login(){ String token = JwtUtils.genJwtToken("java123456"); return R.success().put("token",token); } }
3.2 后端:创建util.StrUtil字符串的工具类
package com.jd.jdadmin.util; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * @author java大帝 * @ClassName StrUtil * @description: 字符串工具类 * @date 2025年 01月 13日 * @version: 1.0 */ public class StrUtil { /** * 判断是否是空 * @param str * @return */ public static boolean isEmpty(String str){ if(str==null||"".equals(str.trim())){ return true; }else{ return false; } } /** * 判断是否不是空 * @param str * @return */ public static boolean isNotEmpty(String str){ if((str!=null)&&!"".equals(str.trim())){ return true; }else{ return false; } } /** * 格式化模糊查询 * @param str * @return */ public static String formatLike(String str){ if(isNotEmpty(str)){ return "%"+str+"%"; }else{ return null; } } /** * 过滤掉集合里的空格 * @param list * @return */ public static List<String> filterWhite(List<String> list){ List<String> resultList=new ArrayList<String>(); for(String l:list){ if(isNotEmpty(l)){ resultList.add(l); } } return resultList; } /** * 去除html标签 */ public static String stripHtml(String content) { // <p>段落替换为换行 content = content.replaceAll("<p .*?>", "\r\n"); // <br><br/>替换为换行 content = content.replaceAll("<br\\s*/?>", "\r\n"); // 去掉其它的<>之间的东西 content = content.replaceAll("\\<.*?>", ""); // 去掉空格 content = content.replaceAll(" ", ""); return content; } /** * 生成六位随机数 * @return */ public static String genSixRandomNum(){ Random random = new Random(); String result=""; for (int i=0;i<6;i++) { result+=random.nextInt(10); } return result; } /** * 生成由[A-Z,0-9]生成的随机字符串 * @param length 欲生成的字符串长度 * @return */ public static String getRandomString(int length){ Random random = new Random(); StringBuffer sb = new StringBuffer(); for(int i = 0; i < length; ++i){ int number = random.nextInt(2); long result = 0; switch(number){ case 0: result = Math.round(Math.random() * 25 + 65); sb.append(String.valueOf((char)result)); break; case 1: sb.append(String.valueOf(new Random().nextInt(10))); break; } } return sb.toString(); } }
3.3前端:二次封装axios
// 引入axios import axios from 'axios' import {ElMessage} from "element-plus" import useUserStore from "@/stores/modules/user" //基础路径 export let baseUrl = 'http://localhost:81/' const request = axios.create({ baseURL:baseUrl, // 基础路径 timeout:5000 // 超时时间 }) // 请求拦截器 request.interceptors.request.use(config => { let userStore = useUserStore() // 在发送请求之前做些什么 比如携带token请求头 config.headers.token = userStore.token return config },err => { return Promise.reject(err) }) // 响应拦截器 request.interceptors.response.use(resp => { console.log(resp) return resp.data },err => { //错误处理 let message = '' let status = err.response.status switch (status){ case 401: message = 'token过期' break case 403: message = '无权访问' break case 404: message='请求地址有误' break case 500: message = '服务器出错' break default: message = '网络错误' break } ElMessage({ type:'error', message }) }) export default request
3.4前端:创建用户相关小仓库
大仓库
import {createPinia} from "pinia" let pinia = createPinia() export default pinia
在main.ts中引入大仓库并使用
import pinia from "@/stores" app.use(pinia)
小仓库
import { defineStore } from "pinia" import request from "@/util/request" import {GET_TOKEN, SET_TOKEN} from "@/util/token"; const useUserStore = defineStore('User',{ state(){ return { token:GET_TOKEN() } }, actions:{ async userLogin(){ let res = await request.get('test/login') if (res.code == 200){ SET_TOKEN(res.data.token) return 'ok' } else { return Promise.reject(res.msg) } } } }) export default useUserStore
3.5 创建token工具类
export const SET_TOKEN = (token:string)=> { localStorage.setItem("TOKEN",token) } export const GET_TOKEN = ()=> { return localStorage.getItem("TOKEN") } export const REMOVE_TOKEN = ()=> { return localStorage.removeItem("TOKEN") }
3.6App.vue中写相关测试代码
<template> <div> <el-button type="danger" @click="handleLogin">测试登录</el-button> <el-button type="primary" @click="handleUserList">测试获取用户请求</el-button> </div> </template> <script setup lang="ts"> import useUserStore from "@/stores/modules/user" import request from "@/util/request" let userStore = useUserStore() const handleLogin = async () => { await userStore.userLogin() } const handleUserList = async () => { let res = await request.get('test/list') if (res.code == 200) { console.log('userlist',res) } } </script> <style scoped> </style>
3.7浏览器访问出现跨域问题
3.8创建config.WebAppConfigurer解决跨域
package com.jd.jdadmin.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author java大帝 * @ClassName WebAppConfigurer * @description: 跨域解决 * @date 2025年 01月 13日 * @version: 1.0 */ @Configuration public class WebAppConfigurer implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry .addMapping("/**") .allowedOrigins("*") .allowCredentials(true) .allowedMethods("GET","HEAD","POST","DELETE","OPTIONS") .maxAge(3600); } }
测试成功
登录功能实现
前端静态登录页面实现
1.准备两个样式文件
border.css
@charset "utf-8"; .border, .border-top, .border-right, .border-bottom, .border-left, .border-topbottom, .border-rightleft, .border-topleft, .border-rightbottom, .border-topright, .border-bottomleft { position: relative; } .border::before, .border-top::before, .border-right::before, .border-bottom::before, .border-left::before, .border-topbottom::before, .border-topbottom::after, .border-rightleft::before, .border-rightleft::after, .border-topleft::before, .border-topleft::after, .border-rightbottom::before, .border-rightbottom::after, .border-topright::before, .border-topright::after, .border-bottomleft::before, .border-bottomleft::after { content: "\0020"; overflow: hidden; position: absolute; } /* border * 因,边框是由伪元素区域遮盖在父级 * 故,子级若有交互,需要对子级设置 * 定位 及 z轴 */ .border::before { box-sizing: border-box; top: 0; left: 0; height: 100%; width: 100%; border: 1px solid #eaeaea; transform-origin: 0 0; } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { left: 0; width: 100%; height: 1px; } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { top: 0; width: 1px; height: 100%; } .border-top::before, .border-topbottom::before, .border-topleft::before, .border-topright::before { border-top: 1px solid #eaeaea; transform-origin: 0 0; } .border-right::before, .border-rightbottom::before, .border-rightleft::before, .border-topright::after { border-right: 1px solid #eaeaea; transform-origin: 100% 0; } .border-bottom::before, .border-topbottom::after, .border-rightbottom::after, .border-bottomleft::before { border-bottom: 1px solid #eaeaea; transform-origin: 0 100%; } .border-left::before, .border-topleft::after, .border-rightleft::after, .border-bottomleft::after { border-left: 1px solid #eaeaea; transform-origin: 0 0; } .border-top::before, .border-topbottom::before, .border-topleft::before, .border-topright::before { top: 0; } .border-right::before, .border-rightleft::after, .border-rightbottom::before, .border-topright::after { right: 0; } .border-bottom::before, .border-topbottom::after, .border-rightbottom::after, .border-bottomleft::after { bottom: 0; } .border-left::before, .border-rightleft::before, .border-topleft::after, .border-bottomleft::before { left: 0; } @media (max--moz-device-pixel-ratio: 1.49), (-webkit-max-device-pixel-ratio: 1.49), (max-device-pixel-ratio: 1.49), (max-resolution: 143dpi), (max-resolution: 1.49dppx) { /* 默认值,无需重置 */ } @media (min--moz-device-pixel-ratio: 1.5) and (max--moz-device-pixel-ratio: 2.49), (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.49), (min-device-pixel-ratio: 1.5) and (max-device-pixel-ratio: 2.49), (min-resolution: 144dpi) and (max-resolution: 239dpi), (min-resolution: 1.5dppx) and (max-resolution: 2.49dppx) { .border::before { width: 200%; height: 200%; transform: scale(.5); } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { transform: scaleY(.5); } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { transform: scaleX(.5); } } @media (min--moz-device-pixel-ratio: 2.5), (-webkit-min-device-pixel-ratio: 2.5), (min-device-pixel-ratio: 2.5), (min-resolution: 240dpi), (min-resolution: 2.5dppx) { .border::before { width: 300%; height: 300%; transform: scale(.33333); } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { transform: scaleY(.33333); } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { transform: scaleX(.33333); } }
reset.css
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } html{ font-size: 12px; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; }
2.准备好背景图片
网址:The night sky over a mountain range with stars in the sky photo – Free Chamonix Image on Unsplash
3. 在main.ts中引入两个样式文件
// 引入样式文件 import '@/assets/styles/border.css' import '@/assets/styles/reset.css'
4. 修改router/index.ts
import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: () => import('@/views/Home.vue') }, { path: '/login', name: 'login', component: () => import('@/views/Login.vue') } ] }) export default router
5.创建Login.vue
<script setup> const handleLogin = () => { } </script> <template> <div class="login"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <h3 class="title">java大帝 Vue3 后台管理系统</h3> <el-form-item prop="username"> <el-input type="text" size="large" auto-complete="off" placeholder="账号" > </el-input> </el-form-item> <el-form-item prop="password"> <el-input type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin" show-password > </el-input> </el-form-item> <el-checkbox style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-form-item style="width:100%;"> <el-button size="large" type="primary" style="width:100%;" @click.prevent="handleLogin" > <span>登 录</span> </el-button> </el-form-item> </el-form> <!-- 底部 --> <div class="el-login-footer"> <span>Copyright © 2013-2022 <a href="http://www.java1234.vip" target="_blank">java大帝.vip</a> 版权所有.</span> </div> </div> </template> <style lang="scss" scoped> a{ color:white } .login { display: flex; justify-content: center; align-items: center; height: 100%; background-image: url("../assets/images/bg.jpg"); background-size: cover; } .title { margin: 0px auto 30px auto; text-align: center; color: #707070; } .login-form { border-radius: 6px; background: #ffffff; width: 400px; padding: 25px 25px 5px 25px; .el-input { height: 40px; input { display: inline-block; height: 40px; } } .input-icon { height: 39px; width: 14px; margin-left: 0px; } } .login-tip { font-size: 13px; text-align: center; color: #bfbfbf; } .login-code { width: 33%; height: 40px; float: right; img { cursor: pointer; vertical-align: middle; } } .el-login-footer { height: 40px; line-height: 40px; position: fixed; bottom: 0; width: 100%; text-align: center; color: #fff; font-family: Arial; font-size: 12px; letter-spacing: 1px; } .login-code-img { height: 40px; padding-left: 12px; } </style>
6.安装scss
npm install sass sass-loader --save-dev
7.修改App.vue
<template> <router-view /> </template> <script setup lang="ts"> </script> <style> html,body,#app { height: 100%; } .app-container { padding: 20px; } </style>
8.浏览器访问
全局注册Element-Plus的图标组件
1.安装element-plus icon
npm install @element-plus/icons-vue
2.将其注册为全局组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue' export default { install(app:any){ //@ts-ignore for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } } }
3.在main.ts中引入钩子并使用
// 引入钩子 import globalComponent from '@/components/index' app.use(globalComponent)
4.在Login.vue的表单项中使用
<el-form-item prop="username"> <el-input prefix-icon="User" type="text" size="large" auto-complete="off" placeholder="账号" > </el-input> </el-form-item> <el-form-item prop="password"> <el-input prefix-icon="Lock" type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin" show-password > </el-input> </el-form-item>
SpringSecurity执行原理概述
SpringSecurity简单原理
SpringSecurity有很多很多的拦截器 在执行流程里面主要有两个核心的拦截器
1.登录验证拦截器 AuthenticationProcessingFilter
2.资源管理拦截器AbstractSecurityInterceptor
但拦截器里面的实现需要一些组件来实现 所以就有了AuthenticationManager认证管理器、accessDecisionManager决策管理器等组件来支撑
FilterChainProxy是一个代理 真正起作用的是各个Filter,这些Filter作为Bean被Spring管理,是Spring Security的核心,各有各的职责 不直接处理认证和授权,交由认证管理器和决策管理器处理!
大概流程
认证管理
流程图解读:
1.用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类.
2.然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证.
3.认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息、身份信息、细节信息,但密码通常会被移除)Authentication实例.
4.SecurityContextHolder安全上下文容器将第三步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(...)方法 设置到其中,可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager,而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的,web表单对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取,最终AuthenticationProvider将UserDetails填充至Authentication
授权管理
访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过Spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票否定、一票决定、少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。
详细执行流程
项目整合SpringSecurity
1.在pom.xml中加入SpringSecurity依赖
<!--springsecurity--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2.创建config.SecurityConfig类
package com.jd.jdadmin.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; /** * @author java大帝 * @ClassName SecurityConfig * @description: TODO * @date 2025年 01月 14日 * @version: 1.0 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用了基于方法的安全性控制 public class SecurityConfig extends WebSecurityConfigurerAdapter { private static final String URI_WHITELIST[] = { "/login", "/logout", "captcha", "/password", "/image/**", "/test/**" }; @Override protected void configure(HttpSecurity http) throws Exception { // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置 http .cors() .and() .csrf() .disable() .formLogin() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(URI_WHITELIST) // 白名单 .permitAll() .anyRequest() .authenticated(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); // 使用默认的用户名和密码[内置] } }
3.启动测试 此时浏览器访问http://localhost:81/test/list会报错 因为必须先登录
控制台会打印出密码
重写登录成功和登录失败处理器
1.创建common.security包 和 LoginSuccessHandler类
package com.jd.jdadmin.common.security; import cn.hutool.json.JSONUtil; import com.jd.jdadmin.entity.R; import com.jd.jdadmin.util.JwtUtils; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author java大帝 * @ClassName LoginSuccessHandler * @description: 登录成功后的处理器 * @date 2025年 01月 14日 * @version: 1.0 */ @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletRespo