SpringSecurity学习(四)密码加密、RememberMe记住我
文章目录
- 密码加密
- 一、简介
- 密码为什么要加密
- 常见的加密解决方案
- PasswordEncoder详解
- DelegatingPasswordEncoder
- 二、自定义加密方式
- 1. 使用灵活的密码加密方案(BCryptPasswordEncoder)
- 加密
- 验证(推荐)需要在密码前指定加密类型`{bcrypt}`
- 2. 使用固定的密码加密方案(BCryptPasswordEncoder)
- 三、密码自动升级
- 实现UserDetailsPasswordService接口的updatePassword方法
- RememberMe记住我
- 一、简介
- 二、基本应用
- 三、原理分析
- RememberMeServices
- 默认使用TokenBasedRememberMeService
- 总结:
- RememberMe流程图
- 四、持久化令牌
- 实现一:需要手动创建数据库表结构
- 实现二:这种方式可以自动创建数据库表结构
- 五、前后端分离开发记住我
- 编写自定义的MyPersistentTokenBasedRememberMeServices
- 修改SecurityConfig配置
- 附:
密码加密
一、简介
密码为什么要加密
密码泄露,多个网站用同一密码。salt加盐。
常见的加密解决方案
- Hash算法:
最早使用类似SHA-256、SHA-512、MD5这样的单向Hash算法。用户注册后,数据库保存加密后的字串,当用户输入密码时,进行加密比对。
但是由于彩虹表攻击以及硬件发展,计算机每秒执行数十亿次hash计算,这意味着及时密码加密加盐也不安全。 - 单项自适应函数:
在SpringSecurity中,主推单向自适应函数。这种自适应单向函数在进行密码比对时,会有意占用大量系统资源(CPU、内存等),增加恶意用户攻击难度。
可以通过bcrypt、PBKDF2、sCrypt、argon2来体验这种自适应单项函数加密。

PasswordEncoder详解
通过对认证流程分析,实际的密码比较是由PasswordEncoder完成的,因此只需要使用PasswordEncoder不同的实现来完成的。
public interface PasswordEncoder {// 用来进行明文加密String encode(CharSequence rawPassword);// 用来比较密码boolean matches(CharSequence rawPassword, String encodedPassword);// 用来对密码进行升级default boolean upgradeEncoding(String encodedPassword) {return false;}
}
默认提供的加密算法如下:


