当前位置: 首页 > news >正文

【黑马点评优化】1-使用JWT登录认证+redis实现自动续期

1-使用JWT登录认证+redis实现自动续期

  • 0 前言
  • 1 原先的redis实现登录鉴权
  • 2 JWT登录认证+Redis自动续期
    • 2.1 认证(identification)授权 (authorization)和鉴权(Authorization)
      • 2.1.1 认证(identification)
      • 2.1.2 授权 (authorization)
      • 2.1.2 鉴权(authentication)
    • 2.2 为什么项目中要鉴权以及现有鉴权方案
    • 2.3 项目中的鉴权(认证+授权)
  • 3 实现
    • 3.1 jwt依赖引入
    • 3.2 Jwt相关文件
      • 3.2.1 JwtUtil.java
      • 3.2.5 JWTUtil单元测试
      • 3.2.3 JwtClaimsConstant.java
      • 3.2.4 JwtProperties
      • 3.2.5 application.yaml
    • 3.3 登录鉴权逻辑修改
      • 3.3.1 登录鉴权逻辑设计思路
      • 3.3.2 登录UserServiceImpl修改
      • 3.3.3 RefreshTokenInterceptor修改
      • 3.3.4 修改MvcConfig如下
  • 4 测试
    • 4.1 登录测试
    • 4.2 鉴权测试
  • 参考资料

项目github地址如下:
https://github.com/xianghua-2/hm-dianping

0 前言

原先的黑马点评中。用户登陆以及后续的鉴权,用的都是用uuid随机生成的token。并不包含任何有效信息

存在以下缺点

  • 缺陷 1:每次请求需查询 Redis
    UUID 本身不携带用户信息,每次鉴权需通过 token 作为 Key 查询 Redis 获取用户数据,高频请求下会显著增加 Redis 负载(参考 9)。
  • 缺陷 2:无法快速验证 Token 合法性
    UUID 没有加密或签名机制,容易被伪造(如恶意构造 token 绕过鉴权),安全性低。
  • 缺陷 3:无法主动失效 Token
    若用户主动退出或 Token 泄露,只能依赖 Redis 过期策略被动等待失效,无法实时拦截。

因此想要用jwt+redis来做登录鉴权

  • 目标 1:减少 Redis 查询压力
    JWT 中直接携带用户 ID,鉴权时无需每次查询用户基础信息(如 ID、角色),仅需解析 JWT。
  • 目标 2:增强安全性
    JWT 通过签名机制防止篡改,结合 Redis 管理 Token 状态,支持主动失效(如踢人下线)。
  • 目标 3:优化存储结构
    仅需将动态或敏感数据(如权限列表、会话状态)存 Redis,减少冗余(参考 36)。

1 原先的redis实现登录鉴权

先说明一下原先的登录注册是如何实现的

具体代码在com/hmdp/service/impl/UserServiceImpl.java 中

    @Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}
//        // 3.校验验证码
//        Object cacheCode = session.getAttribute("code");// 3.从redis获取验证码并校验String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();// @TODO 先不校验验证码if(cacheCode == null || !cacheCode.equals(code)){//3.不一致,报错return Result.fail("验证码错误");}//注释掉以上部分//一致,根据手机号查询用户User user = query().eq("phone", phone).one();//5.判断用户是否存在if(user == null){//不存在,则创建user =  createUserWithPhone(phone);}// 7.保存用户信息到 redis中// 7.1.随机生成token,作为登录令牌String token = UUID.randomUUID().toString(true);// 7.2.将User对象转为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), //beanToMap方法执行了对象到Map的转换CopyOptions.create().setIgnoreNullValue(true) //BeanUtil在转换过程中忽略所有null值的属性.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); //对于每个字段值,它简单地调用toString()方法,将字段值转换为字符串。// 7.3.存储String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.设置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);}

重点关注如下信息

可以看到,先随机生成UUID 作为登录令牌token,也就是后面要存入redis中的key。(之后这个key就对应该用户了)

后面,将用户信息转换成hashMap结构,最后将key和value(用户信息)一起存入redis中。

后续用户发起请求时,会带着生成的token,然后redis中就可以根据token查找到对应用户的信息。

        // 7.保存用户信息到 redis中// 7.1.随机生成token,作为登录令牌String token = UUID.randomUUID().toString(true);// 7.2.将User对象转为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), //beanToMap方法执行了对象到Map的转换CopyOptions.create().setIgnoreNullValue(true) //BeanUtil在转换过程中忽略所有null值的属性.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); //对于每个字段值,它简单地调用toString()方法,将字段值转换为字符串。// 7.3.存储String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.设置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);

2 JWT登录认证+Redis自动续期

可以看到,由于UUID中并不带有有效信息,并且生成的token比较长,会增加redis的存储压力。

在这里,就考虑到使用JWT来实现登录认证和后续的鉴权。

在此之前,我们先来了解一下现有的常用的登录认证方法。以及什么是认证(Authentication),什么是鉴权(Authorization)。

2.1 认证(identification)授权 (authorization)和鉴权(Authorization)

2.1.1 认证(identification)

认证通俗的来讲就是验证用户的身份,证明你是你自己。

举个例子,上班打卡的时候,需要用人脸认证/指纹认证,当你的人脸和指纹与数据库中录入的相匹配的时候,就打卡成功。

具体到互联网中:

  • 用户名密码登录
  • 邮箱发送登陆链接
  • 微信登录

一般用于登录某个系统。

