【深入浅出 Spring Security(十三)】使用 JWT 进行前后端分离认证(附源码)

news2024/10/5 17:20:34

使用 JWT 进行前后端分离认证

  • 一、JWT 的简单介绍
  • 二、使用 JWT 进行安全认证
    • 后端结合SpringSecurity实现
    • 前端Vue3结合Pinia、Axios实现
    • 测试结果

一、JWT 的简单介绍

JWT 全称 Java web Token,在此所讲述的是 JWT 用于身份认证,用服务器端生成的JWT去替代原始的Session认证,以提高安全性。

JWT本质是一个Token令牌,是由三部分组成的字符串,分别是头部(header)、载荷(payload)和签名(signature)。头部一般包含该 JWT 的基本信息,例如所使用的加密算法;载荷一般包含所需要传递的信息,如用户名;签名则是通过对头部、载荷和密钥加密生成的,用于验证 JWT 的真实性和完整性(即拿到前端传过来的Token,通过其头部、载荷和密钥去生成一个签名,然后比对是否与传过来的Token签名部分是否一致)。

二、使用 JWT 进行安全认证

后端结合SpringSecurity实现

  1. 导入相关依赖(jwt相关的和Spring Security依赖)
		<!--SpringSecurity-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</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>
  1. 将生成 jwt 和 认证 jwt 的实现以方法的形式封装成一个工具类(jwt的认证即前端传过来的token和后端中的进行比对),封装的工具类如下(其实封装的方式很多,不局限于这种):
/**
 * jwt加密和解密的工具类
 */
public class JWTUtil {

    /**
     * 签发JWT;这里创建的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();  // 通过操作加密生成key
        JwtBuilder builder = Jwts.builder()
                .setId(id)
                .setSubject(subject)   // 主题
                .setIssuer("xc")// 签发者:小柴
                .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 createJWT(String username) {
        return createJWT(username, username, 60 * 60 * 1000);
    }

    /**
     * 验证JWT
     * 根据验证时抛出的超时异常、签名异常、其他异常进行一定的操作
     *
     * @param jwtStr
     * @return
     */
    public static CheckResult validateJWT(String jwtStr) {
        CheckResult checkResult = new CheckResult();
        // 如果jwtStr为空的话,设置errcode为jwt不存在
        if(StringUtils.isEmpty(jwtStr)){
            checkResult.setSuccess(false);
            checkResult.setErrCode(JWTConstant.JWT_ERRCODE_NULL);
        }
        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_SECRET);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }


    /**
     * 解析JWT字符串
     *
     * @param jwt
     * @return 返回 jwt 解析后的 payload
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

}

在这个工具类中用到了俩个自定义的类,一个封装是验证 jwt 结果集实体类 CheckResult,它内部封装了三个属性:errorCode:错误编码,success:验证是否成功,claims:jwt 中包含的一些信息;使用工具类验证JWT时返回该对象,具体代码如下:

@Data
@NoArgsConstructor
/**
 * JWT 验证信息
 */
public class CheckResult {

    private int errCode;

    private boolean success;

    private Claims claims;
    
}

