SpringSecurity+vue通用权限系统
采用主流的技术栈实现,Mysql数据库,SpringBoot2+Mybatis Plus后端,redis缓存,安全框架
SpringSecurity ,Vue3.2+Element Plus实现后台管理。基于JWT技术实现前后端分离。项目开发同时采
用MybatisX插件生成代码,提高开发效率。
基于SpringSecurity实现了 登录验证鉴权功能,用户管理,角色管理,权限管理。
后端框架搭建
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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.java</groupId>
<artifactId> RightManagement</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>RightManagement</name>
<description>RightManagement-admin</description>
<properties>
<java.version>1.8</java.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.10</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</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>3.2.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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- spring boot redis 缓存引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce pool 缓存连接池 -->
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.2.RELEASE</version>
</plugin>
</plugins>
</build>
</project>
新建yml文件
server:
port: 80
servlet:
context-path: /
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_admin3?serverTimezone=Asia/Shanghai
username: root
password: 123456
redis: # redis配置
host: 127.0.0.1 # IP
port: 6379 # 端口
password: # 密码
connect-timeout: 10s # 连接超时时间
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
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
新建数据库db_admin3
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL 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 '备注',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8;
/*Data for the table `sys_user` */
INSERT INTO `sys_user`(`id`,`username`,`password`,`avatar`,`email`,`phonenumber`,`login_date`,`status`,`create_time`,`update_time`,`remark`) VALUES (1,'java1234','$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');
安装插件
连接mysql
生成
启动类增加MapperScan
添加封装类
package com.java.entity;
import java.util.HashMap;
import java.util.Map;
/**
* 页面响应entity
* @author java1234_小锋
* @site www.java1234.com
* @company Java知识分享网
* @create 2019-08-13 上午 10:00
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", 200);
}
public static R error() {
return error(500, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(500, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
新建testController
package com.java.controller;
import com.java.entity.R;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author: javaLiuwb
* @date: 2025-05-04 17:21
* @description:
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private SysUserService sysUserService;
@RequestMapping("/user/list")
public R userList(){
Map<String,Object> resutlMap=new HashMap<>();
List<SysUser> userList = sysUserService.list();
resutlMap.put("userList",userList);
return R.ok(resutlMap);
}
}
测试
前端架构搭建
用vue ui来搭建vue项目;
vue ui是一个可视化图形界面,方便你去创建、更新和管理vue项目,包括下载router,vuex,axios,
elementui等插件,配置好一些属性以及依赖关系,方便我们使用,我个人第一次接触它就感觉非常非
常非常智能和强大。
安装node
安装Vue Cli
vue-cli 是一个官方发布 vue.js 项目脚手架,使用 vue-cli 可以快速创建 vue 项目。
因为vue ui是在Vue CLI基础上封装的
npm install -g @vue/cli
vue ui搭建vue项目
vue ui
创建成功
webstorm导入
启动
npm run serve
启动成功
安装axios element-plus
element-plus官网:https://element-plus.org/zh-CN/guide/design.html
main.js里添加代码
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
createApp(App).use(store).use(router).use(ElementPlus).mount('#app')
添加代码
<template>
<el-row class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</el-row>
<el-row class="mb-4">
<el-button plain>Plain</el-button>
<el-button type="primary" plain>Primary</el-button>
<el-button type="success" plain>Success</el-button>
<el-button type="info" plain>Info</el-button>
<el-button type="warning" plain>Warning</el-button>
<el-button type="danger" plain>Danger</el-button>
</el-row>
<el-row class="mb-4">
<el-button round>Round</el-button>
<el-button type="primary" round>Primary</el-button>
<el-button type="success" round>Success</el-button>
<el-button type="info" round>Info</el-button>
<el-button type="warning" round>Warning</el-button>
<el-button type="danger" round>Danger</el-button>
</el-row>
<el-row>
<el-button :icon="Search" circle />
<el-button type="primary" :icon="Edit" circle />
<el-button type="success" :icon="Check" circle />
<el-button type="info" :icon="Message" circle />
<el-button type="warning" :icon="Star" circle />
<el-button type="danger" :icon="Delete" circle />
</el-row>
</template>
<script setup>
import {
Check,
Delete,
Edit,
Message,
Search,
Star,
} from '@element-plus/icons-vue'
</script>
点击about 测试成功
引入JWT前后端交互
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC
7519);
JWT就是一段字符串,用来进行用户身份认证的凭证,该字符串分成三段【头部、载荷、签证】
JwtUtils
package com.java.util;
import com.java.common.constant.JwtConstant;
import com.java.entity.CheckResult;
import io.jsonwebtoken.*;
import org.bouncycastle.util.encoders.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
/**
* jwt加密和解密的工具类
*/
public class JwtUtils {
/**
* 签发JWT
* @param id
* @param subject 可以是JSON数据 尽可能少
* @param ttlMillis
* @return
*/
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 builder = Jwts.builder()
.setId(id)
.setSubject(subject) // 主题
.setIssuer("Java1234") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate); // 过期时间
}
return builder.compact();
}
/**
* 生成jwt token
* @param username
* @return
*/
public static String genJwtToken(String username){
return createJWT(username,username,60*60*1000);
}
/**
* 验证JWT
* @param jwtStr
* @return
*/
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
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.decode(JwtConstant.JWT_SECERT);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析JWT字符串
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
public static void main(String[] args) throws InterruptedException {
//小明失效 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());
//Thread.sleep(3000);
System.out.println(validateJWT(sc).getClaims());
Claims claims = validateJWT(sc).getClaims();
String sc2 = createJWT(claims.getId(),claims.getSubject(), JwtConstant.JWT_TTL);
System.out.println(sc2);
}
}
JwtConstant
package com.java.common.constant;
/**
* 系统级静态变量
*/
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 = "8677df7fc3a34e26a61c034d5ec8245d"; //密匙
public static final long JWT_TTL = 24*60 * 60 * 1000; //token有效时间
}
CheckResult
package com.java.entity;
import io.jsonwebtoken.Claims;
/**
* jwt验证信息
*/
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;
}
}
测试
TestController
@RequestMapping("/user/list")
public R userList(@RequestHeader(required = false)String token){
if(StringUtil.isNotEmpty(token)){
Map<String,Object> resutlMap=new HashMap<>();
List<SysUser> userList = sysUserService.list();
resutlMap.put("userList",userList);
return R.ok(resutlMap);
}else{
return R.error(401,"没有权限访问");
}
}
@RequestMapping("/login")
public R login(){
String token = JwtUtils.genJwtToken("java1234");
return R.ok().put("token",token);
}
引入stringUtil
package com.java.util;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 字符串工具类
* @author
*
*/
public class StringUtil {
/**
* 判断是否是空
* @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();
}
}
前端请求后端用axios
前端新建 request.js
// 引入axios
import axios from 'axios';
import store from '@/store'
let baseUrl="http://localhost:8080/";
// 创建axios实例
const httpService = axios.create({
// url前缀-'http:xxx.xxx'
// baseURL: process.env.BASE_API, // 需自定义
baseURL:baseUrl,
// 请求超时时间
timeout: 3000 // 需自定义
});
//添加请求和响应拦截器
// 添加请求拦截器
httpService.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
//config.headers.token=window.sessionStorage.getItem('token');
console.log("store="+store.getters.GET_TOKEN)
config.headers.token=store.getters.GET_TOKEN
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
httpService.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
/*网络请求部分*/
/*
* get请求
* url:请求地址
* params:参数
* */
export function get(url, params = {}) {
return new Promise((resolve, reject) => {
httpService({
url: url,
method: 'get',
params: params
}).then(response => {
resolve(response);
}).catch(error => {
reject(error);
});
});
}
/*
* post请求
* url:请求地址
* params:参数
* */
export function post(url, params = {}) {
return new Promise((resolve, reject) => {
httpService({
url: url,
method: 'post',
data: params
}).then(response => {
console.log(response)
resolve(response);
}).catch(error => {
console.log(error)
reject(error);
});
});
}
/*
* 文件上传
* url:请求地址
* params:参数
* */
export function fileUpload(url, params = {}) {
return new Promise((resolve, reject) => {
httpService({
url: url,
method: 'post',
data: params,
headers: { 'Content-Type': 'multipart/form-data' }
}).then(response => {
resolve(response);
}).catch(error => {
reject(error);
});
});
}
export function getServerUrl(){
return baseUrl;
}
export default {
get,
post,
fileUpload,
getServerUrl
}
APP.VUE
<template>
<el-button type="primary" @click="handleLogin">测试登录</el-button>
<el-button type="danger" @click="handleUserList">测试获取用户请求</el-button>
</template>
<script setup>
import requestUtil from '@/util/request'
import store from '@/store'
const handleLogin=async ()=>{
let result=await requestUtil.get("test/login");
let data=result.data;
if(data.code=200){
const token=data.token;
console.log("登录成功:token="+token);
store.commit('SET_TOKEN',token);
}else{
console.log("登录出错!");
}
}
const handleUserList=async ()=>{
let result=await requestUtil.get("test/user/list");
let data=result.data;
if(data.code=200){
const userList=data.userList;
console.log("用户列表信息:userList="+userList);
}
}
</script>
<style>
</style>
store.js里
import { createStore } from 'vuex'
export default createStore({
state: {
},
getters: {
GET_TOKEN:state => {
return sessionStorage.getItem("token")
}
},
mutations: {
SET_TOKEN:(state,token)=>{
sessionStorage.setItem("token",token);
}
},
actions: {
},
modules: {
}
})
跨域问题报错
后端新建config
package com.java.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* web项目配置类
*/
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE","OPTIONS")
.maxAge(3600);
}
}
测试成功
登录功能实现
前端新建文件
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
@charset "utf-8";
html{font-size:12px}
body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0}
body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height:1.5;font-family:tahoma,arial,"Hiragino Sans GB",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%}
h1,h2,h3,h4,h5,h6{font-family:tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal}
address,cite,dfn,em,i,optgroup,var{font-style:normal}
table{border-collapse:collapse;border-spacing:0;text-align:left}
caption,th{text-align:inherit}
ul,ol,menu{list-style:none}
fieldset,img{border:0}
img,object,input,textarea,button,select{vertical-align:middle}
article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
blockquote:before,blockquote:after,q:before,q:after{content:"\0020"}
textarea{overflow:auto;resize:vertical}
input,textarea,button,select,a{outline:0 none;border: none;}
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}
mark{background-color:transparent}
a,ins,s,u,del{text-decoration:none}
sup,sub{vertical-align:baseline}
html {overflow-x: hidden;height: 100%;font-size: 50px;-webkit-tap-highlight-color: transparent;}
body {font-family: Arial, "Microsoft Yahei", "Helvetica Neue", Helvetica, sans-serif;color: #333;font-size: .28em;line-height: 1;-webkit-text-size-adjust: none;}
hr {height: .02rem;margin: .1rem 0;border: medium none;border-top: .02rem solid #cacaca;}
a {color: #25a4bb;text-decoration: none;}
main.js导入样式文件:
import '@/assets/styles/border.css'
import '@/assets/styles/reset.css'
添加路由
安装sass和sass-loader依赖
新建view Login.vue
<template>
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">Java1234 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"
>
</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">java1234.vip</a> 版权所有.</span>
</div>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
a{
color:white
}
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.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>
App.vue设置下全局样式:
<style>
html,body,#app{
height: 100%;
}
.app-container{
padding:20px
}
</style>
自定义icon实现
新建
<template>
<svg class="svg-icon" aria-hidden="true">
<use :xlink:href="iconName"></use>
</svg>
</template>
<script setup>
import { defineProps, computed } from 'vue'
const props = defineProps({
icon: {
type: String,
required: true
}
})
const iconName = computed(() => {
return `#icon-${props.icon}`
})
</script>
<style lang="scss" scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
src下新建文件夹 新建index.js
安装依赖 webpack
安装依赖 svg-sprite-loader
vue.config.js
// const { defineConfig } = require('@vue/cli-service')
// module.exports = defineConfig({
// transpileDependencies: true
// })
const webpack = require('webpack');
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
lintOnSave: false,
chainWebpack(config) {
// 设置 svg-sprite-loader
// config 为 webpack 配置对象
// config.module 表示创建一个具名规则,以后用来修改规则
config.module
// 规则
.rule('svg')
// 忽略
.exclude.add(resolve('src/icons'))
// 结束
.end()
// config.module 表示创建一个具名规则,以后用来修改规则
config.module
// 规则
.rule('icons')
// 正则,解析 .svg 格式文件
.test(/\.svg$/)
// 解析的文件
.include.add(resolve('src/icons'))
// 结束
.end()
// 新增了一个解析的loader
.use('svg-sprite-loader')
// 具体的loader
.loader('svg-sprite-loader')
// loader 的配置
.options({
symbolId: 'icon-[name]'
})
// 结束
.end()
config
.plugin('ignore')
.use(
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn$/)
)
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
}
}
修改main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import '@/assets/styles/border.css'
import '@/assets/styles/reset.css'
import SvgIcon from '@/icons'
//
// createApp(App).use(store).use(router).use(ElementPlus).mount('#app')
const app=createApp(App);
SvgIcon(app);
app.use(store)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
Login.vue添加代码
SpringSecurity执行原理概述
spring security的简单原理:
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 安全上下文容器将第3步填充了信息的 Authentication ,通过
SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出
AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实
现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个
List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道
web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着
一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至
Authentication。
授权管理
访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用
SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器
AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户
的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决
定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转
到403页面、自定义页面。
转载自:https://blog.csdn.net/weixin_51542566/article/details/119705963
项目整合SpringSecurity
pom.xml加下springsecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
新建配置类
package com.java.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;
/**
* spring security配置
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String URL_WHITELIST[] ={
"/login",
"/logout",
"/captcha",
"/password",
"/image/**",
"/test/**"
} ;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启跨域 以及csrf攻击 关闭
http
.cors()
.and()
.csrf()
.disable()
// 登录登出配置
.formLogin()
// .successHandler()
// .failureHandler()
// .and()
// .logout()
// .logoutSuccessHandler()
// session禁用配置
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
// 拦截规则配置
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll() // 白名单 放行
.anyRequest().authenticated();
// 异常处理配置
// 自定义过滤器配置
}
}
重写登录成功和登录失败处理器
common下新建security包,再新建两个类,LoginSuccessHandler和LoginFailureHandler
LoginSuccessHandler
package com.java.common.security;
import cn.hutool.json.JSONUtil;
import com.java.entity.R;
import com.java.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;
/**
* 登录成功处理器
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
String username="user";
String token = JwtUtils.genJwtToken(username);
outputStream.write(JSONUtil.toJsonStr(R.ok("登录成功").put("authorization",token)).getBytes());
outputStream.flush();
outputStream.close();
}
}
LoginFailureHandler
package com.java.common.security;
import cn.hutool.json.JSONUtil;
import com.java.entity.R;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
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;
/**
* 登录成功处理器
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
String message=e.getMessage();
if(e instanceof BadCredentialsException){
message="用户名或者密码错误!";
}
outputStream.write(JSONUtil.toJsonStr(R.error(message)).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
注入
前端
Login.vue页面修改
script 里添加代码
<script setup>
import {ref} from 'vue'
import requestUtil from '@/util/request'
import store from '@/store'
import qs from "qs"
import {ElMessage} from "element-plus"
const loginRef=ref(null)
const loginForm=ref({
username:"",
password:""
})
const loginRules = {
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }]
};
const handleLogin=()=>{
loginRef.value.validate(async(valid)=>{
if(valid){
let result=await requestUtil.post("login?"+qs.stringify(loginForm.value))
let data=result.data;
if(data.code==200){
const token=data.authorization
store.commit('SET_TOKEN',token);
}else{
ElMessage.error(data.msg)
}
}else{
console.log("验证失败")
}
})
}
</script>
安装qs
什么都不输入验证失败
随便输入密码
输入正确密码验证成功
token也存在
用户登录SpringSecurity查库实现
security包下新建MyUserDetailServiceImpl
package com.java.common.security;
import com.java.common.exception.UserCountLockException;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 自定义UserDetails
*/
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser=sysUserService.getByUsername(username);
if(sysUser==null){
throw new UsernameNotFoundException("用户名或者密码错误!");
}else if("1".equals(sysUser.getStatus())){
throw new UserCountLockException("该用户账号被封禁,具体请联系管理员!");
}
return new User(sysUser.getUsername(),sysUser.getPassword(),getUserAuthority());
}
private List<GrantedAuthority> getUserAuthority() {
return new ArrayList<>();
}
}
SysUserService里新建接口
SysUser getByUsername(String username);
以及对应实现
SysUserServiceImpl
@Override
public SysUser getByUsername(String username) {
return getOne(new QueryWrapper<SysUser>().eq("username",username));
}
以及自定义异常类
全局异常
package com.java.common.exception;
import com.java.entity.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全部异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = RuntimeException.class)
public R handler(RuntimeException e){
log.error("运行时异常:---------{}"+e.getMessage());
return R.error(e.getMessage());
}
}
package com.java.common.security;
import com.java.common.exception.UserCountLockException;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 自定义UserDetails
*/
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser=sysUserService.getByUsername(username);
if(sysUser==null){
throw new UsernameNotFoundException("用户名或者密码错误!");
}else if("1".equals(sysUser.getStatus())){
throw new UserCountLockException("该用户账号被封禁,具体请联系管理员!");
}
return new User(sysUser.getUsername(),sysUser.getPassword(),getUserAuthority());
}
private List<GrantedAuthority> getUserAuthority() {
return new ArrayList<>();
}
}
修改SecurityConfig.java
测试成功
实现JWT认证过滤器
router配置,加下首页路由
{
path: '/',
name: '首页',
component: () => import('../layout')
},
新建主页
<template>
<el-button type="danger" @click="testHandler">测试接口</el-button>
</template>
<script setup>
import requestUtil from '@/util/request'
const testHandler=async ()=>{
let result=await requestUtil.get("test/user/list");
}
</script>
<style scoped>
</style>
Login.vue
import router from "@/router"
const handleLogin=()=>{
loginRef.value.validate(async(valid)=>{
if(valid){
let result=await requestUtil.post("login?"+qs.stringify(loginForm.value))
let data=result.data;
if(data.code==200){
const token=data.authorization
store.commit('SET_TOKEN',token);
router.replace("/")
}else{
ElMessage.error(data.msg)
}
}else{
console.log("验证失败")
}
})
}
成功进入主页
删掉这部分
securityconfig里添加配置
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter=new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
// 自定义过滤器配置
.and()
.addFilter(jwtAuthenticationFilter());
LoginSuccessHandler里修改代码
新建
package com.java.common.security;
import com.java.common.constant.JwtConstant;
import com.java.entity.CheckResult;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import com.java.util.JwtUtils;
import com.java.util.StringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
/**
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
private SysUserService sysUserService;
@Autowired
private MyUserDetailsServiceImpl myUserDetailsService;
private static final String URL_WHITELIST[] ={
"/login",
"/logout",
"/captcha",
"/password",
"/image/**"
} ;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader("token");
System.out.println("请求url:"+request.getRequestURI());
// 如果token是空 或者 url在白名单里,则放行
if(StringUtil.isEmpty(token) || new ArrayList<String>(Arrays.asList(URL_WHITELIST)).contains(request.getRequestURI())){
chain.doFilter(request,response);
return;
}
CheckResult checkResult = JwtUtils.validateJWT(token);
if(!checkResult.isSuccess()){
switch (checkResult.getErrCode()){
case JwtConstant.JWT_ERRCODE_NULL:throw new JwtException("Token不存在");
case JwtConstant.JWT_ERRCODE_FAIL:throw new JwtException("Token验证不通过");
case JwtConstant.JWT_ERRCODE_EXPIRE:throw new JwtException("Token过期");
}
}
Claims claims = JwtUtils.parseJWT(token);
String username = claims.getSubject();
SysUser sysUser = sysUserService.getByUsername(username);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(username,null,myUserDetailsService.getUserAuthority());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
chain.doFilter(request,response);
}
}
修改token
不可访问
实现JWT认证异常处理器
新建 JwtAuthenticationEntryPoint
package com.java.common.security;
import cn.hutool.json.JSONUtil;
import com.java.entity.R;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
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;
/**
* jwt认证失败处理
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(R.error(HttpServletResponse.SC_UNAUTHORIZED,"认证失败,请登录!")).getBytes());
outputStream.flush();
outputStream.close();
}
}
SecurityConfig里添加配置
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// 异常处理配置
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
修改token
认证失败
实现自定义logout处理
默认logout请求实现是有状态的,返回到login请求页面;我们现在是前后端分离处理,所以需要自定义
实现logout
新建JwtLogoutSuccessHandler
package com.java.common.security;
import cn.hutool.json.JSONUtil;
import com.java.entity.R;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
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;
/**
* 自定义logout处理
*/
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(R.ok("登出成功")).getBytes());
outputStream.flush();
outputStream.close();
}
}
SecurityConfig添加配置
@Autowired
private JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
前端添加按钮
<template>
<el-button type="danger" @click="testHandler">测试接口</el-button>
<el-button type="danger" @click="testLogoutHandler">测试logout登出</el-button>
</template>
<script setup>
import requestUtil from '@/util/request'
const testHandler=async ()=>{
let result=await requestUtil.get("test/user/list");
}
const testLogoutHandler=async ()=>{
let result=await requestUtil.get("logout");
}
</script>
<style scoped>
</style>
登出成功
获取用户角色权限信息实现
springsecurity鉴权需要获取用户的角色权限系统,包括前端也需要这些信息;
首先我们新建角色表sys_role,菜单权限表sys_menu,用户角色关联表sys_user_role,角色菜单权限关
联表sys_role_menu
角色表sys_role 菜单权限表sys_menu 用户角色关联表sys_user_role 角色菜单权限关联表sys_role_menu
/*
SQLyog Ultimate v11.33 (64 bit)
MySQL - 5.7.18-log : Database - db_admin3
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`db_admin3` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `db_admin3`;
/*Table structure for table `sys_menu` */
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单主键ID',
`name` varchar(50) DEFAULT NULL COMMENT '菜单名称',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID',
`order_num` int(11) DEFAULT '0' COMMENT '显示顺序',
`path` varchar(200) DEFAULT '' COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`perms` varchar(100) DEFAULT '' COMMENT '权限标识',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8;
/*Data for the table `sys_menu` */
insert into `sys_menu`(`id`,`name`,`icon`,`parent_id`,`order_num`,`path`,`component`,`menu_type`,`perms`,`create_time`,`update_time`,`remark`) values (1,'系统管理','system',0,1,'/sys','','M','','2022-07-04 14:56:29','2022-07-04 14:56:31','系统管理目录'),(2,'业务管理','monitor',0,2,'/bsns','','M','','2022-07-04 14:59:43','2022-07-04 14:59:45','业务管理目录'),(3,'用户管理','user',1,1,'/sys/user','sys/user/index','C','system:user:list','2022-07-04 15:20:51','2022-07-04 15:20:53','用户管理菜单'),(4,'角色管理','peoples',1,2,'/sys/role','sys/role/index','C','system:role:list','2022-07-04 15:23:35','2022-07-04 15:23:39','角色管理菜单'),(5,'菜单管理','tree-table',1,3,'/sys/menu','sys/menu/index','C','system:menu:list','2022-07-04 15:23:41','2022-07-04 15:23:43','菜单管理菜单'),(6,'部门管理','tree',2,1,'/bsns/department','bsns/Department','C','','2022-07-04 15:24:40','2022-07-04 15:24:44','部门管理菜单'),(7,'岗位管理','post',2,2,'/bsns/post','bsns/Post','C','','2022-07-04 15:24:42','2022-07-04 15:24:46','岗位管理菜单'),(8,'用户新增','#',3,2,'','','F','system:user:add','2022-07-04 15:24:42','2022-07-04 15:24:46','添加用户按钮'),(9,'用户修改','#',3,3,'','','F','system:user:edit','2022-07-04 15:24:42','2022-07-04 15:24:46','修改用户按钮'),(10,'用户删除','#',3,4,'','','F','system:user:delete','2022-07-04 15:24:42','2022-07-04 15:24:46','删除用户按钮'),(11,'分配角色','#',3,5,'','','F','system:user:role','2022-07-04 15:24:42','2022-07-04 15:24:46','分配角色按钮'),(12,'重置密码','#',3,6,'','','F','system:user:resetPwd','2022-07-04 15:24:42','2022-07-04 15:24:46','重置密码按钮'),(13,'角色新增','#',4,2,'','','F','system:role:add','2022-07-04 15:24:42','2022-07-04 15:24:46','添加用户按钮'),(14,'角色修改','#',4,3,'','','F','system:role:edit','2022-07-04 15:24:42','2022-07-04 15:24:46','修改用户按钮'),(15,'角色删除','#',4,4,'',NULL,'F','system:role:delete','2022-07-04 15:24:42','2022-07-04 15:24:46','删除用户按钮'),(16,'分配权限','#',4,5,'','','F','system:role:menu','2022-07-04 15:24:42','2022-07-04 15:24:46','分配权限按钮'),(17,'菜单新增','#',5,2,'',NULL,'F','system:menu:add','2022-07-04 15:24:42','2022-07-04 15:24:46','添加菜单按钮'),(18,'菜单修改','#',5,3,'',NULL,'F','system:menu:edit','2022-07-04 15:24:42','2022-07-04 15:24:46','修改菜单按钮'),(19,'菜单删除','#',5,4,'',NULL,'F','system:menu:delete','2022-07-04 15:24:42','2022-07-04 15:24:46','删除菜单按钮'),(20,'用户查询','#',3,1,'',NULL,'F','system:user:query','2022-07-04 15:24:42','2022-07-04 15:24:46','用户查询按钮'),(21,'角色查询','#',4,1,'',NULL,'F','system:role:query','2022-07-04 15:24:42','2022-07-04 15:24:46','角色查询按钮'),(22,'菜单查询','#',5,1,'',NULL,'F','system:menu:query','2022-07-04 15:24:42','2022-07-04 15:24:46','菜单查询按钮'),(33,'测速22','122',3,3,'','34','M','33','2022-08-19 03:11:20','2022-08-18 19:11:33',NULL);
/*Table structure for table `sys_role` */
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色主键ID',
`name` varchar(30) DEFAULT NULL COMMENT '角色名称',
`code` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;
/*Data for the table `sys_role` */
insert into `sys_role`(`id`,`name`,`code`,`create_time`,`update_time`,`remark`) values (1,'超级管理员','admin','2022-07-04 14:40:44','2022-07-04 14:40:47','拥有系统最高权限'),(2,'普通角色','common','2022-07-04 14:41:56','2022-07-04 14:41:58','普通角色'),(3,'测试角色','test','2022-07-04 14:42:24','2022-07-04 14:42:27','测试角色'),(4,'2',NULL,NULL,NULL,NULL),(5,'3',NULL,NULL,NULL,NULL),(6,'4',NULL,NULL,NULL,NULL),(7,'5',NULL,NULL,NULL,NULL),(14,'6',NULL,NULL,NULL,NULL),(16,'8',NULL,NULL,NULL,NULL),(17,'0',NULL,NULL,NULL,NULL),(19,'测2','cc2','2022-08-13 21:06:21','2022-08-13 13:06:27','eewew2'),(20,'ccc测试','test2','2022-08-29 17:10:33',NULL,'xxx'),(21,'今天测试角色','todytest','2022-08-29 22:01:11',NULL,'ccc');
/*Table structure for table `sys_role_menu` */
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色菜单主键ID',
`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
`menu_id` bigint(20) DEFAULT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=239 DEFAULT CHARSET=utf8;
/*Data for the table `sys_role_menu` */
insert into `sys_role_menu`(`id`,`role_id`,`menu_id`) values (8,2,1),(9,2,2),(10,2,3),(11,2,4),(12,2,5),(13,2,6),(14,2,7),(15,3,2),(16,3,6),(17,3,7),(21,7,1),(22,7,2),(23,7,6),(24,7,7),(25,6,1),(26,6,3),(27,6,9),(28,6,10),(29,19,1),(30,19,3),(31,19,2),(32,19,6),(33,1,1),(34,1,3),(35,1,20),(36,1,8),(37,1,9),(38,1,10),(39,1,11),(40,1,12),(41,1,4),(42,1,21),(43,1,13),(44,1,14),(45,1,15),(46,1,16),(47,1,23),(48,1,5),(49,1,22),(50,1,17),(51,1,18),(52,1,19),(53,1,2),(54,1,6),(55,1,7),(208,20,1),(209,20,3),(210,20,20),(211,20,8),(212,20,9),(213,20,33),(214,20,10),(215,20,11),(216,20,4),(217,20,21),(218,20,13),(219,20,5),(220,20,22),(221,20,17),(222,20,18),(223,20,2),(224,20,6),(225,20,7),(232,21,1),(233,21,9),(234,21,4),(235,21,21),(236,21,2),(237,21,6),(238,21,7);
/*Table structure for table `sys_user` */
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL 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 '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8;
/*Data for the table `sys_user` */
insert into `sys_user`(`id`,`username`,`password`,`avatar`,`email`,`phonenumber`,`login_date`,`status`,`create_time`,`update_time`,`remark`) values (1,'java1234','$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');
/*Table structure for table `sys_user_role` */
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户角色主键ID',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8;
/*Data for the table `sys_user_role` */
insert into `sys_user_role`(`id`,`user_id`,`role_id`) values (1,1,1),(2,2,2),(4,1,2),(6,3,3),(7,3,2),(9,4,3),(10,5,3),(11,15,3),(16,28,2),(17,28,3),(20,29,20),(21,30,17),(22,30,21);
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
我们通过MybatisX生成代码:
因为每个实体都有几个通用属性,id,createTime,updateTime,remark,所以我们搞一个通用基础
实体类,让其他类继承下;
方便维护;
新建BaseEntity
package com.java.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 公共基础实体类
*/
@Data
public class BaseEntity implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 创建日期
*/
@JsonSerialize(using=CustomDateTimeSerializer.class)
@JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新日期
*/
@JsonSerialize(using=CustomDateTimeSerializer.class)
@JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 备注
*/
private String remark;
}
自定义返回JSON 数据格式中日期格式化处理
package com.java.entity;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* 自定义返回JSON 数据格式中日期格式化处理
*
*/
public class CustomDateTimeSerializer extends JsonSerializer<Date>{
@Override
public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
gen.writeString(sdf.format(value));
}
}
继承该类,删除相同属性
SysUserServiceImpl``的实现方法getUserAuthorityInfo
public List<GrantedAuthority> getUserAuthority(Long userId) {
// 格式ROLE_admin,ROLE_common,system:user:resetPwd,system:role:delete,system:user:list,system:menu:query,system:menu:list,system:menu:add,system:user:delete,system:role:list,system:role:menu,system:user:edit,system:user:query,system:role:edit,system:user:add,system:user:role,system:menu:delete,system:role:add,system:role:query,system:menu:edit
String authority=sysUserService.getUserAuthorityInfo(userId);
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
生成方法
实现
package com.java.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.java.entity.SysMenu;
import com.java.entity.SysRole;
import com.java.entity.SysUser;
import com.java.mapper.SysMenuMapper;
import com.java.mapper.SysRoleMapper;
import com.java.service.SysUserService;
import com.java.mapper.SysUserMapper;
import com.java.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author 86182
* @description 针对表【sys_user】的数据库操作Service实现
* @createDate 2025-05-04 17:19:02
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser>
implements SysUserService{
@Autowired
SysRoleMapper sysRoleMapper;
@Autowired
SysMenuMapper sysMenuMapper;
@Override
public SysUser getByUsername(String username) {
return getOne(new QueryWrapper<SysUser>().eq("username",username));
}
@Override
public String getUserAuthorityInfo(Long userId) {
StringBuffer authority=new StringBuffer();
// 根据用户id获取所有的角色信息
List<SysRole> roleList = sysRoleMapper.selectList(new QueryWrapper<SysRole>().inSql("id", "SELECT role_id FROM sys_user_role WHERE user_id=" + userId));
if(roleList.size()>0){
String roleCodeStrs = roleList.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
authority.append(roleCodeStrs);
}
// 遍历所有的角色,获取所有菜单权限 而且不重复
Set<String> menuCodeSet=new HashSet<>();
for(SysRole sysRole:roleList){
List<SysMenu> sysMenuList = sysMenuMapper.selectList(new QueryWrapper<SysMenu>().inSql("id", "SELECT menu_id FROM sys_role_menu WHERE role_id=" + sysRole.getId()));
for(SysMenu sysMenu:sysMenuList){
String perms=sysMenu.getPerms();
if(StringUtil.isNotEmpty(perms)){
menuCodeSet.add(perms);
}
}
}
if(menuCodeSet.size()>0){
authority.append(",");
String menuCodeStrs = menuCodeSet.stream().collect(Collectors.joining(","));
authority.append(menuCodeStrs);
}
System.out.println("authority:"+authority.toString());
return authority.toString();
}
}
测试成功
测试
可以访问
改为
不可访问
测试权限
不可访问
删除2可访问
@PreAuthorize("hasAuthority('system:user:list')")
记住密码功能实现
记住密码,我们通过cookie来实现,先安装依赖 ‘js-cookie’
存储用户密码,为了安全需要加密,获取密码解密。所以我们安装依赖’jsencrypt’
util下新建jsencrypt.js
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey =
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
const privateKey =
'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
'7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
'UP8iWi1Qw0Y='
// 加密
export function encrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
// 解密
export function decrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPrivateKey(privateKey) // 设置私钥
return encryptor.decrypt(txt) // 对数据进行解密
}
Login.vue导入
import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/util/jsencrypt";
添加代码
// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 });
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 });
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 });
} else {
// 否则移除
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
添加查询代码
function getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
loginForm.value = {
username: username === undefined ? loginForm.value.username : username,
password: password === undefined ? loginForm.value.password :
decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};
}
getCookie();
记录成功
不选中就不会自动填充
主页面功能实现
主页面布局实现
<template>
<div class="app-wrapper">
<el-container>
<el-aside width="200px" class="sidebar-container"><Menu/></el-aside>
<el-container>
<el-header><Header/></el-header>
<el-main><Tabs/></el-main>
<el-footer><Footer/></el-footer>
</el-container>
</el-container>
</div>
</template>
<script setup>
import Menu from '@/layout/menu'
import Header from '@/layout/header'
import Footer from '@/layout/footer'
import Tabs from '@/layout/tabs'
</script>
<style scoped>
.app-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.sidebar-container {
background-color: #2d3a4b;
height: 100%;
}
.el-container{
height:100%
}
.el-header{
padding-left: 0px;
padding-right: 0px;
}
:deep(ul.el-menu){
border-right-width: 0px
}
</style>
新建文件
成功显示
LoginSuccessHandler里添加
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
String username=authentication.getName();
String token = JwtUtils.genJwtToken(username);
SysUser currentUser = sysUserService.getByUsername(username);
// 根据用户id获取所有的角色信息
List<SysRole> roleList = sysRoleService.list(new QueryWrapper<SysRole>().inSql("id", "SELECT role_id FROM sys_user_role WHERE user_id=" + currentUser.getId()));
// 遍历所有的角色,获取所有菜单权限 而且不重复
Set<SysMenu> menuSet=new HashSet<>();
for(SysRole sysRole:roleList){
List<SysMenu> sysMenuList = sysMenuService.list(new QueryWrapper<SysMenu>().inSql("id", "SELECT menu_id FROM sys_role_menu WHERE role_id=" + sysRole.getId()));
for(SysMenu sysMenu:sysMenuList){
menuSet.add(sysMenu);
}
}
List<SysMenu> sysMenuList=new ArrayList<>(menuSet);
// 排序
sysMenuList.sort(Comparator.comparing(SysMenu::getOrderNum));
// 转菜单树
List<SysMenu> menuList=sysMenuService.buildTreeMenu(sysMenuList);
outputStream.write(JSONUtil.toJsonStr(R.ok("登录成功").put("authorization",token).put("currentUser",currentUser).put("menuList",menuList)).getBytes());
outputStream.flush();
outputStream.close();
}
SysMenuService里新建方法
对应实现
@Override
public List<SysMenu> buildTreeMenu(List<SysMenu> sysMenuList) {
List<SysMenu> resultMenuList=new ArrayList<>();
for(SysMenu sysMenu:sysMenuList){
// 寻找子节点
for(SysMenu e:sysMenuList){
if(e.getParentId()==sysMenu.getId()){
sysMenu.getChildren().add(e);
}
}
if(sysMenu.getParentId()==0L){
resultMenuList.add(sysMenu);
}
}
return resultMenuList;
}
SysMenu里添加字段
@TableField(exist = false)
private List<SysMenu> children=new ArrayList<>();
<template>
<el-menu
active-text-color="#ffd04b"
background-color="#2d3a4b"
class="el-menu-vertical-demo"
text-color="#fff"
router
:default-active="'/index'"
>
<el-menu-item index="/index">
<el-icon><home-filled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-sub-menu :index="menu.path" v-for="menu in menuList">
<template #title>
<el-icon><svg-icon :icon="menu.icon"/></el-icon>
<span>{{ menu.name }}</span>
</template>
<el-menu-item :index="item.path" v-for="item in menu.children">
<el-icon><svg-icon :icon="item.icon"/></el-icon>
<span>{{ item.name }}</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import {HomeFilled,User,Tickets,Goods,DocumentAdd,Management,Setting,Edit,SwitchButton,Promotion} from '@element-plus/icons-vue'
import {ref} from 'vue'
import store from '@/store'
const menuList=ref(store.getters.GET_MENULIST);
</script>
<style lang="scss" scoped>
</style>
显示成功
右上角用户头像显示实现
这里有个用户头像,虚拟路径映射配置下:
WebAppConfigurer类:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/image/userAvatar/**").addResourceLocations("file:D:\\codeWorkspace\\javaWorkspace\\data\\rightUserAvatar\\");
}
Login.vue
avatar.vue
<template>
<el-dropdown>
<span class="el-dropdown-link">
<el-avatar shape="square" :size="40" :src="squareUrl" />
{{currentUser.username}}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item @click="logout">安全退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup>
import { ArrowDown } from '@element-plus/icons-vue'
import {ref} from 'vue'
import store from '@/store'
import requestUtil,{getServerUrl} from '@/util/request'
const currentUser=ref(store.getters.GET_USERINFO);
const squareUrl=ref(getServerUrl()+'image/userAvatar/'+currentUser.value.avatar)
const logout=async ()=>{
let result=await requestUtil.get("/logout")
if(result.data.code==200){
store.dispatch('logout')
}
}
</script>
<style lang="scss" scoped>
.el-dropdown-link {
cursor: pointer;
color: var(--el-color-primary);
display: flex;
align-items: center;
}
</style>
breadcrumb.vue
<template>
面包屑
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>
index.vue
<template>
<div class="navbar">
<Breadcrumb/>
<div class="navbar-right">
<Avatar/>
</div>
</div>
</template>
<script setup>
import Breadcrumb from './components/breadcrumb.vue'
import Avatar from './components/avatar.vue'
</script>
<style lang="scss" scoped>
.navbar {
width: 100%;
height: 60px;
overflow: hidden;
background-color: #F5F5F5;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 0 16px;
display: flex;
align-items: center;
box-sizing: border-box;
position: relative;
.navbar-right {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
:deep(.navbar-item) {
display: inline-block;
margin-left: 18px;
font-size: 22px;
color: #5a5e66;
box-sizing: border-box;
cursor: pointer;
}
}
}
</style>
显示成功
登出成功
路由守卫功能实现
前端如果没有登录过,也就没有token,则自动跳转到登录页面,这个就是路由守卫。
我们通过 router.beforeEach((to, from, next)=>{}) 实现
router目录下新建permission.js
import router from "@/router/index"
import store from "@/store"
router.beforeEach((to,from,next)=>{
const whiteList=['/login'] // 白名单
let token=store.getters.GET_TOKEN;
if(token){
next();
}else{
if(whiteList.includes(to.path)){
next();
}else{
next("/login")
}
}
})
main.js添加代码
自动跳转到login页面
动态路由实现
我们vue路由信息,需要通过后端查询的menuList,动态设置到router里面去;
layout index.vue 加下
修改permission.js
import router from "@/router/index"
import store from "@/store"
router.beforeEach((to,from,next)=>{
const whiteList=['/login'] // 白名单
let token=store.getters.GET_TOKEN;
let hasRoutes=store.state.hasRoutes;
let menuList=store.getters.GET_MENULIST;
if(token){
if(!hasRoutes){
bindRoute(menuList);
store.commit("SET_ROUTES_STATE",true);
}
next();
}else{
if(whiteList.includes(to.path)){
next();
}else{
next("/login")
}
}
})
// 动态绑定路由
const bindRoute=(menuList)=>{
let newRoutes=router.options.routes;
menuList.forEach(menu=>{
if(menu.children){
menu.children.forEach(m=>{
let route=menuToRoute(m,menu.name);
if(route){
newRoutes[0].children.push(route);
}
})
}
})
// 重新添加到路由
newRoutes.forEach(route=>{
router.addRoute(route)
})
}
// 菜单对象转成路由对象
const menuToRoute=(menu,parentName)=>{
if(!menu.component){
return null;
}else{
let route={
name:menu.name,
path:menu.path,
meta:{
parentName:parentName
}
}
route.component=()=>import('@/views/'+menu.component+'.vue');
return route;
}
}
修改router index.js
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('../layout'),
redirect:'/index',
children:[
{
path: '/index',
name: '首页',
component: () => import('../views/index/index')
},
{
path: '/userCenter',
name: '个人中心',
component: () => import('../views/userCenter/index')
},
]
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
显示成功
动态标签实现
store添加默认tabs数组,以及默认选中的tab值,以及添加和充值tab
menu index.vue
修改tabs index.vue
显示成功
修改代码tabs index.vue
<template>
<el-tabs
v-model="editableTabsValue"
type="card"
class="demo-tabs"
closable
@tab-remove="removeTab"
@tab-click="clickTab"
>
<el-tab-pane
v-for="item in editableTabs"
:key="item.name"
:label="item.title"
:name="item.name"
>
{{ item.content }}
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref,watch } from 'vue'
import store from '@/store'
import {useRouter} from 'vue-router'
const router=useRouter();
const editableTabsValue = ref(store.state.editableTabsValue)
const editableTabs = ref(store.state.editableTabs)
const addTab = (targetName) => {
const newTabName = `${++tabIndex}`
editableTabs.value.push({
title: 'New Tab',
name: newTabName,
content: 'New Tab content',
})
editableTabsValue.value = newTabName
}
const removeTab = (targetName) => {
const tabs = editableTabs.value
let activeName = editableTabsValue.value
if(activeName==='/index'){
return
}
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name === targetName) {
const nextTab = tabs[index + 1] || tabs[index - 1]
if (nextTab) {
activeName = nextTab.name
}
}
})
}
editableTabsValue.value = activeName
editableTabs.value = tabs.filter((tab) => tab.name !== targetName)
store.state.editableTabsValue=editableTabsValue.value;
store.state.editableTabs=editableTabs.value;
router.push({path:activeName})
}
const clickTab=(target)=>{
console.log("target.props.label="+target.props.label)
router.push({name:target.props.label})
}
const refreshTabs=()=>{
editableTabsValue.value=store.state.editableTabsValue;
editableTabs.value=store.state.editableTabs;
}
watch(store.state,()=>{
refreshTabs();
},{deep:true,immediate:true})
</script>
<style>
.demo-tabs > .el-tabs__content {
padding: 32px;
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{
background-color: lightgray;
}
</style>
动态面包屑实现
breadcrumb.vue
<template>
<el-icon><HomeFilled /></el-icon>
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="(item,index) in breadcrumbList">
<span class="root" v-if="parentName && index>0">{{parentName}} / </span>
<span v-if="index==breadcrumbList.length-1">{{item.name}}</span>
<span class="root" v-else>{{item.name}}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup>
import {HomeFilled} from '@element-plus/icons-vue'
import {useRoute} from 'vue-router'
import { ref ,watch} from 'vue'
import store from "@/store";
const route=useRoute();
const breadcrumbList=ref([])
const parentName=ref("")
const initBreadcrumbList=()=>{
breadcrumbList.value=route.matched;
parentName.value=route.meta.parentName;
}
watch(route,()=>{
initBreadcrumbList();
},{deep:true,immediate:true})
</script>
<style lang="scss" scoped>
.root{
color:#666;
font-weight:600;
}
</style>
显示成功
个人中心功能实现
路由与导航动态绑定实现
App.vue加上下面这个 监控route,动态添加标签
import store from '@/store'
import { ref ,watch} from 'vue'
import { useRoute,useRouter } from 'vue-router'
const route=useRoute();
const router=useRouter();
const whitePath=['/login','/index','/']
watch(route,(to,from)=>{
console.log("to"+to.name)
console.log(to.path)
if (whitePath.indexOf(to.path)===-1) {
console.log("to.path="+to.path)
let obj = {
name: to.name,
path: to.path
}
store.commit("ADD_TABS", obj)
}
},{deep:true,immediate:true})
src/layout/menu/index.vue里修改代码
const activeIndex=ref("/index")
watch(store.state,()=>{
console.log("editableTabsValue="+store.state.editableTabsValue)
activeIndex.value=store.state.editableTabsValue
},{deep:true,immediate:true})
同步
avatar.vue加下router-link
个人中心页面构建实现
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="box-card">
<template v-slot:header>
<div class="clearfix">
<span>个人信息</span>
</div>
</template>
<div>
<div class="text-center">
修改头像
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<svg-icon icon="user" /> 用户名称
<div class="pull-right">{{currentUser.username}}</div>
</li>
<li class="list-group-item">
<svg-icon icon="phone" /> 手机号码
<div class="pull-right">{{currentUser.phonenumber}}
</div>
</li>
<li class="list-group-item">
<svg-icon icon="email" /> 用户邮箱
<div class="pull-right">{{currentUser.email}}</div>
</li>
<li class="list-group-item">
<svg-icon icon="peoples" /> 所属角色
<div class="pull-right">{{currentUser.roles}}</div>
</li>
<li class="list-group-item">
<svg-icon icon="date" /> 创建日期
<div class="pull-right">
{{formatDate(currentUser.loginDate)}}</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<el-col :span="18">
<el-card>
<template v-slot:header>
<div class="clearfix">
<span>基本资料</span>
</div>
</template>
<el-tabs v-model="activeTab">
<el-tab-pane label="基本资料" name="userinfo">
基本资料
</el-tab-pane>
<el-tab-pane label="修改密码" name="resetPwd">
修改密码
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import {ref} from 'vue'
import store from '@/store'
import { formatDate } from '@/util/formatDate.js'
const currentUser = ref(store.getters.GET_USERINFO);
const activeTab = ref("userinfo");
</script>
<style lang="scss" scoped>
.list-group-striped>.list-group-item {
border-left: 0;
border-right: 0;
border-radius: 0;
padding-left: 0;
padding-right: 0;
}
.list-group-item {
border-bottom: 1px solid #e7eaec;
border-top: 1px solid #e7eaec;
margin-bottom: -1px;
padding: 11px 0;
font-size: 13px;
}
.pull-right{
float: right!important;
}
::v-deep .el-card__body{
height:230px;
}
::v-deep .box-card{
height:450px;
}
</style>
个人中心页面数据显示
新建 avatar.vue resetPwd.vue userInfo.vue
SysUser添加role属性
/**
* 所属角色
*/
@TableField(exist = false)
private String roles;
LoginSuccessHandler.java加上
currentUser.setRoles(roleList.stream().map(SysRole::getName).collect(Collectors.
joining(",")));
重新登录
基本资料修改功能实现
userInfo.vue
<template>
<el-form ref="userRef" :model="form" :rules="rules" label-width="100px" >
<el-form-item label="手机号码:" prop="phonenumber">
<el-input v-model="form.phonenumber" maxlength="11" />
</el-form-item>
<el-form-item label="用户邮箱:" prop="email">
<el-input v-model="form.email" maxlength="50" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import {defineProps, ref} from "vue";
import requestUtil from "@/util/request";
import { ElMessage } from 'element-plus'
import store from "@/store";
const props=defineProps(
{
user:{
type:Object,
default:()=>{},
required:true
}
}
)
const form=ref({
id:-1,
phonenumber:'',
email:''
})
const userRef=ref(null)
const rules = ref({
email: [{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
phonenumber: [{ required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
});
form.value=props.user;
const handleSubmit=()=>{
userRef.value.validate(async (valid)=>{
if(valid) {
let result = await requestUtil.post("sys/user/save", form.value);
let data = result.data;
if (data.code == 200) {
ElMessage.success("执行成功!")
store.commit("SET_USERINFO", form.value)
}
}
})
}
</script>
<style lang="scss" scoped>
</style>
父页面传参
新建SysUserController
package com.java.controller;
import com.java.entity.R;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 用户Controller控制器
*/
@RestController
@RequestMapping("/sys/user")
public class SysUserController {
@Autowired
private SysUserService sysUserService;
/**
* 添加或者修改
* @param sysUser
* @return
*/
@PostMapping("/save")
@PreAuthorize("hasAuthority('system:user:add')"+"||"+"hasAuthority('system:user:edit')")
public R save(@RequestBody SysUser sysUser){
if(sysUser.getId()==null || sysUser.getId()==-1){
}else{
sysUser.setUpdateTime(new Date());
sysUserService.updateById(sysUser);
}
return R.ok();
}
}
修改成功
修改密码功能实现
resetPwd.vue
<template>
<el-form ref="pwdRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="form.oldPassword" placeholder="请输入旧密码" type="password" show-password />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" show-password />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" show-password/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import {defineProps, ref} from "vue";
import requestUtil from "@/util/request";
import { ElMessage } from 'element-plus'
import store from "@/store";
const props=defineProps(
{
user:{
type:Object,
default:()=>{},
required:true
}
}
)
const form=ref({
id:-1,
oldPassword:'',
newPassword:'',
confirmPassword:''
})
const pwdRef=ref(null)
form.value=props.user;
const equalToPassword = (rule, value, callback) => {
if (form.value.newPassword !== value) {
callback(new Error("两次输入的密码不一致"));
} else {
callback();
}
};
const rules = ref({
oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],
newPassword: [{ required: true, message: "新密码不能为空", trigger: "blur" }, { min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" }],
confirmPassword: [{ required: true, message: "确认密码不能为空", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }]
});
const handleSubmit=()=>{
pwdRef.value.validate(async (valid)=>{
if(valid) {
let result = await requestUtil.post("sys/user/updateUserPwd", form.value);
let data = result.data;
if (data.code == 200) {
ElMessage.success("密码修改成功,下一次登录生效!")
store.commit("SET_USERINFO", form.value)
}else{
ElMessage.error(data.msg)
}
}
})
}
</script>
<style lang="scss" scoped>
</style>
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
SysUser
/**
* 确认新密码
*/
@TableField(exist = false)
private String newPassword;
/**
* 旧密码(前端传来的)
*/
@TableField(exist = false)
private String oldPassword;
后端SysUserController.java
/**
* 修改密码
* @param sysUser
* @return
*/
@PostMapping("/updateUserPwd")
@PreAuthorize("hasAuthority('system:user:edit')")
public R updateUserPwd(@RequestBody SysUser sysUser){
SysUser currentUser = sysUserService.getById(sysUser.getId());
if(bCryptPasswordEncoder.matches(sysUser.getOldPassword(),currentUser.getPasswo
rd())){
currentUser.setPassword(bCryptPasswordEncoder.encode(sysUser.getNewPassword()))
;
currentUser.setUpdateTime(new Date());
sysUserService.updateById(currentUser);
}else{
return R.error("输入旧密码错误!");
}
return R.ok();
}
头像更换功能实现
avatar.vue
<template>
<el-form
ref="formRef"
:model="form"
label-width="100px"
style="text-align: center;padding-bottom:10px"
>
<el-upload
:headers="headers"
class="avatar-uploader"
:action="getServerUrl()+'sys/user/uploadImage'"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button @click="handleConfirm" >确认更换</el-button>
</el-form>
</template>
<script setup>
import {defineProps, ref} from "vue";
import requestUtil,{getServerUrl} from "@/util/request";
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import store from "@/store";
const props=defineProps(
{
user:{
type:Object,
default:()=>{},
required:true
}
}
)
const headers=ref({
token:store.getters.GET_TOKEN
})
const form=ref({
id:-1,
avatar:''
})
const formRef=ref(null)
const imageUrl=ref("")
form.value=props.user;
imageUrl.value=getServerUrl()+'image/userAvatar/'+form.value.avatar
const handleAvatarSuccess=(res)=>{
imageUrl.value=getServerUrl()+res.data.src
form.value.avatar=res.data.title;
}
const beforeAvatarUpload = (file) => {
const isJPG = file.type === 'image/jpeg'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
ElMessage.error('图片必须是jpg格式')
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过2M!')
}
return isJPG && isLt2M
}
const handleConfirm=async()=>{
let result=await requestUtil.post("sys/user/updateAvatar",form.value);
let data=result.data;
if(data.code==200){
ElMessage.success("执行成功!")
store.commit("SET_USERINFO",form.value)
}else{
ElMessage.error(data.msg);
}
}
</script>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
.avatar {
width: 120px;
height: 120px;
display: block;
}
</style>
avatarImagesFilePath: D://java1234-admin2/userAvatar/
@Value("${avatarImagesFilePath}")
private String avatarImagesFilePath;
/**
* 上传用户头像图片
* @param file
* @return
* @throws Exception
*/
@RequestMapping("/uploadImage")
@PreAuthorize("hasAuthority('system:user:edit')")
public Map<String,Object> uploadImage(MultipartFile file)throws Exception{
Map<String,Object> resultMap=new HashMap<>();
if(!file.isEmpty()){
// 获取文件名
String originalFilename = file.getOriginalFilename();
String suffixName=originalFilename.substring(originalFilename.lastIndexOf("."));
String newFileName= DateUtil.getCurrentDateStr()+suffixName;
System.out.println("上传文件名:"+originalFilename);
System.out.println(" newFileName:"+newFileName);
System.out.println(" avatarImagesFilePath:"+avatarImagesFilePath);
FileUtils.copyInputStreamToFile(file.getInputStream(),new File(avatarImagesFilePath+newFileName));
resultMap.put("code",0);
resultMap.put("msg","上传成功");
Map<String,Object> dataMap=new HashMap<>();
dataMap.put("title",newFileName);
dataMap.put("src","image/userAvatar/"+newFileName);
resultMap.put("data",dataMap);
}
return resultMap;
}
/**
* 修改用户头像
* @param sysUser
* @return
*/
@RequestMapping("/updateAvatar")
@PreAuthorize("hasAuthority('system:user:edit')")
public R updateAvatar(@RequestBody SysUser sysUser){
SysUser currentUser = sysUserService.getById(sysUser.getId());
currentUser.setUpdateTime(new Date());
currentUser.setAvatar(sysUser.getAvatar());
sysUserService.updateById(currentUser);
return R.ok();
}
修改成功