2.1.2 授权 (authorization)

简单来说,授权一般是指获取用户的委派权限。在英文中对应于authorization这个单词。

在信息安全领域,授权是指资源所有者委派执行者,赋予执行者指定范围的资源操作权限,以便执行者代理执行对资源的相关操作。这里面包含有如下四个重要概念,

资源所有者,拥有资源的所有权利,一般就是资源的拥有者。
资源执行者,被委派去执行资源的相关操作。
操作权限,可以对资源进行的某种操作。
资源,有价值的信息或数据等,受到安全保护。
需要说明的是,资源所有者和执行者可以是自然人,就是普通用户,但不限于自然人。在信息安全领域,资源所有者和执行者,很多时候是应用程序或者机器。比如用户在浏览器上登录一个网站,那么这个浏览器就成为一个执行者,它在用户登录后获取了用户的授权,代表着用户执行各种指令,进行购物、下单、付钱、转账等等操作。

同时,资源所有者和执行者可以是分开的不同实体,也可以是同一个。若是分开的两者,则资源执行者是以资源所有者的代理形式而存在。

授权的实现方式非常多也很广泛,我们常见的银行卡、门禁卡、钥匙、公证书,这些都是现实生活中授权的实现方式。其实现方式主要通过一个共信的媒介完成,这个媒介不可被篡改,不可随意伪造,很多时候需要受保护,防止被窃取。

在互联网应用开发领域,授权所用到的授信媒介主要包括如下几种,

通过web服务器的session机制,一个访问会话保持着用户的授权信息
通过web浏览器的cookie机制,一个网站的cookie保持着用户的授权信息
颁发授权令牌(token),一个合法有效的令牌中保持着用户的授权信息
前面两者常见于web开发,需要有浏览器的支持。

2.1.2 鉴权(authentication)

鉴权比认证多的其实是授权这一步。

鉴权:确认用户/实体身份及权限。同时进行认证和授权。确认用户的身份后,授予其访问资源的权限。

鉴权是指对于一个声明者所声明的身份权利,对其所声明的真实性进行鉴别确认的过程。在英文中对应于authentication这个单词。

鉴权主要是对声明者所声明的真实性进行校验。若从授权出发,则会更加容易理解鉴权。授权和鉴权是两个上下游相匹配的关系,先授权,后鉴权。授权和鉴权两个词中的“权”,是同一个概念,就是所委派的权利,在实现上即为授信媒介的表达形式。

因此,鉴权的实现方式是和授权方式有一一对应关系。对授权所颁发授信媒介进行解析,确认其真实性。下面是鉴权的一些实现方式,

门禁卡:通过门禁卡识别器
钥匙:通过相匹配的锁
银行卡:通过银行卡识别器
互联网web开发领域的session/cookie/token:校验session/cookie/token的合法性和有效性
鉴权是一个承上启下的一个环节,上游它接受授权的输出,校验其真实性后,然后获取权限(permission),这个将会为下一步的权限控制做好准备。

再举个例子。