另一个是一个在验证/生成 JWT 时所需用到的常量类 JWTConstant,如:验证失败所对应异常的编码(自定义的),JWT 秘钥等等。具体代码如下:

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 秘钥 1
     */
    public static final String JWT_SECRET = "bG92ZS14bXE=";
    /**
     * JWT 秘钥 2
     */
    public static final String JWT_SECERT2 = "8677df7fc3a34e26a61c034d5ec8245d";			//密匙
    public static final long JWT_TTL = 24*60 * 60 * 1000;		//token有效时间
}

  1. 由于前后端的话你使用了 JWT 进行认证,所以我们得关闭Spring Security 默认的Session认证,即得把 Session 管理关了,至于为什么不使用默认的进行认证(Session 认证)?原因很多,如:当认证的用户多了,Session占有的内存会不断地增大;Session是不安全的,很容易造成 CSRF 等等…即在配置 SecurityFilterChain 的时候填上如下代码:
        // 关闭session
        // 关闭原因:
        // 1. 前后端进行通信,每个请求都是一个独立的事务,开启session管理可能会使得信息无法共享
        // 2. 采用session管理的话,多个用户进行访问服务器端的内存会占用过高,这是因为session的废除机制是超时机制
        // 3. 采用session管理功能,这也是一个安全漏洞
        // 这里使用jwt(Java web token)令牌的方式进行认证,不需要session了
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  1. 当用户登录成功后,是需要将这token传给前端的,然后让前端发送请求的时候携带这个token,请求报文中有了这个token才允许请求通过,否则返回401,无权限(当然这异常处理可以自定义,这里不说明了,还有这个token一般在请求报文中的请求头中,当然这是下面前端该实现的),那如何将token传递给前端呢?即在登录认证成功后,Spring Security会去调用配置的AuthenticationSuccessHandler 中的 onAuthenticationSuccess 方法对登录成功的一些操作(即登录成功后需要返回给前端的数据就可以在这个方法中进行实现),那有了JWT工具类,这方法就简单实现了,下面是实现的具体代码(当然如何配置这个handler这里就不说了,在专栏里有专门的博客解释了):
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        // 设置响应编码格式
        response.setContentType("json/application;charset=utf-8");
        // 获取用户名
        String username = authentication.getName();
        // 生成 jwt
        String jwt = JWTUtil.createJWT(username);
        ServletOutputStream out = response.getOutputStream();
        // 将 jwt 返回给前端
        out.write(objectMapper.writeValueAsString(BackResult.success(jwt)).getBytes());
        out.close();
    }
}
  1. 首先得明白认证成功后的数据是放在 SecurityContextgHolder 中的,内部默认使用的是 ThreadLocal 去存放认证信息(内部用了策略模式,默认采用的策略是用ThreadLocal),当一个请求结束后这个Authentication会移除,原本移除会放在Session里一同返回给前端,但咱现在把Session管理给静止了(这在【深入浅出 Spring Security(四)】登录用户数据的获取,超详细的源码分析 中详细说明了)。咱现在用的是JWT认证方式了,前端拿到这个token后,放在请求头中向后端发送请求时,后端得对这个token进行验证,如果验证成功了咱得从这个token中提取一些数据封装成Authentication放入 SecurityContextHolder 中,将 SecurityContextHolder 中的对应 Authentication 中的 authenticated 属性设置为 true,以表示认证成功,即这个请求认证成功了(但不代表授权成功哈,提一嘴😇)
    至于为什么要设置为 true,是因为在后面遇到 FilterSecurityInterceptor 拦截器判断是否授权时,会对这个进行判断,如果不是true的话会重新认证得到 Authentication 然后进行授权,到时候所响应的就是无权限访问401了。

例如 Authentication 实现类中的 UsernamePasswordAuthenticationToken,即可调用 authenticated 方法来返回一个认证成功了的 Authentication 认证信息,当然你也可以用别的重载构造…:

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}

	public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
		return new UsernamePasswordAuthenticationToken(principal, credentials);
	}

	public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
	}

至于如何去实现,其实在 Spring Security 中提供了基本的认证过滤器,我们可以自定义基本的认证过滤器 BasicAuthenticationFilter,去重写它的 doFilterInternal 方法,对该认证操作进行实现。具体代码如下,下面是小编自定义的 JWTAuthenticationFilter,内部是 sysUsreService 对象是用来根据获取数据库的用户信息的,而 URL_PERMITTED_LIST 中的具体 uri 是小编配置的无需通过Token认证即可请求服务器端的,这些都是根据具体需求自己配置的:

