认证学习3 - Digest摘要认证讲解、代码实现、演示

article/2025/11/8 15:57:15

文章目录

      • Digest摘要认证 - 密文
        • 讲解(Digest摘要认证)
        • 实现(Digest认证)
          • 代码(Digest认证)
            • 代码(Digest认证-客户端)
            • 演示(Digest认证-postman)


认证大全(想学习其他认证,请到此链接查看)

Digest摘要认证 - 密文

背景: 仅仅只是用来替代Basic认证,由于Basic认证使用明文账户、密码传输这得确保客户端、服务端传输之间不会被任何人截取报文,这就要求很高,我们都知道网络传输没有绝对的安全,为了不使得明文传输,RFC提出另一种认证方式即Digest认证,认证时仅传账户、密码的摘要,这就避免有人即是劫持报文获得认证信息也不会解析得到用户的账户、密码

在这里插入图片描述

讲解(Digest摘要认证)

参考文章: https://www.cnblogs.com/huey/p/5490759.html

官方规范(新-兼容老规范-必须看): https://datatracker.ietf.org/doc/html/rfc7616
官方规范(老规范-必须看): https://datatracker.ietf.org/doc/html/rfc2617

请求头字段规范: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Digest

好文1(强烈建议看-可以不用看好文2的实现看我的实现也可以): https://www.cnblogs.com/xiaoxiaotank/p/11078571.html
好文2(强烈建议看): https://www.cnblogs.com/xiaoxiaotank/p/11079024.html

优点
用户、密码非明文传输
防止对报文内容的篡改
防止重放攻击
响应码
400:认证失败,认证失败,认证必要的参数缺失
401:认证失败,客户端传过来的摘要跟服务器计算的不一致,需要用户重新输入用户名、密码再次进行认证

在这里插入图片描述

在这里插入图片描述

//响应头:WWW-Authenticate  或者 请求头:Authorization
Digest username="用户名", realm="角色描述", nonce="服务端随机数(Base64或十六进制编码)", uri="请求uri部分", response="摘要(客户端计算、到时服务端也会计算一份看是否一致)", algorithm="摘要算法", cnonce="客户端随机数(Base64或十六进制编码)", opaque="服务端给的信息原封不动换回去即可", nc="使用次数", qop="auth|auth-init"//响应头WWW-Authenticate 必须提供
opaque、algorithm//响应头WWW-Authenticate 提供
qop,则请求头Authorization将这个参数原封不动的还回去//摘要公式  secret=密钥key  data=需要摘要的数据   两个参数都是根据规则由WWW-Authenticate里面的参数进行构建
//由于摘要算法无需密钥即可生成,故充分利用将secret也作为被摘要的部分数据
KD(secret, data)=摘要算法(concat(secret, ":", data))   
secret(也称A1)=摘要算法(摘要算法(<username>:<realm>:<password>):<nonce>:<cnonce>)
secret(也称A1-sess形式)=摘要算法(<username>:<realm>:<password>)
data=<nonce>:<nc>:<cnonce>:<qop>:摘要算法(A2)
A2=<request-method>:<uri>
原英文参数(请求头携带的参数名)作用
(响应头-认证失败)WWW-Authentication用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源、以及各种其他参数添加
digest-uriuri当前请求的uri
username即将认证的用户名
realm表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
message-qopqop保护质量,包含 auth(默认的)和 auth-int(增加了报文完整性检测)以及 Token值 三种策略,(可以为空,但是)不推荐为空值
noncenonce服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击 = 官方建议每次请求都不同
userhashuserhash(选填)是否支持用户名使用hash,默认false
opaque(选填)服务端给的数据一般是Base64或十六进制编码,客户端请求认证时原封不动的返回给服务端
(请求头-开始认证)Authorization认证类型以及用于携带客户端认证信息
nonce-countncnonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求
cnonce客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
request-digestresponse这是由用户代理软件计算出的一个字符串,以证明用户知道口令 == 认证凭证(服务端校验)== 公式:MD5(MD5(A1):::::MD5(A2))
(响应头-认证成功) Authorization-Info用于返回一些与授权会话相关的附加信息
nextnonce下一个服务端随机数,使客户端可以预先发送正确的摘要
rspauth响应摘要,用于客户端对服务端进行认证 == 认证凭证(客户端校验) == 公式:MD5(MD5(A1):::::MD5(A2))
stale当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了
algorithm摘要算法,由服务端(响应头)进行指定,默认MD5,支持MD5、MD5-sess、SHA-256、SHA-256-sess、SHA-512-256、SHA-512-256-sess、token值
//认证凭证生成算法=即参数response、rspauth
MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))