DelegatingPasswordEncoder
在SpringSecurity后,默认的密码加密方式为DelegatingPasswordEncoder(一个代理类,而不是加密方案)。主要用来代理上面不同的加密方案。为什么不直接使用加密方案,而采用代理类?
- 兼容性:使用DelegatingPasswordEncoder可以帮助使用旧密码加密方式的系统顺利迁移到SpringSecurity。允许在同一个系统中同时存在不同加密方案。
- 便捷性:密码存储方案是可以变化的。使用DelegatingPasswordEncoder作为默认密码加密方案,在需要修改加密方案时,只需要做出小部分改动。
通过源码分析可以知道,如果在工厂类中制定了PasswordEncoder,就会使用PasswordEncoder,否则默认使用DelegatingPasswordEncoder。
SecurityConfigure -> AuthenticationManager -> PasswordEncoder -> DelegatingPasswordEncoder -> PasswordEncoderFactory
二、自定义加密方式
1. 使用灵活的密码加密方案(BCryptPasswordEncoder)
加密
@Test
void test_bcrypt_security() {BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);String encode = bCryptPasswordEncoder.encode("123");System.out.println("encode = " + encode);
}
验证(推荐)需要在密码前指定加密类型{bcrypt}
// 使用PasswordEncoder的第一种方式
@Bean
public UserDetailsService userDetailsService() {InMemoryUserDetailsManager memoryUserDetailsManager = new InMemoryUserDetailsManager();memoryUserDetailsManager.createUser(User.withUsername("whx").password("{bcrypt}$2a$10$DAmuV68SQcVTIXf9Pvb3kerV6KSmX6sNNV/o9LUoejrC0A21Bw/m.").roles("admin").build());return memoryUserDetailsManager;
}
2. 使用固定的密码加密方案(BCryptPasswordEncoder)
// 使用PasswordEncoder的第二种方式
@Bean
public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder(10);
}
@Bean
public UserDetailsService userDetailsService() {InMemoryUserDetailsManager memoryUserDetailsManager = new InMemoryUserDetailsManager();memoryUserDetailsManager.createUser(User.withUsername("whx").password("$2a$10$DAmuV68SQcVTIXf9Pvb3kerV6KSmX6sNNV/o9LUoejrC0A21Bw/m.").roles("admin").build());return memoryUserDetailsManager;
}
三、密码自动升级
实现UserDetailsPasswordService接口的updatePassword方法
要实现密码的自动升级,我们只需要实现UserDetailsPasswordService接口中的updatePassword方法即可。
@Component
public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.getUserByUname(username);if (ObjectUtils.isEmpty(user)) {throw new UsernameNotFoundException("用户名不正确");}List<Role> roles = userMapper.getRolesByUid(user.getId());user.setRoles(roles);return user;}@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {User param = (User) user;Integer result = userMapper.updateUnameByName(user.getUsername(), newPassword);if (result == 1) {param.setPassword(newPassword);}return param;}
}
RememberMe记住我
一、简介
RememberMe记住我。是一种服务端的行为,而不是将用户密码保存在cookie中。传统登录方式是基于Session的,这样一旦用户关闭浏览器重开,就需要再次登录,太过麻烦。
实现思路是通过cookie来记住当前用户身份。当用户登录成功后,通过算法,将用户信息、时间戳加密后通过响应头带回前端存到cookie。当重开浏览器后,会自动将cookie信息发送给服务器进行校验分析。从而确定用户身份。具有时效性,一般的为一周左右。
二、基本应用
- 后端开启记住我功能
http.rememberMe();
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated();http.formLogin();// 开启记住我功能http.rememberMe();http.csrf().disable();return http.build();
}
- 前端自定义页面
<p><input type="checkbox" name="remember-me">记住我</p>
<!-- 参数:remember-me:on -->
- 测试
配置session过期时间1分钟。
登陆时勾选记住我选项,然后重启服务端,就可以在测试接口中免登录。
server:servlet:session:timeout: 1
三、原理分析
1. 求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没值的话表示用户尚未登录,此时调用autoLogin 方法进行自动登录。
2. 当自动登录成功后返回的rememberMeAuth 不为null 时,表示自动登录成功,此时调用authenticate方法对 key 进行校验,并且将登录成功的用户信息保存到SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含RememberMeServices 中的 1oginSuccess 方法。
3. 如果自动登录失败,则调用 remenberMeServices.loginFail方法处理登录失败回调.onUnsuccessfulAuthentication 和onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现这就是RememberMeAuthenticationFilter 过滤器所做的事情,成功将RememberMeServices的服务集成进来。
RememberMeServices
public interface RememberMeServices {// 从请求中获取参数,完成自动登录Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);// 自动登录失败的回调void loginFail(HttpServletRequest request, HttpServletResponse response);// 自动登录成功的回调void loginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication);
}

默认使用TokenBasedRememberMeService
processAutoLoginCookie方法用来验证 Cookie 中的令牌信息是否合法:
- 首先判断 cookieTokens 长度是否为3,格式不对,则直接抛出异常
- 从cookieTokens 数组中提取出第1项(过期时间),判断令牌是否过期,过期则抛出异常。
- 根据用户名(cookieTokens[1])查询出当前用户对象
- 调用 makeTokenSignature 方法生成一个签名,签名的生成过程如下:将用户名、令牌过期时间、用户密码以及 key组成一个宇符串,中间用
:隔开,通过MD5进行加密,并将加密结果转为一个字符串返回。 - 判断第4步生成的签名和通过 Cookie 传来的签名是否相等(cookieTokens[1]),相等表示令牌合法,则直接返回用户对象,否则抛出异常.
1. onLoginSuccess回调中,首先获取用户经和密码信息,如果登录成功后用户名密码从successfulAuthentication对象中擦除,则从数据库中重新加载。
2. 计算出令牌的过期时间,令牌默认有效期是两周。
3. 根据令牌的过期时间、用户名以及用户密码,计算签名
4. 调用 setCookie 方法设置 Cookie.。参数按顺序是用户名、过期时间以及签名,在setCookie 方法中会将数组转为字符串,并进行 Base64编码后响应给前端。
总结:

RememberMe流程图

四、持久化令牌
实现一:需要手动创建数据库表结构

@EnableWebSecurity
public class SecurityConfig2 {@Autowiredprivate MyUserDetailsService myUserDetailsService;@Autowiredprivate DataSource dataSource;@Autowiredprivate JdbcTemplate jdbcTemplate;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated();http.formLogin();// 开启记住我功能http.rememberMe()// 是否总是记住我.alwaysRemember(true)// 指定rememberme的实现.rememberMeServices(rememberMeServices());http.csrf().disable();return http.build();}@Beanpublic RememberMeServices rememberMeServices() {JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);// 启动时候创建表结构tokenRepository.setCreateTableOnStartup(true);return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),myUserDetailsService, tokenRepository);}
}
实现二:这种方式可以自动创建数据库表结构
@EnableWebSecurity
public class SecurityConfig2 {@Autowiredprivate MyUserDetailsService myUserDetailsService;@Autowiredprivate DataSource dataSource;@Autowiredprivate JdbcTemplate jdbcTemplate;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated();http.formLogin();// 开启记住我功能http.rememberMe()// 是否总是记住我.alwaysRemember(true)// 指定rememberme的实现.tokenRepository(persistentTokenRepository());http.csrf().disable();return http.build();}@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();List<Map<String, Object>> result = jdbcTemplate.queryForList(" select 1 from information_schema.tables where table_name = 'persistent_logins' ");// 这里一直为true在后续启动时会报错,所以我们在启动之前先查询表结构是否存在,如果存在就不创建repository.setCreateTableOnStartup(result.isEmpty());repository.setDataSource(dataSource);return repository;}
}
五、前后端分离开发记住我
cookie实现:
- 认证成功保存记住我cookie到客户端
- 只有cookie写入客户端成功才能实现自动登录功能
编写自定义的MyPersistentTokenBasedRememberMeServices
public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {@Overrideprotected boolean rememberMeRequested(HttpServletRequest request, String parameter) {// 这里可以在LoginFilter中读取出来,保存到request中。String paramValue = String.valueOf(request.getAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER));// 也可以在这里获取
// try {
// Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
// String rememberVal = String.valueOf(userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER));
// } catch (IOException e) {
// e.printStackTrace();
// }if (paramValue != null) {if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {return true;}}return false;}public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {super(key, userDetailsService, tokenRepository);}
}
修改SecurityConfig配置
这里只展示关键部分代码,详细参考附录一说明。
/*** @author Huathy* @date 2023-03-06 23:07* @description*/
// 开启web安全
@EnableWebSecurity
@Slf4j
public class SecurityConfig {@Autowiredprivate MyUserDetailsService myUserDetailsService;@Autowiredprivate AuthenticationConfiguration authenticationConfiguration;@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();return authenticationManager;}@Beanpublic LoginVcFilter loginVcFilter() throws Exception {log.info(" === loginFilter init ===");LoginVcFilter loginVcFilter = new LoginVcFilter();// 2. 指定认证处理URLloginVcFilter.setFilterProcessesUrl("/dologin");// 设置认证成功时使用自定义的记住我功能loginVcFilter.setRememberMeServices(rememberMeServices());return loginVcFilter;}@Beanprotected SecurityFilterChain configure(HttpSecurity http) throws Exception {log.info(" === 替换了 loginVcFilter === ");http.addFilterAt(loginVcFilter(), UsernamePasswordAuthenticationFilter.class);// 开启记住我 这里是设置携带记住我cookie的处理http.rememberMe().rememberMeServices(rememberMeServices());return http.build();}@Beanpublic RememberMeServices rememberMeServices() {return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), myUserDetailsService, new InMemoryTokenRepositoryImpl());}
}
附:
- 本文所涉及源码地址:https://gitee.com/huathy/study-all
相关文章:
SpringSecurity学习(四)密码加密、RememberMe记住我
文章目录密码加密一、简介密码为什么要加密常见的加密解决方案PasswordEncoder详解DelegatingPasswordEncoder二、自定义加密方式1. 使用灵活的密码加密方案(BCryptPasswordEncoder)加密验证(推荐)需要在密码前指定加密类型{bcryp…...
vue专项练习
一、循环实现一个列表的展示及删除功能 1.1 列表展示 1、背景: 完成一个这样的列表展示。使用v-for 循环功能 id接口名称测试人员项目名项目ID描述信息创建时间用例数1首页喵酱发财项目a1case的描述信息2019/11/6 14:50:30102个人中心张三发财项目a1case的描述信…...
【笔试题】百度+美团
发工资 链接:https://www.nowcoder.com/questionTerminal/e47cffeef25d43e3b16c11c9b28ac7e8 来源:牛客网 小度新聘请了一名员工牛牛, 每个月小度需要给牛牛至少发放m元工资(给牛牛发放的工资可以等于m元或者大于m元, 不能低于m)。 小度有一些钞票资金…...
【8.索引篇】
索引分类 索引和数据就是位于存储引擎中: 按「数据结构」分类:Btree索引、Hash索引、Full-text索引。按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。按「字段特性」分类&#…...
MySQL InnoDB存储引擎锁与事务实现原理解析(未完成)
InnoDB MySQL存储引擎是基于表的,也就是说每张表可以选择不同的存储引擎。 InnoDB存储引擎的表是索引组织的,也就是数据即索引。 存储引擎文件 InnoDB引擎会包含RedoLog重做日志文件和TableSpace表空间文件。 表空间文件 默认表空间文件(…...
P1683 入门(洛谷)JAVA
题目描述: 不是任何人都可以进入桃花岛的,黄药师最讨厌像郭靖一样呆头呆脑的人。所以,他在桃花岛的唯一入口处修了一条小路,这条小路全部用正方形瓷砖铺设而成。有的瓷砖可以踩,我们认为是安全的,而有的瓷砖…...
yocto编译烧录和脚本解析
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言一、初始化构建目录二、imx-setup-release.sh脚本解析三、编译单独编译内核四、烧录总结前言 本篇文章主要讲解如何在下载好源码之后进行编译和yocto的脚本解析…...
Proteus 8.15安装包安装教程
Proteus介绍Proteus的介绍Proteus8.15安装包Proteus8.15安装教程Proteus的介绍 Proteus是英国著名的EDA工具(仿真软件),从原理图布图、代码调试到单片机与外围电路协同仿真,一键切换到PCB设计,真正实现了从概念到产品的完整设计。是世界上唯…...
Spring——AOP工作流程
AOP就是代理模式的开发简化 1.Spring容器启动 因为AOP是要将通知类作为一个bean对象交给spring进行管理的,还有经过通知类被增强的类。 此时还没有创建bean对象 2.读取所有切面配置中的切入点 在下面这段代码中,定义了两个切入点,但是只…...
c++11多线程之condition_variable、wait()、notify_one()、notify_all()的使用。
系列文章目录 文章目录系列文章目录前言一、基本概念1.1 std::condition_variable1.2 wait()函数1.2.1 wait()带第二个参数1.2.2 wait()不带第二个参数1.2.3 当其他线程用notify_one()或notify_all()1.3 notify函数二、代码实例总结前言 C11多线程&…...
skywalking扩展实现 —— 监控数据的动态上报
把标题名整高大上一些,来掩盖需求的奇葩。 0. 目录1. 需求背景2. 需求描述3. 优势4. 实现4.1 扩展点4.2 配置项5. 优化6. 提醒7. 补充 - 关于微服务8. 参考1. 需求背景 过去一段时间,接手了一个迭代了数年的"基于微服务架构"搭建的产品。 自…...
【GoF 23】23种设计模式与OOP七大原则概述
1. 什么是GoF 23? GoF 23也就是23种设计模式。1995年GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,一共收录了23种设计模式,从此梳理了软件设计模式领…...
Java 日期时间
Java 日期时间是 Java 标准库中一个非常重要的部分,它提供了丰富的 API 来处理日期、时间以及日期时间。在 Java 应用程序中,我们经常需要处理日期时间相关的操作,例如计算两个日期之间的差、将日期时间转换为不同的时区等。在本篇文章中&…...
Face Forgery Suvery
文章目录Face ForgeryFace Forgery classAttribute ManipulationExpression SwapIdentity SwapEntire Face SynthesisFace Forgery DetectionLow-levelOn the Detection of Digital Face Manipulation(CVPR2020)High-levelProtecting World Leaders Against Deep FakesDetectin…...
案例学习--016 消息队列作用和意义
简介MQ全称为Message Queue, 是一种分布式应用程序的的通信方法,它是消费-生产者模型的一个典型的代表,producer往消息队列中不断写入消息,而另一端consumer则可以读取或者订阅队列中的消息。主要产品有:ActiveMQ、RocketMQ、Rabb…...
【MySQL】MySQL的锁机制
目录 概述 MyISAM 表锁 InnoDB行锁 概述 锁是计算机协调多个进程或线程并发访问某一资源的机制(避免争抢)。 在数据库中,除传统的 计算资源(如 CPU、RAM、I/O 等)的争用以外,数据也是一种供许多用户共…...
HTML 背景
一个富有美感的背景会让站点看上去更加高级、更有吸引力。本篇为大家来的是 HTML 背景相关内容。 背景(Backgrounds) <body> 拥有两个配置背景的标签。背景可以是颜色或者图像。 背景颜色(Bgcolor) 背景颜色属性将背景设…...
Lombok
文章目录简介原理安装常用Getter、SetterToStringEqualsAndHashCodeNonNullNoArgsConstructor、RequiredArgsConstructor、AllArgsConstructorDATABuilderLogvalCleanup简介 Project Lombok is a java library that automatically plugs into your editor and build tools, spi…...
Koa源码学习
前言 koa是一个非常流行的Node.js http框架。本文我们来学习下它的使用和相关源码 来自官网的介绍: Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。…...
一种延迟加载自定义元素的方法
您可能实际上并不需要所有这些;通常有一个更简单的方法。如果有意使用,此处显示的技术可能仍然对您的工具集有用。 为了保持一致性,我们希望我们的自动加载器也成为一个自定义元素——这也意味着我们可以通过 HTML 轻松配置它。但首先&#…...
智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)
宇树机器人多姿态起立控制强化学习框架论文解析 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一) 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...
2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...
Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...
苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会
在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...
【Elasticsearch】Elasticsearch 在大数据生态圈的地位 实践经验
Elasticsearch 在大数据生态圈的地位 & 实践经验 1.Elasticsearch 的优势1.1 Elasticsearch 解决的核心问题1.1.1 传统方案的短板1.1.2 Elasticsearch 的解决方案 1.2 与大数据组件的对比优势1.3 关键优势技术支撑1.4 Elasticsearch 的竞品1.4.1 全文搜索领域1.4.2 日志分析…...
深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏
一、引言 在深度学习中,我们训练出的神经网络往往非常庞大(比如像 ResNet、YOLOv8、Vision Transformer),虽然精度很高,但“太重”了,运行起来很慢,占用内存大,不适合部署到手机、摄…...