@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    @Resource
    private SysUserService sysUserService;

    private static final String[] URL_PERMITTED_LIST = {
            "/api/auth/login",
            "/api/auth/logout",
            "/captcha",
            "/password",
            "/image/**",
            "/test/**"
    };

    public JWTAuthenticationFilter(@Autowired AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws IOException, ServletException {

        String token = request.getHeader("token");
        log.info("token--------{}",token);
        System.out.println("请求 URL:" + request.getRequestURI());
        if(Arrays.asList(URL_PERMITTED_LIST).contains(request.getRequestURI())){
            chain.doFilter(request,response);
            return;
        }
        // 验证Token,如果验证失败对失败进行处理
        CheckResult checkResult = JWTUtil.validateJWT(token);
        if(!checkResult.isSuccess()){
            switch(checkResult.getErrCode()){
                case JWTConstant.JWT_ERRCODE_NULL: throw new JwtException("Token 不存在");
                case JWTConstant.JWT_ERRCODE_EXPIRE: throw new JwtException("Token 已过期");
                case JWTConstant.JWT_ERRCODE_FAIL: throw new JwtException("Token 认证过期");
            }
        }
        // 解析jwt去获取用户名
        Claims claims = checkResult.getClaims();
        String username = claims.getSubject();
        SysUser sysUser = sysUserService.getByUserName(username);
        // 根据查询的用户信息封装成一个Authentication用户认证信息
        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(sysUser.getUsername(),null,new ArrayList<GrantedAuthority>());
        // 将得到的用户认证信息填入到上下文中
        SecurityContextHolder.getContext().setAuthentication(auth);
        System.out.println(SecurityContextHolder.getContext());
        // 放行
        chain.doFilter(request,response);
    }
}

在Security配置SecurityFilterChain时进行如下配置,将过滤器添加到过滤器链中。

        // 添加自定义的过滤器-基本认证过滤器,让每个请求都得经过jwt认证...
        http.addFilter(jwtAuthenticationFilter(http));

可以说这 5 步,这 JWT 认证的后端部分就算完成了。

前端Vue3结合Pinia、Axios实现

  1. 下载 Pinia、Axios
    npm install axios
    npm install pinia
    然后在 main.js 中,使用 pinia 这个插件。

  2. 定义自定义的 Store,登录成功后可通过调用 SET_TOKEN 方法将 token 存入到 sessionStorage 中。

import {defineStore} from "pinia";

export const XCStore = defineStore("XCStore",{
    state: () => ({
        token: 'xxx'
    }),
    actions:{
        SET_TOKEN(state,token){
            state.token = token
            sessionStorage.setItem("token",token)
        }
    },
    getters : {
        GET_TOKEN(){
            return sessionStorage.getItem("token")
        }
    }
})
  1. 给 Axios 添加请求拦截器,让每个 Axios 请求都携带上这个 token。下面是整个 Axios 配置,其中包含了添加请求拦截器:
// 引入axios
import axios from 'axios';

let baseUrl="http://localhost:8081/";
// 创建axios实例
const httpService = axios.create({
    // url前缀-'http:xxx.xxx'
    // baseURL: process.env.BASE_API, // 需自定义
    baseURL:"http://localhost:8081/",
    // 请求超时时间
    timeout: 3000 // 需自定义
});

//添加请求和响应拦截器
// 添加请求拦截器
httpService.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    config.headers.token=window.sessionStorage.getItem('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
}

  1. 通过了登录认证,通过调用 store 中的 SET_TOKEN 方法将登录认证传过来的 token 添加到 sessionStorage 中。
    function submit() {
        formRef.value.validate(async(valid)=>{
            if(valid) {
                try {
                    let result = await RequestUtil.post(`api/auth/login`, formData.value);
                    let data = result.data
                    if(data.status === 200){
                        store.SET_TOKEN(store.$state,data.data)
                    }else {
                        ElMessage.error(data.msg)
                    }
                }catch (err) {
                    console.log("error :" + err)
                    ElMessage.error("服务器出错,请联系管理员")
                }
            } else {
                console.log("验证失败")
            }
        })
    }

测试结果

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/727491.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

spring--Ioc控制反转/DI依赖注入

