SpringBoot | 使用jwt令牌实现登录认证,使用Md5加密实现注册
对于登录认证中的令牌,其实就是一段字符串,那为什么要那么麻烦去用jwt令牌?其实对于登录这个业务,在平常我们实现这个功能时,可能大部分都是通过比对用户名和密码,只要正确,就登录成功;而并不会考虑到使用jwt令牌。
但是其实使用jwt令牌的好处是:
- 它可以承载业务数据,减少后续请求查询数据库的次数;
- 并且可以防篡改,保证信息的合法性和有效性。
前提:
pom.xml文件添加以下依赖:
<!-- java-jwt坐标--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.4.0</version></dependency><!-- 单元测试的坐标--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>compile</scope></dependency>
测试代码:
package com.xu;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.jupiter.api.Test;import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class JwtTest {@Testpublic void testGen(){Map<String,Object> claims=new HashMap<>();claims.put("id",1);claims.put("username","张三");//生成jwt的代码String token= JWT.create().withClaim("user",claims)//添加载荷.withExpiresAt(new Date(System.currentTimeMillis()+1000*60*60*3)).sign(Algorithm.HMAC256("xu"));//指定算法,配置密钥System.out.println(token);}@Testpublic void testParse(){//定义字符串,模拟用户传递过来的tokenString token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MTk0ODgxMjR9.RtU7DxCKObLO2MyVlh0g7GbwxCBI2H23y9kn0kt34Lc";JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256("xu")).build();DecodedJWT decodedJWT= jwtVerifier.verify(token);//验证token,生成一个解析后的jwt对象Map<String, Claim> claims=decodedJWT.getClaims();System.out.println(claims.get("user"));}}
jwt令牌的生成:

验证jwt令牌:

使用jwt令牌实现登录功能,并且进入需要登录为前提的页面进行判断
项目框架目录:

pojo层:
package com.xu.pojo;
import lombok.Data;import java.time.LocalDateTime;
//lombok 在编译阶段,为实体类自动生成setter getter toString
// pom文件中引入依赖 在实体类上添加注解
@Data
public class User {private Integer id;//主键IDprivate String username;//用户名private String password;//密码private String nickname;//昵称private String email;//邮箱private String userPic;//用户头像地址private LocalDateTime createTime;//创建时间private LocalDateTime updateTime;//更新时间
}
package com.xu.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;//统一响应结果@NoArgsConstructor
@AllArgsConstructor
@Data
public class Result<T> {private Integer code;//业务状态码 0-成功 1-失败private String message;//提示信息private T data;//响应数据//快速返回操作成功响应结果(带响应数据)public static <E> Result<E> success(E data) {return new Result<>(0, "操作成功", data);}//快速返回操作成功响应结果public static Result success() {return new Result(0, "操作成功", null);}public static Result error(String message) {return new Result(1, message, null);}
}
service层:
package com.xu.service;import com.xu.pojo.User;
import org.springframework.stereotype.Service;@Service
public interface UserService {//根据用户名查询用户User findByUserName(String username);//注册void register(String username, String password);
}
package com.xu.service.impl;import com.xu.mapper.UserMapper;
import com.xu.pojo.User;
import com.xu.service.UserService;
import com.xu.utils.Md5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic User findByUserName(String username) {User u=userMapper.findByUserName(username);return u;}@Overridepublic void register(String username, String password) {//加密String md5String= Md5Util.getMD5String(password);//添加userMapper.add(username,md5String);}
}
mapper层:
package com.xu.mapper;import com.xu.pojo.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper {//根据用户名查询用户@Select("select * from user where username=#{username}")User findByUserName(String username);//添加@Insert("insert into user(username,password,create_time,update_time)"+" values(#{username},#{password},now(),now())")void add(String username, String password);
}
utils层:
实现登录的密码加密功能:
package com.xu.utils;import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;public class Md5Util {/*** 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合*/protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};protected static MessageDigest messagedigest = null;static {try {messagedigest = MessageDigest.getInstance("MD5");} catch (NoSuchAlgorithmException nsaex) {System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");nsaex.printStackTrace();}}/*** 生成字符串的md5校验值** @param s* @return*/public static String getMD5String(String s) {return getMD5String(s.getBytes());}/*** 判断字符串的md5校验码是否与一个已知的md5码相匹配** @param password 要校验的字符串* @param md5PwdStr 已知的md5校验码* @return*/public static boolean checkPassword(String password, String md5PwdStr) {String s = getMD5String(password);return s.equals(md5PwdStr);}public static String getMD5String(byte[] bytes) {messagedigest.update(bytes);return bufferToHex(messagedigest.digest());}private static String bufferToHex(byte bytes[]) {return bufferToHex(bytes, 0, bytes.length);}private static String bufferToHex(byte bytes[], int m, int n) {StringBuffer stringbuffer = new StringBuffer(2 * n);int k = m + n;for (int l = m; l < k; l++) {appendHexPair(bytes[l], stringbuffer);}return stringbuffer.toString();}private static void appendHexPair(byte bt, StringBuffer stringbuffer) {char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>// 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换stringbuffer.append(c0);stringbuffer.append(c1);}}
实现注册的jwt令牌功能:
package com.xu.utils;import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;import java.util.Date;
import java.util.Map;public class JwtUtil {private static final String KEY = "xu";//接收业务数据,生成token并返回public static String genToken(Map<String, Object> claims) {return JWT.create().withClaim("claims", claims).withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)).sign(Algorithm.HMAC256(KEY));}//接收token,验证token,并返回业务数据public static Map<String, Object> parseToken(String token) {return JWT.require(Algorithm.HMAC256(KEY)).build().verify(token).getClaim("claims").asMap();}}
controller层:
package com.xu.controller;import com.xu.pojo.Result;
import com.xu.pojo.User;
import com.xu.service.UserService;
import com.xu.utils.JwtUtil;
import com.xu.utils.Md5Util;
import jakarta.validation.constraints.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/user")
@Validated
public class UserController {@Autowiredprivate UserService userService;@PostMapping("register")public Result register(@Pattern(regexp = "^\\S{5,16}$")String username, @Pattern(regexp = "^\\S{5,16}$")String password){//查询用户User u=userService.findByUserName(username);if(u==null){//没有占用,注册userService.register(username,password);return Result.success();}else {//占用return Result.error("用户名已被占用");}}@PostMapping("login")public Result<String> login(@Pattern(regexp = "^\\S{5,16}$")String username, @Pattern(regexp = "^\\S{5,16}$")String password){//根据用户名查询用户User loginUser=userService.findByUserName(username);//判断用户是否存在if(loginUser==null){return Result.error("用户名错误");}//判断密码是否正确if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){//登录成功Map<String,Object> claims=new HashMap<>();claims.put("id",loginUser.getId());claims.put("username",loginUser.getUsername());String token= JwtUtil.genToken(claims);return Result.success(token);}return Result.error("密码错误");}
}
使用localhost:8080/article/list测试登录中jwt令牌的功能:
package com.xu.controller;import com.xu.pojo.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/article")
public class ArticleController {@GetMapping("/list")public Result<String> list(){return Result.success("所有文章数据");}
}
interceptors层(令牌验证):
package com.xu.interceptors;import com.xu.pojo.Result;
import com.xu.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import java.util.Map;@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//令牌验证String token=request.getHeader("Authorization");//验证tokentry {Map<String,Object> claims= JwtUtil.parseToken(token);//放行return true;}catch (Exception e){response.setStatus(401);//不放行return false;}}
}
exception层:
package com.xu.exception;import com.xu.pojo.Result;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public Result handleException(Exception e){e.printStackTrace();return Result.error(StringUtils.hasLength(e.getMessage())?e.getMessage():"操作失败");}
}
config层(没有header的“令牌”,不拦截登录,注册页面):
package com.xu.config;import com.xu.interceptors.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//登录接口和注册接口不拦截registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");}
}
pom.xml文件:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><artifactId>spring-boot-starter-parent</artifactId><groupId>org.springframework.boot</groupId><version>3.1.3</version></parent><groupId>com.xu</groupId><artifactId>big-news</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><name>big-news</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies>
<!-- web依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
<!-- <version>3.3.0</version>--></dependency><!-- mybatis依赖--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.0</version></dependency>
<!-- mysql驱动依赖--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId>
<!-- <version></version>--></dependency><!-- lombok依赖--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- validation依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- java-jwt坐标--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.4.0</version></dependency><!-- 单元测试的坐标--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>compile</scope></dependency></dependencies>
</project>
分析:
- 用户登录:
用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,浏览器都需要在请求头header中携带到服务端,请求头的名称为 Authorization,值为true 登录时下发的JWT令牌。
- 用户注册:
用户注册使用Md5加密,防止数据库的密码被泄露,并且在用户注册时使用validation校验参数。
测试结果:
使用Postman测试:
登录功能:

登录成功后,获取jwt令牌,进入localhost:8080/article/list:

修改Authorization里面的字符串,没有获取jwt令牌:

注册功能:
注册成功(输入内容字符串在5~16个里面):


注册失败(输入用户名或者密码,字符串不在5~16个里面):




相关文章:
SpringBoot | 使用jwt令牌实现登录认证,使用Md5加密实现注册
对于登录认证中的令牌,其实就是一段字符串,那为什么要那么麻烦去用jwt令牌?其实对于登录这个业务,在平常我们实现这个功能时,可能大部分都是通过比对用户名和密码,只要正确,就登录成功ÿ…...
Springboot基于Redis的高性能分布式缓存数据库的实现与实例
一、引言 在现代的分布式系统和高并发应用中,缓存机制显得尤为重要。Redis作为一种开源(BSD许可)的内存键值存储,因其高性能、丰富的数据结构和多样化的应用场景,成为开发者们的首选。在这篇博客中,我们将…...
防止多次点击,vue的按钮上做简易的防抖节流处理
话不多说,上个视频,看看是不是你要的效果 防抖节流 1.创建一个directive.js // directive.js export default {install(Vue) {// 防重复点击(指令实现)Vue.directive(repeatClick, {inserted(el, binding) {el.addEventListener(click, () > {if (!el.disabled) {el.disabl…...
云计算【第一阶段(21)】Linux引导过程与服务控制
目录 一、linux操作系统引导过程 1.1、开机自检 1.2、MBR引导 1.3、GRUB菜单 1.4、加载 Linux 内核 1.5、init进程初始化 1.6、简述总结 1.7、初始化进程centos 6和7的区别 二、排除启动类故障 2.1、修复MBR扇区故障 2.1.1、 实验 2.2、修复grub引导故障 2.2.1、实…...
Google 发布最新开放大语言模型 Gemma 2,现已登陆 Hugging Face Hub
Google 发布了最新的开放大语言模型 Gemma 2,我们非常高兴与 Google 合作,确保其在 Hugging Face 生态系统中的最佳集成。你可以在 Hub 上找到 4 个开源模型 (2 个基础模型和 2 个微调模型) 。发布的功能和集成包括: Hub 上的模型https://hf.…...
智能分析赋能等保:大数据技术在安全审计记录中的应用
随着信息技术的飞速发展,大数据技术在各行各业中的应用愈发广泛,特别是在网络安全领域,大数据技术为安全审计记录提供了强有力的支撑。本文将深入探讨智能分析如何赋能等保(等级保护),以及大数据技术在安全…...
Django中,update_or_create()
在Django中,可以使用update_or_create()方法来更新现有记录或创建新记录。该方法接受一个字典作为参数,用于指定要更新或创建的字段和对应的值。 update_or_create()方法的语法如下: 代码语言:python obj, created Model.obje…...
每日一学(1)
目录 1、ConCurrentHashMap为什么不允许key为null? 2、ThreadLocal会出现内存泄露吗? 3、AQS理解 4、lock 和 synchronized的区别 1、ConCurrentHashMap为什么不允许key为null? 底层 putVal方法 中 如果key || value为空 抛出…...
SpringMVC(1)——入门程序+流程分析
MVC都是哪三层?在Spring里面分别对应什么?SpringMVC的架构是什么? 我们使用Spring开发JavaWeb项目,一般都是BS架构,也就是Browser(浏览器)-Server(服务器)架构 这种架构…...
成绩发布背后:老师的无奈与痛点
在教育的广阔天地里,教师这一角色承载着无数的期望与责任。他们不仅是知识的传播者,更是学生心灵的引路人。而对于班主任老师来说,他们的角色更加多元,他们不仅是老师,还必须是“妈妈”。除了像其他老师一样备课、上课…...
MySQL 索引之外的相关查询优化总结
在这之前先说明几个概念: 1、驱动表和被驱动表:驱动表是主表,被驱动表是从表、非驱动表。驱动表和被驱动表并非根据 from 后面表名的先后顺序而确定,而是根据 explain 语句查询得到的顺序确定;展示在前面的是驱动表&am…...
EE trade:贵金属投资的优点及缺点
贵金属(如黄金、白银、铂金和钯金)一直以来都是重要的投资和避险工具。它们具有独特的物理和化学特性,广泛应用于各种行业,同时也被视为财富储备。在进行贵金属投资时,了解其优点和缺点对于做出明智的投资决策至关重要。 一、贵金属投资的优…...
python工作目录与文件目录
工作目录 文件目录:文件所在的目录 工作目录:执行python命令所在的目录 D:. | main.py | ---data | data.txt | ---model | | model.py | | train.py | | __init__.py | | | ---nlp | | | bert.py | …...
可信和可解释的大语言模型推理-RoG
大型语言模型(LLM)在复杂任务中表现出令人印象深刻的推理能力。然而,LLM在推理过程中缺乏最新的知识和经验,这可能导致不正确的推理过程,降低他们的表现和可信度。知识图谱(Knowledge graphs, KGs)以结构化的形式存储了…...
秋招季的策略与行动指南:提前布局,高效备战,精准出击
6月即将进入尾声,一年一度的秋季招聘季正在热火进行中。对于即将毕业的学生和寻求职业发展的职场人士来说,秋招是一个不容错过的黄金时期。 秋招的序幕通常在6月至9月间拉开,名企们纷纷开启网申的大门。在此期间,求职备战是一个系…...
Java并发编程-wait与notify详解及案例实战
文章目录 概述wait()notify()作用注意事项用wait与notify手写一个内存队列wait与notify的底层原理:monitor以及wait_setMonitor(监视器)Wait Set(等待集合)Wait() 原理Notify() / NotifyAll() 原理注意事项wait与notify在代码中使用时的注意事项总结案例实战:基于wait与not…...
204.贪心算法:分发饼干(力扣)
以下来源于代码随想录 class Solution { public:int findContentChildren(vector<int>& g, vector<int>& s) {// 对孩子的胃口进行排序sort(g.begin(), g.end());// 对饼干的尺寸进行排序sort(s.begin(), s.end());int index s.size() - 1; // 从最大的饼…...
AI奥林匹克竞赛:Claude-3.5-Sonnet对决GPT-4o,谁是最聪明的AI?
目录 实验设置 评估对象 评估方法 结果与分析 针对学科的细粒度分析 GPT-4o vs. Claude-3.5-Sonnet GPT-4V vs. Gemini-1.5-Pro 结论 AI技术日新月异,Anthropic公司最新发布的Claude-3.5-Sonnet因在知识型推理、数学推理、编程任务及视觉推理等任务上设立新…...
【C++】const修饰成员函数
const修饰成员函数 常函数: 成员函数后加const后我们称为这个函数为常函数 常函数内不可以修改成员属性 成员属性声明时加关键字mutable后,在常函数中依然可以修改 class Animal { public:void fun1(){//这是一个普通的成员函数 }void fun2…...
基于模糊神经网络的时间序列预测(以hopkinsirandeath数据集为例,MATLAB)
模糊神经网络从提出发展到今天,主要有三种形式:算术神经网络、逻辑模糊神经网络和混合模糊神经网络。算术神经网络是最基本的,它主要是对输入量进行模糊化,且网络结构中的权重也是模糊权重;逻辑模糊神经网络的主要特点是模糊权值可…...
InfiniTime智能手表固件完全指南:从零开始打造你的开源智能手表
InfiniTime智能手表固件完全指南:从零开始打造你的开源智能手表 【免费下载链接】InfiniTime Firmware for Pinetime smartwatch written in C and based on FreeRTOS 项目地址: https://gitcode.com/gh_mirrors/in/InfiniTime InfiniTime是一款为Pinetime智…...
【苍穹外卖 | 篇⑥】登录流程
在牛某网看见了牛肉哥的帖子之后,打算向牛肉大佬学习,故开始书写CSDN博客,通过博客的方式来巩固自身知识学习。因为之前有粗略的学习了Java Web 的基础课程,所以博客内容主要是巩固之前学习当中的模糊点,以及一些自己认…...
【多模态实战】Swift框架高效微调Qwen2-VL:从SFT到RLHF的完整指南
1. 为什么选择Swift框架微调Qwen2-VL 第一次接触Qwen2-VL这个多模态大模型时,我被它强大的图文理解能力惊艳到了。但真正让我惊喜的是发现Swift框架能让模型微调变得如此简单。记得当时为了测试一个定制化需求,传统方法需要写上百行训练代码,…...
Ubuntu 20.04 虚拟机环境快速克隆与迁移实战指南
1. 为什么需要虚拟机环境克隆与迁移? 作为常年和虚拟机打交道的开发者,我深刻理解重复搭建环境的痛苦。每次新项目启动都要从头配置Ubuntu环境,安装依赖库,调试网络,这个过程至少要浪费半天时间。更可怕的是当团队需要…...
QChart实战:从零构建动态数据波形图(含完整代码与注释)
1. 环境准备与基础配置 在开始构建动态波形图之前,我们需要先搭建好开发环境。这里假设你已经安装了Qt Creator,我推荐使用5.15或更高版本,因为这个版本对QChart的支持最完善。如果你还没安装,可以直接去Qt官网下载开源版本。 首…...
别再让输入框‘抢焦点’了!手把手封装一个Vue扫码枪工具类SCAN,解决页面刷新监听丢失
从零构建高可靠Vue扫码枪工具库:SCAN类深度封装与工程化实践 扫码枪在零售、仓储、医疗等行业的Web系统中应用广泛,但传统实现强依赖输入框焦点,用户体验差且稳定性低。本文将带你从底层原理出发,完整封装一个无需输入框聚焦、支持…...
推荐系统优化秘籍:如何用Metric Learning解决冷启动问题?
推荐系统优化秘籍:如何用Metric Learning解决冷启动问题? 在推荐系统领域,冷启动问题一直是困扰算法工程师和产品经理的核心挑战之一。新用户缺乏历史行为数据,新商品没有足够的曝光记录,传统协同过滤方法在这些场景下…...
Spring Authorization Server设备授权深度实践:从协议解析到企业落地
Spring Authorization Server设备授权深度实践:从协议解析到企业落地 【免费下载链接】spring-authorization-server Spring Authorization Server 项目地址: https://gitcode.com/gh_mirrors/sp/spring-authorization-server 一、概念解析:为什么…...
Vivado进阶指南:网表物理约束实战解析
1. 网表物理约束的核心价值 第一次接触Vivado的网表物理约束时,我和很多初学者一样困惑:明明RTL代码已经定义了所有功能,为什么还要多此一举?直到在某次高速ADC数据采集项目中,系统频繁出现时序违例,我才真…...
Photoshop AI绘画革命:3分钟学会Auto-Photoshop-StableDiffusion-Plugin终极指南
Photoshop AI绘画革命:3分钟学会Auto-Photoshop-StableDiffusion-Plugin终极指南 【免费下载链接】Auto-Photoshop-StableDiffusion-Plugin A user-friendly plug-in that makes it easy to generate stable diffusion images inside Photoshop using either Automa…...