你通过用户名/密码登录到学校的图书馆里系统中。学校的图书馆管理系统确认了你的身份 (认证

之后,图书馆系统会根据你的身份(学生/老师/管理员)来分配不同的权限。(授权

鉴权就是上述两步加起来。认证+授权。

2.2 为什么项目中要鉴权以及现有鉴权方案

登录时,需要输入手机号和验证码进行登录。

在这一步进行登录认证。

但是,当用户登录之后,后面会不停的向服务器发起请求,服务器是怎么知道是哪个用户发出的呢?(HTTP无状态协议)

  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • 服务器为什么要知道是哪个用户发出的?

    答:因为有些页面只有登录之后/特定用户才能访问。

所以,为了解决http无状态协议带来的,服务器不知道哪个用户发起的请求这一问题。

现有的解决方案有:cookie,session,jwt,token。

简单说明一下这三者的区别和共同点。

Cookie

  • cookie 存储在客户端:cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
  • cookie 是不可跨域的:每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。

Session

  • session 是另一种记录服务器和客户端会话状态的机制
  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中

image.png

session 认证流程:

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
  • 是一种认证授权机制。
  • JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
  • 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。

2.3 项目中的鉴权(认证+授权)

因此,使用JWT+Redis来实现项目鉴权。

用户登录

  • 生成jwttoken(token中存储用户id)

  • Redis中也将用户id作为key,用户信息+当前生成的jwttoken作为value

鉴权:

  • 后续,从请求的authorization字段中,获取jwttoken
  • 解析jwttoken,得到用户id
  • 根据用户id,去redis查当前用户最新对应的jwttoken2
  • 判断jwttoken是否和jwttoken2一致。
  • 一致,则说明当前jwttoken有效,放行。

为什么redis中除了要存储用户信息,同时还要存储当前生成的jwttoken呢?

  • 因为假如不存储当前生成的jwttoken的话,而是只根据从jwt中解析出的userid,去redis中查询当前是否有效。会存在以下情况
  • 用户a先在某平台登录,此时后端返回给当前的jwttoken1;()
  • 之后,用户a退出登录。
  • 之后,某时刻,用户a再次登录平台,此时后端会返回给新的jwttoken2;
  • 与此同时,我们假设用户b盗取了用户a上次登录的jwttoken1;
  • 此时,用户b也能够登录后端。(因为jwttoken1,jwttoken2解析出的userId都是一致的。去redis中查询的话,由于用户a现在是在登录的状态,所以redis中该userId也是有效的)

我们通过存储当前最新的jwttoken就能防止这种情况。

方案对比表如下:

维度UUID Token + RedisJWT + Redis
鉴权性能每次请求查 Redis仅解析 JWT,按需查 Redis
安全性低(无签名)高(签名防篡改)
主动失效能力依赖 Redis 过期支持实时踢人下线
存储冗余高(完整用户数据)低(仅动态数据)
适用场景简单低频场景中高频、需安全控制的场景

3 实现

3.1 jwt依赖引入

  1. pom.xml中引入依赖
        <!--引入jwt--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.2</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.2</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.2</version></dependency>

3.2 Jwt相关文件

3.2.1 JwtUtil.java

com/hmdp/utils/JwtUtil.java

在utils下创建工具类文件JwtUtil.java

内容如下:

  • createJWT(String secretKey, long ttlMillis, Map<String, Object> claims)

    根据传入的密钥,过期时间和要存储的信息,创建jwttoken

  • parseJWT(String secretKey, String token)

    根据传入的密钥和jwttoken,解析出jwt中存储的信息。

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;public class JwtUtil {/*** 生成jwt* 使用Hs256算法, 私匙使用固定秘钥** @param secretKey jwt秘钥* @param ttlMillis jwt过期时间(毫秒)* @param claims    设置的信息* @return*/public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {// 指定签名的时候使用的签名算法,也就是header那部分SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;// 生成JWT的时间long expMillis = System.currentTimeMillis() + ttlMillis;Date exp = new Date(expMillis);// 设置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的.setClaims(claims)// 设置签名使用的签名算法和签名使用的秘钥.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))// 设置过期时间.setExpiration(exp);return builder.compact();}/*** Token解密** @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个* @param token     加密后的token* @return*/public static Claims parseJWT(String secretKey, String token) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 设置签名的秘钥.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 设置需要解析的jwt.parseClaimsJws(token).getBody();return claims;}}

3.2.5 JWTUtil单元测试

(1)在pom.xml中引入juit相关依赖

        <!--引入junit--><!-- 单元测试Junit --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency>

(2)在test/java/com.hmdp文件夹下

新建utils package

之后,新建JwtUtilTest.java文件如下。

运行单元测试即可

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.security.Keys;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;import javax.crypto.SecretKey;public class JwtUtilTest {// 密钥private static final String SECRET_KEY = "ThisIsA32BytesLongSecretKeyForHS256";private Map<String,Object> sampleClaims;@Beforepublic void setUp() {sampleClaims = new HashMap<>();sampleClaims.put("userId", 1001);sampleClaims.put("username", "testUser");}// 正常场景测试@Testpublic void testCreateJWT() {String token = JwtUtil.createJWT(SECRET_KEY, 3600000L, sampleClaims);Claims claims = JwtUtil.parseJWT(SECRET_KEY, token);System.out.println(claims);// 断言验证Assertions.assertEquals(1001, claims.get("userId"));Assertions.assertEquals("testUser", claims.get("username"));Assertions.assertNotNull(claims.getExpiration());}// 异常场景:过期Token测试@Testpublic void should_throw_expired_exception() throws InterruptedException {// 生成1毫秒过期的TokenString token = JwtUtil.createJWT(SECRET_KEY, 1L, sampleClaims);Thread.sleep(2); // 确保过期Assertions.assertThrows(ExpiredJwtException.class,() -> JwtUtil.parseJWT(SECRET_KEY, token));}// 异常场景:签名密钥错误测试@Testpublic void should_throw_signature_exception() {String token = JwtUtil.createJWT(SECRET_KEY, 3600000L, sampleClaims);Assertions.assertThrows(SignatureException.class,() -> JwtUtil.parseJWT("wrongSecretKey", token));}// 边界测试:空claims处理@Testpublic void should_handle_empty_claims() {Map<String, Object> emptyClaims = new HashMap<>();String token = JwtUtil.createJWT(SECRET_KEY, 3600000L, emptyClaims);Claims claims = JwtUtil.parseJWT(SECRET_KEY, token);Assertions.assertTrue(claims.isEmpty());}}

3.2.3 JwtClaimsConstant.java

com.hmdp下新建一个constant package之后,新建JwtClaimsConstant类

com/hmdp/constant/JwtClaimsConstant.java

public class JwtClaimsConstant {public static final String EMP_ID = "empId";public static final String USER_ID = "userId";public static final String PHONE = "phone";public static final String USERNAME = "username";public static final String NAME = "name";}

3.2.4 JwtProperties

com.hmdp下新建properties包,之后新建JwtProperties类

com/hmdp/properties/JwtProperties.java

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@ConfigurationProperties(prefix = "hmdp.jwt")
@Data
public class JwtProperties {//    /**
//     * 管理端员工生成jwt令牌相关配置
//     */
//    private String adminSecretKey;
//    private long adminTtl;
//    private String adminTokenName;/*** 用户端微信用户生成jwt令牌相关配置*/private String userSecretKey;private long userTtl;private String userTokenName;}

3.2.5 application.yaml

加上以下内容。

在application中配置jwttoken的密钥,过期时间以及headername

hmdp:jwt:
#    # 设置jwt签名加密时使用的秘钥
#    admin-secret-key: itcast
#    # 设置jwt过期时间
#    admin-ttl: 7200000
#    # 设置前端传递过来的令牌名称
#    admin-token-name: tokenuser-secret-key: ThisIsA32BytesLongSecretKeyForHS256user-ttl: 7200000user-token-name: authorization

3.3 登录鉴权逻辑修改

3.3.1 登录鉴权逻辑设计思路

(1)之前的登录鉴权(UUID+Redis)

回想一下,之前的登录鉴权主要涉及两个地方,一个是用户登录,一个是双拦截器拦截用户请求时。

登录

  • 用户登录时根据uuid生成token,并且将用户信息存储到redis中(key是token

登录完后发的其他请求

  • 登陆后的其他请求,发送时会带着authorizatioin字段(存储token值)。请求会经过以下两个拦截器。
  • 拦截器1(refreshToken):
    • 拦截所有请求
    • 判断当前请求中是否有authorizatioin字段
    • 有的话根据authorizatioin字段的token去redis中查询是否存在当前用户
      • 存在的话,将查到的用户信息放到ThreadLocal中,并刷新redis中token的有效期
      • 不存在的话,放行
    • 没有的话放行
  • 拦截器2(LoginInterceptor)
    • 拦截需要登录的页面请求(比如/user/login/me)
      • 从ThreadLocal中查找是否有当前用户
      • 无则不放行
      • 查到了放行。

(2)现在的登录鉴权(JWT+Redis)

因此,现在我们也是。

  • 用户登录时生成jwttoken(存储的有效信息为userid),并且将用户信息和jwttoken存储到redis中(key是userid
  • 登陆后的其他请求,发送时会带着authorizatioin字段,解析authorizatioin中的jwttoken,之后从jwttoken中解析出userId,根据userId去redis中查询,判断当前用户是否登录。

登录

  • 用户登录时生成jwttoken(存储的有效信息为userid),并且将用户信息和jwttoken存储到redis中(key是userid

登录完后发的其他请求

  • 登陆后的其他请求,发送时会带着authorizatioin字段(存储token值)。请求会经过以下两个拦截器。
  • 拦截器1(refreshToken):
    • 拦截所有请求
    • 判断当前请求中是否有authorizatioin字段
    • 有的话根据authorizatioin字段的jwttoken解析出userId,根据userId去redis中查询是否存在当前用户,并且对比jwttoken和redis中查出的jwttoken是否一致。
      • 存在且一致的话,将查到的用户信息放到ThreadLocal中,并刷新redis中token的有效期
      • 不存在的话,放行
    • 没有的话放行
  • 拦截器2(LoginInterceptor)
    • 拦截需要登录的页面请求(比如/user/login/me)
      • 从ThreadLocal中查找是否有当前用户
      • 无则不放行
      • 查到了放行。

3.3.2 登录UserServiceImpl修改

修改UserServiceImpl.java中的login方法如下:

    // @TODO 2.使用JWT实现登录功能@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}
//        // 3.校验验证码
//        Object cacheCode = session.getAttribute("code");// 3.从redis获取验证码并校验String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();// @TODO 假如要生成1k个用户测试的话,注释掉以下部分,先不校验验证码
//        if(cacheCode == null || !cacheCode.equals(code)){
//            //3.不一致,报错
//            return Result.fail("验证码错误");
//        }//注释掉以上部分//一致,根据手机号查询用户User user = query().eq("phone", phone).one();//5.判断用户是否存在if(user == null){//不存在,则创建user =  createUserWithPhone(phone);}// 6.生成JWTMap<String,Object> claims = new HashMap<>();claims.put(JwtClaimsConstant.USER_ID,user.getId());
//        String token = JwtUtil.createJWT(jwtProperties.getSecretKey(),jwtProperties.getTtl(),claims);
//        String jwttoken = JwtUtil.createJWT("ThisIsA32BytesLongSecretKeyForHS256",30*60*1000,claims);String jwttoken = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);// 7.保存用户信息到 redis中// 7.1.随机生成token,作为登录令牌
//        String token = UUID.randomUUID().toString(true);// 7.2.将User对象转为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), //beanToMap方法执行了对象到Map的转换CopyOptions.create().setIgnoreNullValue(true) //BeanUtil在转换过程中忽略所有null值的属性.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); //对于每个字段值,它简单地调用toString()方法,将字段值转换为字符串。// 7.3.存储String tokenKey = LOGIN_USER_KEY + userDTO.getId();// 7.4.将jwttoken存入userMap中userMap.put("jwttoken",jwttoken);stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.5.设置redis中 userId的有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(jwttoken);}

3.3.3 RefreshTokenInterceptor修改

修改RefreshTokenInterceptor如下

public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;private final JwtProperties jwtProperties; // 直接通过构造器注入public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate,JwtProperties jwtProperties) {this.stringRedisTemplate = stringRedisTemplate;this.jwtProperties = jwtProperties; // 手动接收依赖}
/* 1.基于token来拦截@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}// 2.基于TOKEN获取redis中的用户String key  = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if (userMap.isEmpty()) {return true;}// 5.将查询到的hash数据转为UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的token
//        String token = request.getHeader("authorization");String token = request.getHeader(jwtProperties.getUserTokenName());if (StrUtil.isBlank(token)) {return true;}Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(),token);Long userId =  claims.get(JwtClaimsConstant.USER_ID,Long.class);// 2.基于userId获取redis中的用户String key  = LOGIN_USER_KEY + userId;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if (userMap.isEmpty()) {return true;}// 4.判断token是否一致,防止有以前生成的jwt,仍然能够登录String jwttoken = userMap.get("jwttoken").toString();if(!jwttoken.equals(token)){return true;}// 5.将查询到的hash数据转为UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}

3.3.4 修改MvcConfig如下

import com.hmdp.properties.JwtProperties;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;@Configuration
//Configuration注解表明该类是一个配置类,它允许你通过Java代码来配置Spring应用程序,而不是使用XML文件。Spring容器在启动时会扫描并加载这些配置类。
public class MvcConfig implements WebMvcConfigurer { //WebMvcConfigurer接口允许自定义Spring MVC的配置。通过实现这个接口,可以添加拦截器、视图控制器、视图解析器等。@Resourceprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate JwtProperties jwtProperties;public void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);// order越小,优先级越高,所以是先会通过token刷新拦截器// token刷新的拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate,jwtProperties)).addPathPatterns("/**").order(0); //拦截所有请求}
}

4 测试

4.1 登录测试

使用APIfox测试

http://localhost:8081/user/login

其中请求体如下:

// 场景1:手机号+验证码
{"phone": "13812345678","code": "123456"
}

在这里插入图片描述

4.2 鉴权测试

GET请求,下面的请求是只有登录后才能够得到结果的。注意要加上authorization参数,参数值为刚刚返回的data。

http://localhost:8081/user/me

当jwttoken不对的时候

在这里插入图片描述

jwttoken正确的时候。

在这里插入图片描述

参考资料

还分不清 Cookie、Session、Token、JWT?看这一篇就够了-阿里云开发者社区 (aliyun.com)

认证、授权、鉴权和权限控制概念区别_鉴权和授权的区别-CSDN博客

相关文章:

【黑马点评优化】1-使用JWT登录认证+redis实现自动续期

1-使用JWT登录认证redis实现自动续期 0 前言1 原先的redis实现登录鉴权2 JWT登录认证Redis自动续期2.1 认证&#xff08;identification&#xff09;授权 &#xff08;authorization&#xff09;和鉴权&#xff08;Authorization&#xff09;2.1.1 认证&#xff08;identificat…...

一个让Stable Diffusion更稳定、更易用的Github开源项目

2023除了ChatGPT大火&#xff0c;Stable Diffusion同样也是非常火热&#xff0c;Stable Diffusion是一个Github开源项目&#xff0c;很多爱好者都会本地安装&#xff0c;但面对一些初学者来说&#xff0c;在安装、配置和使用过程中还是会经常出现很多问题&#xff0c;特别不了解…...

Mac之JDK安装

Mac之JDK安装 一.安装 jdk 打开终端输入命令:java -version 查看是否已安装 JDK Oracle 官方下载地址 根据自己Mac 系统安装 查看 Mac 系统&#xff0c;打开中断命令&#xff0c;输入: uname -a Compressed Archive 是压缩文档&#xff0c;下载的是一个 .tar.gz 压缩包 D…...

深入Flask:如何优雅地处理HTTP请求与响应

哈喽,大家好,我是木头左! 本文将带你深入了解如何在Flask中优雅地处理HTTP请求和响应,让你的应用更加高效、安全和用户友好。 创建一个简单的Flask应用 让从创建一个最简单的Flask应用开始: from flask import Flaskapp = Flask(__name__)@app.route(/) def...

kron积计算mask类别矩阵

文章目录 1. 生成类别矩阵如下2. pytorch 代码3. 循环移动矩阵 1. 生成类别矩阵如下 2. pytorch 代码 import torch import torch.nn as nn import torch.nn.functional as Ftorch.set_printoptions(precision3, sci_modeFalse)if __name__ "__main__":run_code 0…...

Redis实现消息队列

什么是消息列队。 消息队列是一种应用间的异步协作机制&#xff0c;同时消息队列中间件是分布式系统中重要的组件&#xff0c;主要解决应用耦合&#xff0c;异步消息&#xff0c;流量削峰等问题。实现高性能&#xff0c;高可用&#xff0c;可伸缩和最终一致性架构。市面上的 M…...

vue+springboot+webtrc+websocket实现双人音视频通话会议

前言 最近一些时间我有研究&#xff0c;如何实现一个视频会议功能&#xff0c;但是找了好多资料都不太理想&#xff0c;最终参考了一个文章 WebRTC实现双端音视频聊天&#xff08;Vue3 SpringBoot&#xff09; 只不过&#xff0c;它的实现效果里面只会播放本地的mp4视频文件&…...

【免费送书活动】《MySQL 9从入门到性能优化(视频教学版)》

本博主免费赠送读者3本书&#xff0c;书名为《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;》。 《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;&#xff08;数据库技术丛书&#xff09;》(王英英)【摘要 书评 试读】- 京东图书 这本书已经公开…...

【设计模式】【行为型模式】命令模式(Command)

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f3b5; 当你的天空突…...

1.14学习总结

日常刷题单 刷了题目后&#xff0c;对于排序方法更加熟练&#xff0c;手搓代码的速度也得到了提高。 感觉字符串还不熟练&#xff0c;高精度更是云里雾里&#xff0c;上升空间极大。 同时看见今晚有个入门难度的测试&#xff0c;去练了练手&#xff0c;想看看自己是什么成分&…...

QxOrm生成json

下载Qxorm-1.5版本 使用vs打开项目&#xff0c;直接生成即可&#xff1a; lib目录中会生成dll和lib文件 新建Qt项目使用Qxorm: 将QxOrm中上面三个目录拷贝到新建的Qt项目中 pro文件添加使用QxOrm第三方库 INCLUDEPATH $$PWD/include/ LIBS -L"$$PWD/lib" LIBS…...

伯克利 CS61A 课堂笔记 09 —— Data Abstraction

本系列为加州伯克利大学著名 Python 基础课程 CS61A 的课堂笔记整理&#xff0c;全英文内容&#xff0c;文末附词汇解释。 目录 01 Data Abstraction 数据抽象 Ⅰ Rational Numbers Ⅱ Rational Number Arithmetic 02 Pairs 对 Ⅰ Representing Pairs Using Lists Ⅱ Re…...

高频 SQL 50 题(基础版)

高频 SQL 50 题&#xff08;基础版&#xff09; 查询连接聚合函数排序和分组高级查询和连接子查询高级字符串函数 / 正则表达式 / 子句 查询 链接: link 链接: link 链接: link 链接: link 链接: link 连接 链接: 高频 SQL 50 题基础版_1378. 使用唯一标识码替换员工ID 链接…...

HtmlRAG:RAG系统中,HTML比纯文本效果更好

HtmlRAG 方法通过使用 HTML 而不是纯文本来增强 RAG 系统中的知识表示能力。通过 HTML 清洗和两步块树修剪方法&#xff0c;在保持关键信息的同时缩短了 HTML 文档的长度。这种方法优于现有基于纯文本的RAG的性能。 方法 其实主要看下围绕html提纯思路&#xff0c;将提纯后的…...

python学opencv|读取图像(六十二)使用cv2.morphologyEx()形态学函数实现图像梯度处理

【1】引言 前序已经学习了腐蚀和膨胀的单独作用函数&#xff0c;还研究了按照不同顺序调用腐蚀和膨胀函数调整图像效果&#xff0c;相关文章包括且不限于&#xff1a; python学opencv|读取图像&#xff08;六十一&#xff09;先后使用cv2.dilate()函数和cv2.erode()函数实现图…...

10G EPON光模块

一、10G EPON对称光模块 工作模式&#xff1a;上行突发接收、下行连续发射。 工作原理&#xff1a;当需要发送信号时&#xff0c;系统信号通过光模块的电接口把信号传送到驱动芯片&#xff0c;芯片处理后&#xff0c;驱动激光器发出调制光信号&#xff0c;经光纤发到远端&…...

RocketMQ与kafka如何解决消息丢失问题?

0 前言 消息丢失基本是分布式MQ中需要解决问题&#xff0c;消息丢失时保证数据可靠性的范畴。如何保证消息不丢失程序员面试中几乎不可避免的问题。本文主要说明RocketMQ和Kafka在解决消息丢失问题时&#xff0c;在生产者、Broker和消费者之间如何解决消息丢失问题。 1.Rocket…...

每日Attention学习23——KAN-Block

模块出处 [SPL 25] [link] [code] KAN See In the Dark 模块名称 Kolmogorov-Arnold Network Block (KAN-Block) 模块作用 用于vision的KAN结构 模块结构 模块代码 import torch import torch.nn as nn import torch.nn.functional as F import mathclass Swish(nn.Module)…...

【前端】ES6新特性汇总

本文作者&#xff1a; slience_me ES6新特性汇总 1. let声明变量 1&#xff09;let作用域 // var 声明的变量往往会越域 // let 声明的变量有严格的局部作用域 {var a 1;let b 2; } console.log(a); // 1 console.log(b); // 报错 b is not defined2&#xff09;声明次数 …...

2024 CyberHost 语音+图像-视频

项目&#xff1a;CyberHost: Taming Audio-driven Avatar Diffusion Model with Region Codebook Attention 音频驱动的身体动画面临两个主要挑战&#xff1a;&#xff08;1&#xff09;关键人体部位&#xff0c;如面部和手部&#xff0c;在视频帧中所占比例较小&#x…...

Git命令摘录

使用 Git 升级软件通常是指通过 Git 仓库获取软件的最新版本或更新代码。以下是详细的步骤和方法&#xff1a; 1. 克隆软件仓库 如果这是你第一次获取软件代码&#xff0c;可以使用 git clone 命令将远程仓库克隆到本地。 git clone <仓库地址> 例如&#xff1a; git cl…...

DeepSeek24小时写作机器人,持续创作高质量文案

内容创作已成为企业、自媒体和创作者的核心竞争力。面对海量的内容需求&#xff0c;人工创作效率低、成本高、质量参差不齐等问题日益凸显。如何在有限时间内产出高质量内容&#xff1f;DeepSeek写作机器人&#xff0c;一款24小时持续创作的智能工具&#xff0c;为企业和个人提…...

Python 面向对象的三大特征

前言&#xff1a;本篇讲解面向对象的三大特征&#xff08;封装&#xff0c;继承&#xff0c;多态&#xff09;&#xff0c;还有比较细致的&#xff08;类属性类方法&#xff0c;静态方法&#xff09;&#xff0c;分步骤讲解&#xff0c;比较适合理清楚三大特征的思路 面向对象的…...

在mac中安装Colima使用docker(替代Docker Desktop)

目录 推荐方案&#xff1a;Colima Docker CLI&#xff08;原生 ARM 支持&#xff09; 步骤 1: 安装必需工具 步骤 2: 启动 Colima (优化 ARM 虚拟机) 步骤 3: 绑定 Docker CLI 到 Colima 步骤 4: 验证 Docker 运行 方案对比与注意事项 常见陷阱 卸载残留配置&#xff…...

YOLO11网络结构以及改进1

YOLO11 1.YOLO11网络结构图在哪里&#xff1f;2.对应的网络结构图3.每一个模块详解3.1 Conv模块3.2关于卷积模块3.3 关于给各个模块指定参数的细节 4.加入CBAM 1.YOLO11网络结构图在哪里&#xff1f; 2.对应的网络结构图 3.每一个模块详解 3.1 Conv模块 位置&#xff1a;ultr…...

EtherNetIP转ModbusTCP网关,给风电注入“超级赛亚人”能量

EtherNetIP转ModbusTCP网关&#xff0c;给风电注入“超级赛亚人”能量 在工业通信领域&#xff0c;常常需要将不同网络协议的设备和系统连接起来&#xff0c;以实现更高效的数据交互和系统集成。比如&#xff0c;把EtherNet/IP设备及其网络连接到ModbusTCP网络系统&#xff0c…...

30天开发操作系统 第 20 天 -- API

前言 大家早上好&#xff0c;今天我们继续努力哦。 昨天我们已经实现了应用程序的运行, 今天我们来实现由应用程序对操作系统功能的调用(即API, 也叫系统调用)。 为什么这样的功能称为“系统调用”(system call)呢&#xff1f;因为它是由应用程序来调用(操作)系统中的功能来完…...

DeepSeek处理自有业务的案例:让AI给你写一份小众编辑器(EverEdit)的语法着色文件

1 DeepSeek处理自有业务的案例&#xff1a;让AI给你写一份小众编辑器(EverEdit)的语法着色文件 1.1 背景 AI能力再强&#xff0c;如果不能在企业的自有业务上产生助益&#xff0c;那基本也是一无是处。将企业的自有业务上传到线上训练&#xff0c;那是脑子进水的做法&#xff…...

在香橙派5 NPU上使用Yolov5

【香橙派】使用NPU部署Yolov5的完整解决方案 香橙派使用NPU部署Yolov5的完整解决方案 Orangepi 5 Pro(香橙派5pro)部署yolov5 RK3588实战&#xff1a;调用npu加速&#xff0c;yolov5识别图像、ffmpeg发送到rtmp服务器 香橙派5 RK3588 yolov5模型转换rknn及部署踩坑全记录 orang…...

常用排序算法

1. 基础排序算法 1.1 冒泡排序&#xff08;Bubble Sort&#xff09; 原理&#xff1a; 依次比较相邻元素&#xff0c;将较大的元素逐步"冒泡"到右侧。 def bubble_sort(arr):n len(arr)for i in range(n):swapped Falsefor j in range(0, n-i-1):if arr[j] >…...

MySQL判空函数--IFNULL函数的使用

文章目录 IFNULL函数介绍IFNULL函数的语法举例相关扩展 IFNULL函数介绍 在MySQL中&#xff0c;IFNULL函数用于判断给定的表达式是否为NULL。如果表达式为NULL&#xff0c;则IFNULL函数返回指定的替代值&#xff1b;如果表达式不为NULL&#xff0c;则返回表达式本身的值。 IFN…...

Git 设置代理

设置 HTTP 和 HTTPS 代理 运行以下命令来配置 Git 的 HTTP 和 HTTPS 代理&#xff1a; git config --global http.proxy http://127.0.0.1:7890 git config --global https.proxy https://127.0.0.1:7890 验证代理设置 你可以通过以下命令检查代理是否设置成功&#xff1a; g…...

nsc account 及user管理

从安全角度&#xff0c;推荐使用sign 模式进行nats account及用户管理 把权限放到account level 用户密码泄露可以通过快速更换用户可以设置过期日期&#xff0c;进行安全轮换 此外通过nsc 管理用户和权限&#xff0c;可以统一实现全局管控&#xff0c;包括subject管控&#…...

llama.cpp部署 DeepSeek-R1 模型

一、llama.cpp 介绍 使用纯 C/C推理 Meta 的LLaMA模型&#xff08;及其他模型&#xff09;。主要目标llama.cpp是在各种硬件&#xff08;本地和云端&#xff09;上以最少的设置和最先进的性能实现 LLM 推理。纯 C/C 实现&#xff0c;无任何依赖项Apple 芯片是一流的——通过 A…...

NO.18十六届蓝桥杯备战|循环嵌套|乘法表|斐波那契|质数|水仙花数|(C++)

循环嵌套 循环嵌套的使⽤ while &#xff0c; do while &#xff0c; for &#xff0c;这三种循环往往会嵌套在⼀起才能更好的解决问题&#xff0c;就是我们所说的&#xff1a;循环嵌套。这三种循环都可以任意嵌套使⽤ ⽐如&#xff1a; 写⼀个代码&#xff0c;打印⼀个乘法⼝…...

用什么办法能实现ubuntu里面运行的自己开发的python程序能自动升级。

要实现Ubuntu中自己开发的Python程序自动升级&#xff0c;可以通过以下几种方式&#xff1a; 1. 使用 Git 仓库 定时任务 如果你的Python程序托管在Git仓库中&#xff0c;可以通过定时拉取最新代码来实现自动升级。 步骤&#xff1a; 确保Python程序在Git仓库中。在Ubuntu上…...

org.apache.kafka.common.errors.TimeoutException

个人博客地址&#xff1a;org.apache.kafka.common.errors.TimeoutException | 一张假钞的真实世界 使用kafka-console-producer.sh向远端Kafka写入数据时遇到以下错误&#xff1a; $ bin/kafka-console-producer.sh --broker-list 172.16.72.202:9092 --topic test This is …...

【AI实践】deepseek支持升级git

当前Windows 11 WSL的git是2.17&#xff0c;Android Studio提示需要升级到2.19版本 网上找到指导文章 安装git 2.19.2 cd /usr/src wget https://www.kernel.org/pub/software/scm/git/git-2.19.2.tar.gz tar xzf git-2.19.2.tar.gz cd git-2.19.2 make prefix/usr/l…...

IntelliJ IDEA 2024.1.4版无Tomcat配置

IntelliJ IDEA 2024.1.4 (Ultimate Edition) 安装完成后&#xff0c;调试项目发现找不到Tomcat服务&#xff1a; 按照常规操作添加&#xff0c;发现服务插件中没有Tomcat。。。 解决方法 1、找到IDE设置窗口 2、点击Plugins按钮&#xff0c;进入插件窗口&#xff0c;搜索T…...

利用Firewalld和Iptables实现IP端口限制与开放

这里写目录标题 前言一、FirewalldIP端口限制1.1 确认启动状态1.2 启动Firewalld1.3 查看当前连接到Nacos的IP1.4 添加访问规则1.5 重新加载配置1.6 查看当前活动的规则列表1.7 移除某个规则 二、Firewalld 开放端口2.1 开放 6379端口2.2 重新加载防火墙2.3 验证规则 三、Iptab…...

两步在 Vite 中配置 Tailwindcss

第一步&#xff1a;安装依赖 npm i -D tailwindcss tailwindcss/vite第二步&#xff1a;引入 tailwindcss 更改配置 // src/main.js import tailwindcss/index// vite.config.js import vue from vitejs/plugin-vue import tailwindcss from tailwindcss/viteexport default …...

单片机原理与运用

个人主页&#xff1a;java之路-CSDN博客(期待您的关注) 目录 一、走进单片机的世界 二、单片机是什么 &#xff08;一&#xff09;定义与本质 &#xff08;二&#xff09;与普通计算机的区别 三、单片机的工作原理深度剖析 &#xff08;一&#xff09;硬件组成及功能 &am…...

MYSQL-数据恢复与备份

个人主页&#xff1a;java之路-CSDN博客(期待您的关注) 目录 数据的重要性与备份恢复的意义 MySQL 备份大揭秘 备份类型大盘点 备份工具展示台 备份实操全流程 MySQL 恢复大作战 恢复原理深剖析 恢复方法大集合 实战案例大放送 备份与恢复的最佳实践 备份策略制定法 …...

【Java】实现后端请求接口

【Java】实现后端请求接口 【一】使用 HttpURLConnection 实现四种请求方式的示例【1】Get请求【2】POST请求【3】PUT请求【4】DELETE 请求【5】汇总工具类&#xff0c;通过传参实现4种请求 【二】HttpClient 实现四种请求方式的示例【1】GET请求【2】POST 请求【3】PUT 请求【…...

人工智能之深度学习的革命性突破

深度学习的革命性突破 深度学习是机器学习的一个子领域&#xff0c;通过模拟人脑神经网络的结构和功能&#xff0c;实现对复杂数据的高效处理。近年来&#xff0c;深度学习在计算机视觉、自然语言处理、语音识别等领域取得了革命性突破。本文将深入探讨深度学习的核心架构、突…...

JavaScript document.write()介绍(直接将内容写入HTML文档的早期方法,已逐渐被现代DOM操作方法取代)

文章目录 **一、基本语法****二、核心功能**1. **在文档加载阶段写入**2. **文档加载后调用会导致覆盖****三、注意事项**1. **覆盖风险**2. **性能问题**3. **XSS 漏洞**4. **已关闭的文档流** **四、使用场景&#xff08;不推荐&#xff0c;但需了解&#xff09;****五、现代…...

mybatis-plus逆向code generator pgsql实践

mybatis-plus逆向code generator pgsql实践 环境准备重要工具的版本供参考pom依赖待逆向的SQL 配置文件CodeGenerator配置类配置类说明 环境准备 重要工具的版本 jdk1.8.0_131springboot 2.7.6mybatis-plus 3.5.7pgsql 14.15 供参考pom依赖 <?xml version"1.0&quo…...

【二叉树学习7】

力扣236.二叉树的最近公共祖先 链接: link 思路 要找p&#xff0c;q的公共祖先&#xff0c;可以从下往上遍历二叉树&#xff0c;而二叉树的后序遍历是天然的从下往上遍历。这题采用的是递归的方法&#xff0c;递归结束条件就是root为null或者rootp或者rootq就结束递归。 然后…...

LabVIEW显微镜成像偏差校准

在高精度显微镜成像中&#xff0c;用户常常需要通过点击图像的不同位置&#xff0c;让电机驱动探针移动到指定点进行观察。然而&#xff0c;在实际操作中&#xff0c;经常会遇到一个问题&#xff1a;当点击位于图像中心附近的点时&#xff0c;探针能够相对准确地定位&#xff1…...

什么是弧形光源

工业检测中的弧形光源是一种专门设计用于机器视觉和自动化检测的照明设备,通常用于提供均匀、高对比度的照明,以增强图像采集质量,便于检测系统识别和分析目标物体的特征。以下是关于工业检测弧形光源的详细介绍: 特点 1均匀照明: 弧形设计能够提供均匀的漫反射光,减少阴…...