IOC控制反转-解耦 1.概念&#xff1a;在使用对象的时候&#xff0c;由主动的new转换为外部提供对象&#xff0c;将对象创建的控制权交给外部&#xff0c;即控制反转 2.spring提供了一个容器&#xff0c;称为IOC容器&#xff0c;用来从当ioc中的外部 3.被管理或者被创建的对象在…

ChatGPT实战:如何规划自己的职业生涯?

ChatGPT的出现&#xff0c;不仅改变了人们对人工智能技术的认识&#xff0c;也对经济社会发展产生了深远的影响。那么&#xff0c;在ChatGPT时代&#xff0c;人们应该如何规划自己的职业呢&#xff1f; 职业规划是一个有意义且重要的过程&#xff0c;它可以帮助你在职业生涯中…

什么是BI可视化?企业管理决策为什么要用BI系统?

在当今的商业环境中&#xff0c;数据已经成为企业决策制定的重要基础。然而&#xff0c;面对海量的数据&#xff0c;如何有效地分析和利用这些数据&#xff0c;成为了企业管理者面临的一大挑战。BI(Business Intelligence)系统应运而生&#xff0c;它可以帮助企业管理者从复杂的…

基于Java企业人事管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

如何用好强大的 TDengine 集群 ? 先了解 RAFT 在 3.0 中的应用

大家都知道&#xff1a;由于单机数据库在数据规模、并发访问量等方面存在瓶颈&#xff0c;无法满足大规模应用的需求。因此才有了把数据切割分片&#xff0c;分布存储分布处理在多个节点上的数据库&#xff0c;也就是分布式数据库的由来。 而为了实现数据库的高可用&#xff0…

新装Ubuntu虚拟机环境--基本配置流程

新装Ubuntu虚拟机环境--基本配置流程 安装vmware tools修改源 安装vmware tools 点击菜单栏–>虚拟机–>安装VMwaretools桌面上有个图标&#xff0c;点进去 打开这个压缩包–>Extract到桌面&#xff08;or别的文件夹都可以&#xff0c;如果提示空间不足就换其它文件夹…

手把手写一个LED驱动(1)

目录 1.开启驱动开发之路 1.1、驱动开发的准备工作 1.2、驱动开发的步骤 1.3、实践 2.最简单的模块源码分析 2.1、常用的模块操作命令 2.2、模块的安装 2.3、模块的版本信息vermagic 2.4、模块卸载 2.5、模块中常用宏(MODULE_xxx这种宏的作用是用来添加模块描述信息) …

2023年仪器仪表行业研究报告

第一章 行业概况 仪器仪表行业是指专门从事研究、设计、制造、销售和服务于科学研究、生产和生活中所需的各种仪器、仪表和自动化设备的行业。这些仪器和仪表可以用于测量、指示、记录、调节和控制物理、化学和生物过程中的各种参数。这个行业涵盖了广泛的设备和系统&#xff…

今日分享:Midjourney巧妙地用参考图/垫图来绘画图

大家都知道AI绘画工具每次生成的效果都是随机的&#xff0c;但是现在很多AI绘图工具都提供了利用参考图/垫图的方式出图&#xff0c;这样就可以让让AI画作生成自己想要的布局、场景、色彩等等。 国内的AI绘图工具一般都好操作&#xff0c;国外主流的Midjourney也可以添加参考图…

利用for循环和innerHTML在div中再填入多个div

目录 棋盘给每个小格子加上不同的id加上不同的参数传入 我们设置小格子的点击触发事件 循环填充元素 棋盘 先做棋盘&#xff0c;点击出现 <!DOCTYPE HTML> <html><head><meta charset"utf-8"><style>.sty1{background-color:#aaa;he…

旅游网站制作搭建,为旅行业务带来新机遇

旅游业在全球范围内一直都是蓬勃发展的行业之一。随着互联网的普及以及人们对旅行需求的增加&#xff0c;拥有一个精美而功能强大的旅游网站已经成为了旅行从业者的必备条件。本文旨在简单介绍旅游网站是什么&#xff0c;旅游网站的好处&#xff0c;并提供一些快速制作搭建旅游…