客户端生成用户凭证的参数内容

算法A1
MD5(默认)MD5(::)
MD5-sessMD5(::)::
qopA2
auth(默认):
auth-int::MD5()

服务端生成用户凭证的参数内容 == 无请求方式-参数变化即是少了请求方式而已

qopA2
auth(默认):
auth-int::MD5()

实现(Digest认证)

代码(Digest认证)
代码(Digest认证-客户端)

在这里插入图片描述


需要额外引入这两个依赖

<!-- 主要用于该工具类线程写好的 sha-512/256摘要算法 ->
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.70</version>
</dependency><!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.5</version>
</dependency>   


DigestAuthInterceptor.java

package work.linruchang.qq.mybaitsplusjoin.config.interceptor;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.CharPool;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import work.linruchang.qq.mybaitsplusjoin.config.interceptor.algorithm.digest.DigestAlgorithmUtil;
import work.linruchang.qq.mybaitsplusjoin.config.interceptor.algorithm.digest.WWWAuthenticate;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;/*** 作用:Basic认证** @author LinRuChang* @version 1.0* @date 2022/08/04* @since 1.8**/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DigestAuthInterceptor implements HandlerInterceptor {@AllArgsConstructor@Getterenum AuthErrorEnum {INVALID_REQUEST(400, "invalid_request", "参数异常"),INVALID_DIGEST(401, "invalid_digest", "摘要不一致");Integer errorCode;String error;String errorDescription;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {boolean authStatusFlag = false;String authorization = request.getHeader("Authorization");AuthErrorEnum authErrorEnum = AuthErrorEnum.INVALID_DIGEST;if (StrUtil.isNotBlank(authorization)) {String[] splitInfos = authorization.split(StrUtil.COMMA + StrUtil.SPACE);splitInfos[0] = StrUtil.removePrefix(splitInfos[0], "Digest ");//解析认证参数Map<String, String> splitInfosMap = Stream.of(splitInfos).map(StrUtil::trim).map(splitInfo -> StrUtil.removeAll(splitInfo, CharPool.DOUBLE_QUOTES)).collect(Collectors.toMap(splitInfo -> {return splitInfo.split("=")[0];}, splitInfo -> {return splitInfo.split("=")[1];}));WWWAuthenticate wwwAuthenticate = BeanUtil.toBean(splitInfosMap, WWWAuthenticate.class);Dict userInfo = getUserInfo(wwwAuthenticate.getUsername());if (userInfo != null) {wwwAuthenticate.setPassword(userInfo.getStr("password"));wwwAuthenticate.setRequestMethod(request.getMethod());String serverResponse = DigestAlgorithmUtil.result(wwwAuthenticate);if (StrUtil.equals(serverResponse, wwwAuthenticate.getResponse())) {  //摘要一致authStatusFlag = true;}} else {authErrorEnum = AuthErrorEnum.INVALID_REQUEST;}}//认证失败if (!authStatusFlag) {response.setHeader("WWW-Authenticate", StrUtil.format("Digest realm=\"rights of administrators\", nonce=\"{}\", qop=\"auth\"", UUID.randomUUID().toString(true)));response.setHeader("content-type", "text/plain;charset=utf-8");response.setStatus(authErrorEnum.getErrorCode());response.getWriter().write("认证失败");return false;}return authStatusFlag;}public Dict getUserInfo(String userName) {if (StrUtil.equals(userName, "admin")) {return Dict.create().set("userName", "admin").set("password", "admin123");}return null;}
}


DigestAuthInterceptor.java

@Configuration
public class MyConfig implements WebMvcConfigurer {@AutowiredDigestAuthInterceptor digestAuthInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(digestAuthInterceptor).addPathPatterns("/**").excludePathPatterns("/js/**","/user*/**","/user*/**");}
}


DigestAlgorithm.java

public interface DigestAlgorithm {/*** 计算摘要结果* 摘要算法(摘要算法(A1):<nonce>:<nc>:<cnonce>:<qop>:摘要算法(A2))*  A1(sess形式):  摘要算法(<username>:<realm>:<password>):<nonce>:<cnonce>*  A1(非sess形式):  <username>:<realm>:<password>**  A2:<request-method>:<uri>* @param wwwAuthenticate* @return*/String result(WWWAuthenticate wwwAuthenticate);}


MD5DigestAlgorithm.java

 **/
public class MD5DigestAlgorithm implements DigestAlgorithm{@Overridepublic String result(WWWAuthenticate wwwAuthenticate) {String A1Digest = null;String A2Digest = null;//MD5-sessif(StrUtil.containsIgnoreCase(wwwAuthenticate.getAlgorithm(),"sess")) {A1Digest = SecureUtil.md5(StrUtil.format("{}:{}:{}", SecureUtil.md5(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword())), wwwAuthenticate.getNonce(), wwwAuthenticate.getCnonce()));A2Digest = SecureUtil.md5(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));}else { //MD5A1Digest = SecureUtil.md5(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword()));A2Digest = SecureUtil.md5(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));}String serverResponse = SecureUtil.md5(StrUtil.format("{}:{}:{}:{}:{}:{}", A1Digest, wwwAuthenticate.getNonce(), wwwAuthenticate.getNc(), wwwAuthenticate.getCnonce(), wwwAuthenticate.getQop(), A2Digest));return serverResponse;}
}


SHA256DigestAlgorithm.java

public class SHA256DigestAlgorithm implements DigestAlgorithm{@Overridepublic String result(WWWAuthenticate wwwAuthenticate) {String A1Digest = null;String A2Digest = null;//SHA256-sessif(StrUtil.containsIgnoreCase(wwwAuthenticate.getAlgorithm(),"sess")) {A1Digest = SecureUtil.sha256(StrUtil.format("{}:{}:{}", SecureUtil.md5(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword())), wwwAuthenticate.getNonce(), wwwAuthenticate.getCnonce()));A2Digest = SecureUtil.sha256(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));}else { //SHA256A1Digest = SecureUtil.sha256(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword()));A2Digest = SecureUtil.sha256(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));}String serverResponse = SecureUtil.sha256(StrUtil.format("{}:{}:{}:{}:{}:{}", A1Digest, wwwAuthenticate.getNonce(), wwwAuthenticate.getNc(), wwwAuthenticate.getCnonce(), wwwAuthenticate.getQop(), A2Digest));return serverResponse;}
}


SHA512256DigestAlgorithm.java

public class SHA512256DigestAlgorithm implements DigestAlgorithm{@Overridepublic String result(WWWAuthenticate wwwAuthenticate) {String A1Digest = null;String A2Digest = null;Digester digester = DigestUtil.digester("sha-512/256");//SHA-512/256-sessif(StrUtil.containsIgnoreCase(wwwAuthenticate.getAlgorithm(),"sess")) {A1Digest = digester.digestHex(StrUtil.format("{}:{}:{}", digester.digestHex(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword())), wwwAuthenticate.getNonce(), wwwAuthenticate.getCnonce()));A2Digest = digester.digestHex(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));}else { //SHA-512/256A1Digest = digester.digestHex(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword()));A2Digest = digester.digestHex(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));}String serverResponse =digester.digestHex(StrUtil.format("{}:{}:{}:{}:{}:{}", A1Digest, wwwAuthenticate.getNonce(), wwwAuthenticate.getNc(), wwwAuthenticate.getCnonce(), wwwAuthenticate.getQop(), A2Digest));return serverResponse;}
}


DigestAlgorithmUtil.java

public class DigestAlgorithmUtil {private final static ConcurrentHashMap<String, DigestAlgorithm> digestAlgorithmMap = new ConcurrentHashMap<>();/*** 计算摘要结果** @param wwwAuthenticate* @return*/@SneakyThrowspublic static String result(WWWAuthenticate wwwAuthenticate) {String packageName = ClassUtil.getPackage(DigestAlgorithmUtil.class);String algorithm = StrUtil.removeAll(StrUtil.split(wwwAuthenticate.getAlgorithm(), "-sess").get(0), StrUtil.DASHED);DigestAlgorithm digestAlgorithm = Optional.ofNullable(digestAlgorithmMap.get(algorithm)).orElseGet(() -> {DigestAlgorithm currentDigestAlgorithm = null;try {Class<?> digestAlgorithmClass = Class.forName(StrUtil.format("{}.{}DigestAlgorithm", packageName, algorithm));currentDigestAlgorithm = (DigestAlgorithm) ReflectUtil.newInstance(digestAlgorithmClass);digestAlgorithmMap.put(algorithm, currentDigestAlgorithm);} catch (ClassNotFoundException e) {}return currentDigestAlgorithm;});Assert.notNull(digestAlgorithm, "【{}】摘要算法找不到,请检查", algorithm);return digestAlgorithm.result(wwwAuthenticate);}@SneakyThrowspublic static void main(String[] args) {Class<?> md5DigestAlgorithm = Class.forName("MD5DigestAlgorithm");Console.log(md5DigestAlgorithm);}}


WWWAuthenticate.java

@Data
public class WWWAuthenticate {String qop;String nc;String realm;String cnonce;String uri;String nonce;String algorithm;String username;String response;/*** 用于计算摘要*/String password;String requestMethod;
}


ArticleCategoryController.java

@RestController
@RequestMapping("article-category")
public class ArticleCategoryController {@GetMapping("/one/{id}")public CommonHttpResult<ArticleCategory> findById(@PathVariable("id") String id) {return CommonHttpResult.success(articleCategoryService.getById(id));}
}
演示(Digest认证-postman)

理由: 为什么使用postman,因为只要输入账号、密码,发现第一个请求认证失败,返回的nonce等信息,postman自动帮你计算好摘要,然后重新发送,如果再次认证认证失败,则不会在帮你重新发送,所以postman最多帮你发送两遍。

在这里插入图片描述

发送错误的用户名或密码
在这里插入图片描述

在这里插入图片描述



发送正确的用户名或密码

在这里插入图片描述

在这里插入图片描述


http://chatgpt.dhexx.cn/article/p8DSbPZD.shtml

相关文章

HTTP的认证方式之DIGEST 认证(摘要认证)

核心步骤&#xff1a; 步骤 1&#xff1a; 请求需认证的资源时&#xff0c;服务器会随着状态码 401Authorization Required&#xff0c;返回带WWW-Authenticate 首部字段的响应。该字段内包含质问响应方式认证所需的临时质询码&#xff08;随机数&#xff0c;nonce&#xff09;…

Digest Auth 摘要认证

Digest Auth 摘要认证 1.非常规方式 转载&#xff1a;https://blog.csdn.net/qq_25391785/article/details/86595529 public static void postMethod(String url, String query) {try {CredentialsProvider credsProvider new BasicCredentialsProvider();credsProvider.setC…

digest鉴权

“摘要”式认证&#xff08; Digest authentication&#xff09;是一个简单的认证机制&#xff0c;最初是为HTTP协议开发的&#xff0c;因而也常叫做HTTP摘要&#xff0c;在RFC2671中描述。其身份验证机制很简单&#xff0c;它采用杂凑式&#xff08;hash&#xff09;加密方法&…

消息摘要(Digest),数字签名(Signature),数字证书(Certificate)是什么?

1. 消息摘要&#xff08;Digest&#xff09; 1. 什么是消息摘要&#xff1f; 对一份数据&#xff0c;进行一个单向的 Hash 函数&#xff0c;生成一个固定长度的 Hash 值&#xff0c;这个值就是这份数据的摘要&#xff0c;也称为指纹。 2. 摘要算法 常见的摘要算法有 MD5、SHA…

HTTP通讯安全中的Digest摘要认证释义与实现

摘要 出于安全考虑&#xff0c;HTTP规范定义了几种认证方式以对访问者身份进行鉴权&#xff0c;最常见的认证方式之一是Digest认证 Digest认证简介 HTTP通讯采用人类可阅读的文本格式进行数据通讯&#xff0c;其内容非常容易被解读。出于安全考虑&#xff0c;HTTP规范定义了几…

http协议之digest(摘要)认证,详细讲解并附Java SpringBoot源码

目录 1.digest认证是什么&#xff1f; 2.digest认证过程 3.digest认证参数详解 4.基于SpringBoot实现digest认证 5.digest认证演示 6.digest认证完整项目 7.参考博客 1.digest认证是什么&#xff1f; HTTP通讯采用人类可阅读的文本格式进行数据通讯&#xff0c;其内容非…

【WinRAR】WinRAR 6.01 官方最新简体中文版

WinRAR 6.01 官方简体中文商业版下载地址&#xff08;需要注册&#xff09;&#xff1a; 64位&#xff1a; https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20210414/wrr/winrar-x64-601sc.exe https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20210414/…

WinRAR命令行

基本使用 实践 将文件夹压缩到zip包 输入&#xff1a;文件夹如下&#xff0c;文件夹为class。 输出&#xff1a;classes.zip 指令如下&#xff1a; rar a classes.zip .\classes或者 WinRAR a classes.zip .\classes结果如下&#xff1a; PS C:\Users\liyd\Desktop\kuai…

WinRAR安装教程

文章目录 WinRAR安装教程无广告1. 下载2. 安装3. 注册4. 去广告 WinRAR安装教程无广告 1. 下载 国内官网&#xff1a;https://www.winrar.com.cn/ 2. 安装 双击&#xff0c;使用默认路径&#xff1a; 点击“安装”。 点击“确定”。 点击“完成”。 3. 注册 链接&#x…

WinRAR注册+去广告教程

1、注册 在WinRAR安装目录创建rarreg.key文件&#xff0c; 拷贝如下内容并保存&#xff1a; RAR registration data Federal Agency for Education 1000000 PC usage license UIDb621cca9a84bc5deffbf 6412612250ffbf533df6db2dfe8ccc3aae5362c06d54762105357d 5e3b1489e751c…

WinRAR4.20注册文件rarreg.key

2019独角兽企业重金招聘Python工程师标准>>> 在WinRAR的安装目录下&#xff0c;新建rarreg.key文件&#xff08;注意不要创建成rarreg.key.txt文件了^_^&#xff09;&#xff0c;内容为如下&#xff1a; RAR registration data Team EAT Single PC usage license UI…

Android按钮样式

//创建一个新的XML文件&#xff0c;可命名为styles<style name"button1"><item name"android:layout_height">wrap_content</item><item name"android:textColor">#FFFFFF</item><item name"android:text…

漂亮的Button按钮样式

开发中各种样式的Button,其实这些样式所有的View都可以共用的,可能对于你改变的只有颜色 所有的都是用代码实现 边框样式,给你的View加上边框 <Buttonandroid:layout_width="0dip"android:layout_height="match_parent"android:layout_margin=&q…

「HTML+CSS」--自定义按钮样式【001】

前言 Hello&#xff01;小伙伴&#xff01; 首先非常感谢您阅读海轰的文章&#xff0c;倘若文中有错误的地方&#xff0c;欢迎您指出&#xff5e; 哈哈 自我介绍一下 昵称&#xff1a;海轰 标签&#xff1a;程序猿一只&#xff5c;C选手&#xff5c;学生 简介&#xff1a;因C语…

HTML_炫酷的按钮样式

html部分 <a href"#"><span></span><span></span><span></span><span></span>Neon button</a><a href"#"><span></span><span></span><span></span…

html改变按钮样式

今天有人问我怎么改样式&#xff0c;需求是三个按钮&#xff0c;一次点一个&#xff0c;要求被点击的按钮和没被点的按钮是两种不同的样式&#xff0c;如图所示。 最初三个按钮都没选如图一&#xff0c;然后点击“已读”按钮&#xff0c;“已读”按钮样式改变。再点击“全部”按…

button按钮的一些样式效果

先制作一个button按钮 &#xff0c;将它原本的样式取消掉再把button按钮的颜色设置成transparent &#xff0c;再设置button按钮的边框。首先将button按钮的初始样式取消掉 &#xff0c;在设置button按钮的width和 height &#xff0c;font-size &#xff0c;还有border 现在写…

vue点击按钮改变按钮样式

一. 效果 点击按钮前&#xff1a; 点击按钮后&#xff1a; 再次点击按钮变回原来的样式&#xff1a; 二. 具体代码 <template><div id"box"><button click"btn" id"but" v-bind:class"{ but01: style1, but02: style2 }&qu…

CSS 按钮button美化

.login-button { /* 按钮美化 */width: 270px; /* 宽度 */height: 40px; /* 高度 */border-width: 0px; /* 边框宽度 */border-radius: 3px; /* 边框半径 */background: #1E90FF; /* 背景颜色 */cursor: pointer; /* 鼠标移入按钮范围时出现手势 */outline: none; /* 不显示轮廓…

css 按钮按下样式

在项目开发中&#xff0c;按钮通常需要添加按钮的获得焦点状态&#xff0c;电脑端用 :hover 移动端用 :active 。多个按钮需要添加时&#xff0c;就得添加多个获得焦点样式。 可通过添加背景图片的方式来给所有的按钮添加样式&#xff0c;该样式会给当前按钮添加一个白色的透明…