springboot+vue3无感知刷新token实战

目录

一、java后端

1、token构造实现类

①验证码方式实现类

②刷新token方式实现类

 2、token相关操作:setCookie

①createToken

②refreshToken

二、前端(vue3+axios)


        web网站中,前后端交互时,通常使用token机制来做认证,token一般会设置有效期,当token过了有效期后,用户需要重新登录授权获取新的token,但是某些业务场景下,用户不希望频繁的进行登录授权,但是安全考虑,token的有效期不能设置太长时间,所以有了刷新token的设计,无感知刷新token的机制更进一步优化了用户体验,本文是博主实际业务项目中基于springboot和vue3无感知刷新token的代码实战。

首先介绍无感知刷新token的实现思路:

①首次授权颁发token时,我们通过后端给前端请求response中写入两种cookie

        - access_token

        - refresh_token(超时时间比access_token长一些)

需要注意:

        -后端setCookie时httpOnly=true(限制cookie只能被http请求携带使用,不能被js操作)

        -前端axios请求参数withCredentials=true(http请求时,自动携带token)

②access_token失效时,抛出特殊异常,前后端约定http响应码(401),此时触发刷新token逻辑

③前段http请求钩子中,如果出现http响应码为401时,立即触发刷新token逻辑,同时缓存后续请求,刷新token结束后,依次续发缓存中的请求

一、java后端

后端java框架使用springboot,spring-security

登录接口: 

/**
 * @author lichenhao
 * @date 2023/2/8 17:41
 */
@RestController
public class AuthController {

    /**
     * 登录方法
     *
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/oauth")
    public AjaxResult login(@RequestBody LoginBody loginBody) {
        ITokenGranter granter = TokenGranterBuilder.getGranter(loginBody.getGrantType());
        return granter.grant(loginBody);
    }
}


import lombok.Data;

/**
 * 用户登录对象
 *
 * @author lichenhao
 */
@Data
public class LoginBody {

    /**
     * 用户名
     */
    private String username;

    /**
     * 用户密码
     */
    private String password;

    /**
     * 验证码
     */
    private String code;

    /**
     * 唯一标识
     */
    private String uuid;

    /*
     * grantType 授权类型
     * */
    private String grantType;

    /*
    * 是否直接强退该账号登陆的其他客户端
    * */
    private Boolean forceLogoutFlag;
}

token构造接口类和token实现类构造器如下:

/**
 * @author lichenhao
 * @date 2023/2/8 17:29
 * <p>
 * 获取token
 */
public interface ITokenGranter {

    AjaxResult grant(LoginBody loginBody);
}


/**
 * @author lichenhao
 * @date 2023/2/8 17:29
 */
@AllArgsConstructor
public class TokenGranterBuilder {

    /**
     * TokenGranter缓存池
     */
    private static final Map<String, ITokenGranter> GRANTER_POOL = new ConcurrentHashMap<>();

    static {
        GRANTER_POOL.put(CaptchaTokenGranter.GRANT_TYPE, SpringUtils.getBean(CaptchaTokenGranter.class));
        GRANTER_POOL.put(RefreshTokenGranter.GRANT_TYPE, SpringUtils.getBean(RefreshTokenGranter.class));
    }

    /**
     * 获取TokenGranter
     *
     * @param grantType 授权类型
     * @return ITokenGranter
     */
    public static ITokenGranter getGranter(String grantType) {
        ITokenGranter tokenGranter = GRANTER_POOL.get(StringUtils.toStr(grantType, PasswordTokenGranter.GRANT_TYPE));
        if (tokenGranter == null) {
            throw new ServiceException("no grantType was found");
        } else {
            return tokenGranter;
        }
    }

}

这里通过LoginBody的grantType属性,指定实际的token构造实现类;同时,需要有token

本文我们用到了验证码方式和刷新token方式,如下

1、token构造实现类

①验证码方式实现类

/**
 * @author lichenhao
 * @date 2023/2/8 17:32
 */
@Component
public class CaptchaTokenGranter implements ITokenGranter {

    public static final String GRANT_TYPE = "captcha";