10亿上下文!微软新作,引入LongNet将Transformers上下文长度扩充到10亿

夕小瑶科技说 原创 作者 | python, ZenMogre Transformer处理长序列时较为吃力。因为global attention的存在&#xff0c;模型的时间复杂度是序列长度的2次方级。为了建模更长的上下文&#xff0c;人们也提出了各种稀疏注意力机制。而这次&#xff0c;微软卷到家了&#xff0…

word免费转为pdf怎么转,分享这几个方法给大家!

将Word文档转换为PDF格式是一种常见的需求&#xff0c;因为PDF文件具有广泛的兼容性和安全性。本文将介绍三种免费转换Word为PDF的方法&#xff0c;包括记灵在线工具、使用Word自带功能以及使用Smallpdf。这些方法简单易行&#xff0c;帮助您轻松完成转换&#xff0c;方便与他人…

【尚医通】vue3+ts前端项目开发笔记——项目分析

尚医通开发笔记 一、项目分析 项目在线地址&#xff1a;http://syt.atguigu.cn测试帐号&#xff1a;17720125002 首页 home header 全局组件布局 左&#xff1a;logo 、title中&#xff1a;初始隐藏 搜索框 公共组件显示条件&#xff1a;在页面滚动到页面内搜索框的位置显示…

Go语言中的运算符

Golang 内置的运算符 算术运算符 关系运算符 逻辑运算符 位运算符&#xff08;不常用&#xff09; 赋值运算符 算数运算符 运算符描述相加-相减*相乘/相除%求余 a : 10b : 9fmt.Printf("ab的值为%v\na-b的值为%v\na*b的值为%v\n",ab,a-b,a*b) 除法注意&#xff1a;…

uniapp:粘性布局(吸顶:u-sticky)生效的注意事项

使用场景&#xff1a;要求首次渲染时不需要固定在页面顶部&#xff08;正常布局&#xff09;&#xff0c;当随着页面的滚动&#xff0c;需要将起固定在页面顶部&#xff0c;会使用到可能的有&#xff1a;tab、搜索框、导航、标题、头图…工具&#xff1a;用了uview2的组件<u…

手撕spring04源码(A依赖B)

概述 本章节优化上一章节通过构造方法实例化对象属性填充问题的痛点&#xff0c;并解决A bean依赖B bean的问题 整体设计 知识点补充 spring生命周期 在Spring中&#xff0c;Bean的生命周期包括实例化、初始化和销毁三个阶段。下面是对每个阶段的解释&#xff1a; 实例化…

河南企业级泛域名SSL证书

电脑的普及让网络可以快速发展&#xff0c;紧随网络的发展各个CA认证机构推出了泛域名SSL证书、多域名SSL证书等可以用一张SSL证书保护多个域名网站的SSL数字证书。泛域名SSL证书也叫通配符SSL证书&#xff0c;可以用一张SSL证书保护主域名以及主域名下所有的子域名网站&#x…

NR PDCP(三) data transfer

这篇看下PDCP的data transfer过程&#xff0c;如NR RLC(三) TM and UM mode所述&#xff0c;在UL grant充足的情况下&#xff0c;UM RLC 一直在传输完整的RLC SDU&#xff0c;通过log只能知道UE有在收发data&#xff0c;并不能像LTE似的通过SN去判断UE DL data是否有序接收以及…

平板触控笔要原装的吗?apple pencil的平替笔推荐

如今的电容笔种类越来越多&#xff0c;相信不少人都会在挑选电容笔中踩过坑&#xff0c;例如书写频繁断触&#xff0c;防误触失灵&#xff0c;续航能力欠佳等问题。这样的坑本人也是踩过不少&#xff0c;于是&#xff0c;我决定为大家出一期电容笔详细测评&#xff0c;特意地去…