    @Autowired
    private SysLoginService loginService;

    @Override
    public AjaxResult grant(LoginBody loginBody) {
        String username = loginBody.getUsername();
        String code = loginBody.getCode();
        String password = loginBody.getPassword();
        String uuid = loginBody.getUuid();
        Boolean forceLogoutFlag = loginBody.getForceLogoutFlag();

        AjaxResult ajaxResult = validateLoginBody(username, password, code, uuid);
        // 验证码
        loginService.validateCaptcha(username, code, uuid);
        // 登录
        loginService.login(username, password, uuid, forceLogoutFlag);
        // 删除验证码
        loginService.deleteCaptcha(uuid);
        return ajaxResult;
    }

    private AjaxResult validateLoginBody(String username, String password, String code, String uuid) {
        if (StringUtils.isBlank(username)) {
            return AjaxResult.error("用户名必填");
        }
        if (StringUtils.isBlank(password)) {
            return AjaxResult.error("密码必填");
        }
        if (StringUtils.isBlank(code)) {
            return AjaxResult.error("验证码必填");
        }
        if (StringUtils.isBlank(uuid)) {
            return AjaxResult.error("uuid必填");
        }
        return AjaxResult.success();
    }
}


    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @return 结果
     */
    public void login(String username, String password, String uuid, Boolean forceLogoutFlag) {
        // 校验basic auth
        IClientDetails iClientDetails = tokenService.validBasicAuth();
        // 用户验证
        Authentication authentication = null;
        try {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);
        } catch (Exception e) {
            if (e instanceof BadCredentialsException) {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            } else {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        } finally {
            AuthenticationContextHolder.clearContext();
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        tokenService.setUserAgent(loginUser);
        Long customerId = loginUser.getUser().getCustomerId();
        Boolean singleClientFlag = SystemConfig.isSingleClientFlag();
        if(customerId != null){
            Customer customer = customerService.selectCustomerById(customerId);
            singleClientFlag = customer.getSingleClientFlag();
            log.info(String.format("客户【%s】单账号登录限制开关:%s", customer.getCode(), singleClientFlag));
        }
        if(singleClientFlag){
            List<SysUserOnline> userOnlineList = userOnlineService.getUserOnlineList(null, username);
            if(CollectionUtils.isNotEmpty(userOnlineList)){
                if(forceLogoutFlag != null && forceLogoutFlag){
                    // 踢掉其他使用该账号登陆的客户端
                    userOnlineService.forceLogoutBySysUserOnlineList(userOnlineList);
                }else{
                    throw new ServiceException("【" + username + "】已登录,是否仍然登陆", 400);
                }
            }
        }
        // 生成token
        tokenService.createToken(iClientDetails, loginUser, uuid);
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        recordLoginInfo(loginUser.getUserId());
    }

②刷新token方式实现类

/**
 * @author lichenhao
 * @date 2023/2/8 17:35
 */
@Component
public class RefreshTokenGranter implements ITokenGranter {

    public static final String GRANT_TYPE = "refresh_token";

    @Autowired
    private TokenService tokenService;

    @Override
    public AjaxResult grant(LoginBody loginBody) {
        tokenService.refreshToken();
        return AjaxResult.success();
    }
}

 2、token相关操作:setCookie

①createToken

    /**
     * 创建令牌
     * 注意:access_token和refresh_token 使用同一个tokenId
     */
    public void createToken(IClientDetails clientDetails, LoginUser loginUser, String tokenId) {

        if(loginUser == null){
            throw new ForbiddenException("用户信息无效,请重新登陆!");
        }

        loginUser.setTokenId(tokenId);

        String username = loginUser.getUsername();
        String clientId = clientDetails.getClientId();

        // 设置jwt要携带的用户信息
        Map<String, Object> claimsMap = new HashMap<>();
        initClaimsMap(claimsMap, loginUser);

        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        int accessTokenValidity = clientDetails.getAccessTokenValidity();
        long accessTokenExpMillis = nowMillis + accessTokenValidity * MILLIS_SECOND;
        Date accessTokenExpDate = new Date(accessTokenExpMillis);
        String accessToken = createJwtToken(SecureConstant.ACCESS_TOKEN, accessTokenExpDate, now, JWT_TOKEN_SECRET, claimsMap, clientId, tokenId, username);

        int refreshTokenValidity = clientDetails.getRefreshTokenValidity();
        long refreshTokenExpMillis = nowMillis + refreshTokenValidity * MILLIS_SECOND;
        Date refreshTokenExpDate = new Date(refreshTokenExpMillis);
        String refreshToken = createJwtToken(SecureConstant.REFRESH_TOKEN, refreshTokenExpDate, now, JWT_REFRESH_TOKEN_SECRET, claimsMap, clientId, tokenId, username);

        // 写入cookie中
        HttpServletResponse response = ServletUtils.getResponse();
        WebUtil.setCookie(response, SecureConstant.ACCESS_TOKEN, accessToken, accessTokenValidity);
        WebUtil.setCookie(response, SecureConstant.REFRESH_TOKEN, refreshToken, refreshTokenValidity);

        //插入缓存(过期时间为最长过期时间=refresh_token的过期时间 理论上,保持操作的情况下,一直会被刷新)
        loginUser.setLoginTime(nowMillis);
        loginUser.setExpireTime(refreshTokenExpMillis);
        updateUserCache(loginUser);
    }

    private void initClaimsMap(Map<String, Object> claims, LoginUser loginUser) {
        // 添加jwt自定义参数
    }

    /**
     * 生成jwt token
     *
     * @param jwtTokenType token类型:access_token、refresh_token
     * @param expDate      token过期日期
     * @param now          当前日期
     * @param signKey      签名key
     * @param claimsMap    jwt自定义信息(可携带额外的用户信息)
     * @param clientId     应用id
     * @param tokenId      token的唯一标识(建议同一组 access_token、refresh_token 使用一个)
     * @param subject      jwt下发的用户标识
     * @return token字符串
     */
    private String createJwtToken(String jwtTokenType, Date expDate, Date now, String signKey, Map<String, Object> claimsMap, String clientId, String tokenId, String subject) {

        JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
                .setId(tokenId)
                .setSubject(subject)
                .signWith(SignatureAlgorithm.HS512, signKey);

        //设置JWT参数(user维度)
        claimsMap.forEach(jwtBuilder::claim);

        //设置应用id
        jwtBuilder.claim(SecureConstant.CLAIMS_CLIENT_ID, clientId);

        //设置token type
        jwtBuilder.claim(SecureConstant.CLAIMS_TOKEN_TYPE, jwtTokenType);

        //添加Token过期时间
        jwtBuilder.setExpiration(expDate).setNotBefore(now);
        return jwtBuilder.compact();
    }

    /*
     * 更新缓存中的用户信息
     * */
    public void updateUserCache(LoginUser loginUser) {
        // 根据tokenId将loginUser缓存
        String userKey = getTokenKey(loginUser.getTokenId());
        redisService.setCacheObject(userKey, loginUser, parseIntByLong(loginUser.getExpireTime() - loginUser.getLoginTime()), TimeUnit.MILLISECONDS);
    }

    private String getTokenKey(String uuid) {
        return "login_tokens:" + uuid;
    }

②refreshToken

    /**
     * 刷新令牌有效期
     */
    public void refreshToken() {
        // 从cookie中拿到refreshToken
        String refreshToken = WebUtil.getCookieVal(ServletUtils.getRequest(), SecureConstant.REFRESH_TOKEN);
        if (StringUtils.isBlank(refreshToken)) {
            throw new ForbiddenException("认证失败!");
        }
        // 验证 refreshToken 是否有效
        Claims claims = parseToken(refreshToken, JWT_REFRESH_TOKEN_SECRET);
        if (claims == null) {
            throw new ForbiddenException("认证失败!");
        }
        String clientId = StringUtils.toStr(claims.get(SecureConstant.CLAIMS_CLIENT_ID));
        String tokenId = claims.getId();
        LoginUser loginUser = getLoginUserByTokenId(tokenId);
        if(loginUser == null){
            throw new ForbiddenException("用户信息无效,请重新登陆!");
        }
        IClientDetails clientDetails = getClientDetailsService().loadClientByClientId(clientId);
        // 删除原token缓存
        delLoginUserCache(tokenId);
        // 重新生成token
        createToken(clientDetails, loginUser, IdUtils.simpleUUID());
    }

    /**
     * 根据tokenId获取用户信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUserByTokenId(String tokenId) {
        String userKey = getTokenKey(tokenId);
        LoginUser user = redisService.getCacheObject(userKey);
        return user;
    }

    /**
     * 删除用户缓存
     */
    public void delLoginUserCache(String tokenId) {
        if (StringUtils.isNotEmpty(tokenId)) {
            String userKey = getTokenKey(tokenId);
            redisService.deleteObject(userKey);
        }
    }

③异常码

 401:access_token无效,开始刷新token逻辑

403:refresh_token无效,或者其他需要跳转登录页面的场景

二、前端(vue3+axios)

// 创建axios实例
const service = axios.create({
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    baseURL: import.meta.env.VITE_APP_BASE_API,
    // 超时
    timeout: 120000,
    withCredentials: true
})

// request拦截器
service.interceptors.request.use(config => {
    // do something
    return config
}, error => {

})


// 响应拦截器
service.interceptors.response.use(res => {
        loadingInstance?.close()
        loadingInstance = null
        // 未设置状态码则默认成功状态
        const code = res.data.code || 200;
        // 获取错误信息
        const msg = errorCode[code] || res.data.msg || errorCode['default']
        if (code === 500) {
            ElMessage({message: msg, type: 'error'})
            return Promise.reject(new Error(msg))
        } else if (code === 401) {
            return refreshFun(res.config);
        } else if (code === 601) {
            ElMessage({message: msg, type: 'warning'})
            return Promise.reject(new Error(msg))
        } else if (code == 400) {
            // 需要用户confirm是否强制登陆
            return Promise.resolve(res.data)
        } else if (code !== 200) {
            ElNotification.error({title: msg})
            return Promise.reject('error')
        } else {
            return Promise.resolve(res.request.responseType === 'blob' ? res : res.data)
        }
    },
    error => {
        loadingInstance?.close()
        loadingInstance = null
        if (error.response.status == 401) {
            return refreshFun(error.config);
        }
        let {message} = error;
        if (message == "Network Error") {
            message = "后端接口连接异常";
        } else if (message.includes("timeout")) {
            message = "系统接口请求超时";
        } else {
            message = error.response.data ? error.response.data.msg : 'message'
        }
        ElMessage({message: message, type: 'error', duration: 5 * 1000})
        return Promise.reject(error)
    }
)

// 正在刷新标识,避免重复刷新
let refreshing = false;
// 请求等待队列
let waitQueue = [];

function refreshFun(config) {
    if (refreshing == false) {
        refreshing = true;
        return useUserStore().refreshToken().then(() => {
            waitQueue.forEach(callback => callback()); // 已成功刷新token,队列中的所有请求重试
            waitQueue = [];
            refreshing = false;
            return service(config)
        }).catch((err) => {
            waitQueue = [];
            refreshing = false;
            if (err.response) {
                if (err.response.status === 403) {
                    ElMessageBox.confirm('登录状态已过期(认证失败),您可以继续留在该页面,或者重新登录', '系统提示', {
                        confirmButtonText: '重新登录',
                        cancelButtonText: '取消',
                        type: 'warning'
                    }).then(() => {
                        useUserStore().logoutClear();
                        router.push(`/login`);
                    }).catch(() => {

                    });
                    return Promise.reject()
                } else {
                    console.log('err:' + (err.response && err.response.data.msg) ? err.response.data.msg : err)
                }
            } else {
                ElMessage({
                    message: err.message,
                    type: 'error',
                    duration: 5 * 1000
                })
            }
        })
    } else {
        // 正在刷新token,返回未执行resolve的Promise,刷新token执行回调
        return new Promise((resolve => {
            waitQueue.push(() => {
                resolve(service(config))
            })
        }))
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VS Code 配置cmake(Linux环境)

通过sudo apt install cmake在linux上安装cmake 在Vs Code中安装这两个插件 通过命令whereis cmake获取linux中cmake的路径信息 右键CMake Tools右下角齿轮标志&#xff0c;选择扩展设置&#xff08;Extension Settings&#xff09; 注意要设置的是本地&#xff0c;还是远程连接…

如何在FastAPI服务器中添加黑名单和白名单实现IP访问控制

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 添加黑名单功能步骤1:安装依赖步骤2:创建FastAPI应用步骤3:添加黑名单📝 添加白名单功能步骤1:创建白名单列表步骤2:添加白名单检查⚓️ 相关链接 ⚓️📖 介绍 📖 在现代网络应用开发中,为了增强…

(9)农作物喷雾器

文章目录 前言 1 必要的硬件 2 启用喷雾器 3 配置水泵 4 参数说明 前言 Copter 包括对农作物喷雾器的支持。该功能允许自动驾驶仪连接到一个 PWM 操作的泵和&#xff08;可选&#xff09;旋转器&#xff0c;根据飞行器速度控制液体肥料的流动速度。 稍微过时的视频显示了…

【PB案例学习笔记】-24创建一个窗口图形菜单

写在前面 这是PB案例学习笔记系列文章的第24篇&#xff0c;该系列文章适合具有一定PB基础的读者。 通过一个个由浅入深的编程实战案例学习&#xff0c;提高编程技巧&#xff0c;以保证小伙伴们能应付公司的各种开发需求。 文章中设计到的源码&#xff0c;小凡都上传到了gite…

第一百二十九节 Java面向对象设计 - Java枚举比较

Java面向对象设计 - Java枚举比较 您可以通过三种方式比较两个枚举常量&#xff1a; 使用Enum类的compareTo()方法使用Enum类的equals()方法使用运算符 Enum类的compareTo()方法比较同一枚举类型的两个枚举常量。它返回两个枚举常量的序数差。如果两个枚举常量相同&#xff0…

《山西化工》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答 问&#xff1a;《山西化工》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的第一批认定学术期刊。 问&#xff1a;《山西化工》级别&#xff1f; 答&#xff1a;省级。主办单位&#xff1a;山西省工业和信息化厅 主管单位&#xff1a;山…

基于SaaS平台的iHRM管理系统测试学习

目录 目录 1、登录模块 2、员工管理模块 3、Postmannewman软件的安装&#xff0c;学习 1、Postman的使用 2、Postman断言 3、全局变量和环境变量 4、请求时间戳 5、Postman关联 6、批量执行测试用例 7、Postman生成测试报告 8、Postman读取外部数据文件&#xff08…

Java——IO流(一)-(7/8):字节流-FileOutputStream、字节流完成文件拷贝

目录 文件字节输出流&#xff1a;写字节出去 构造器及常用方法 实例演示 案例&#xff1a;文件复制 过程分析 复制照片 复制文件 文件字节输出流&#xff1a;写字节出去 FileOutputStream&#xff08;文件字节输出流&#xff09; 作用&#xff1a;以内存为基准&#x…

如何提高pcdn技术的传输效率?

提高PCDN技术的传输效率是一个复杂且多层面的任务&#xff0c;涉及多个关键策略和方法的结合。以下是一些具体的建议和措施&#xff0c;有助于提升PCDN技术的传输效率&#xff1a; 一&#xff0e;优化缓存策略&#xff1a; 精准定位热点内容&#xff0c;优先将这部分内容缓存…

《数字图像处理》实验报告四

一、实验任务与要求 对 Fig0403.tif 进行傅里叶变换并显示其频谱图像&#xff1b;fft2(x) 对 Fig0405.tif 图像进行填充和非填充的高斯滤波&#xff0c;并观察其不同&#xff1b;paddedsize&#xff0c;fft2&#xff08;x,m,n&#xff09; 由 sobel 空间滤波算子生成相应的频率…

小柴冲刺嵌入式系统设计师系列总目录

工作两年 逐渐意识到基础知识的重要性✌️ 意识到掌握了这个证书好像就已经掌握了80%工作中用到的知识了。剩下的就在工作的实战中学习 来和小柴一起冲刺软考吧&#xff01;加油&#x1f61c; 【小柴冲刺软考中级嵌入式系统设计师系列】总目录 前言 专栏目标&#xff1a;冲刺…

最新国内首码对接app平台汇总,一手项目资源!

在当前激烈的移动应用市场竞争环境下&#xff0c;有效推广首次代码App项目变得至关重要。文章将探讨一些推广首次代码App项目的策略和适用的推广渠道&#xff0c;助于开发者获取更多流量和用户关注。 选择可靠的平台来进行推广。 在推广首码App项目之前&#xff0c;首先要考虑…

lmdeploy部署chatglm3模型并对话

lmdeploy部署chatglm3模型并对话 环境准备创建虚拟环境安装组件下载模型 chat启动模型并对话启动成api_server服务并对话启动成gradio服务 环境准备 使用30% A100 来运行chatglm3模型&#xff0c;采用lmdeploy来启动。 创建虚拟环境 # 创建虚拟环境 conda create -n langcha…

如何利用AI工具高效写作?

利用AI工具进行高效写作已经成为许多人的选择&#xff0c;因为它们能够帮助用户节省时间、提高效率&#xff0c;并在一定程度上保证写作质量。下面小编就和大家分享的一些具体的步骤和建议&#xff0c;帮助大家更好地利用AI工具进行写作。 1.选择合适的AI写作工具 根据自己的写…

以敏感数据保护为中心,建立健全高校数据安全治理体系

教育行业数据安全事件频发 2023年8月&#xff0c;南昌某高校3万余条师生个人信息数据在境外互联网上被公开售卖&#xff0c;该校受到责令改正、警告并处80万元人民币罚款的处罚&#xff0c;主要责任人被罚款5万元人民币。2023 年 7月&#xff0c;中国人民大学一名毕业生马某某…

BarTender版软件下载及安装教程

​根据行业数据显示强大的配套应用软件甚至能够管理系统安全性、网络打印功能、文档发布、打印作业记录等&#xff0c;为满足不同的需要和预算&#xff0c;BarTender 提供四个版本&#xff0c;每个都拥有卓越的功能和特性。根据软件大数据显示多国语言支持&#xff1a;轻松设计…

微信小程序-人脸核身解决方案

微信小程序-人脸核身解决方案 名词解释 由于不同公司对于 人脸识别的用词不一致&#xff0c;微信小程序背靠腾讯&#xff0c;因此以下的名词主要采集于腾讯云的解释 人脸识别&#xff1a; 主要关注人脸的检测、分析、比对等技术层面&#xff0c;侧重于识别个体身份的技术实现。…

【SSM】医疗健康平台-管理端-统计分析

知识目标 了解ECharts&#xff0c;能够说出ECharts的作用 掌握会员数量统计的实现&#xff0c;能够使用Echarts绘制会员数量统计图形报表 掌握套餐预约占比统计的实现&#xff0c;能够使用Echarts绘制套餐预约占比统计图形报表 掌握运营数据报表的实现 通过对数据进行统计…

Games101 透视投影矩阵推导

目录 齐次坐标 透视投影 透视投影的四棱锥体挤压为正交投影的长方体 变换规定 转换过程 观察1 观察2 关于任意一点挤压后向哪里移动的问题&#xff0c;简单推导了一下 齐次坐标 如下&#xff0c;(x, y, z, 1) 表示空间中的xyz点&#xff0c;让它每个分量乘以k&#…

使用Hugging Face获取BERT预训练模型

【图书推荐】《从零开始大模型开发与微调&#xff1a;基于PyTorch与ChatGLM》_《从零开始大模型开发与微调:基于pytorch与chatglm》-CSDN博客 BERT是一个预训练模型&#xff0c;其基本架构和存档都有相应的服务公司提供下载服务&#xff0c;而Hugging Face是一家目前专门免费提…