利他才能利己


  • 首页

  • 标签

  • 归档

  • 搜索

Windows+Nginx 部署 Flutter Web

发表于 2021-07-09 | 分类于 Tools , Flutter |

今天的内容比较简单,我们直接进入主题。


创建工程

目前笔者用的 Flutter SDK 是 Stable 2.x,其已经支持 Web了。

使用 Android Studio 创建 Flutter 工程,需要勾选上对 Web 的支持,如下图所示:

如果你之前创建的工程没有勾选该选项,可以在工程目录下执行如下命令:

1
2
3
# flutter create <output directory>
# Create a new Flutter project in the specified directory.
flutter create .
1
2
3
4
5
6
7
8
9
10
Recreating project ....
web\favicon.png (created)
web\icons\Icon-192.png (created)
web\icons\Icon-512.png (created)
web\index.html (created)
web\manifest.json (created)

Wrote 8 files.

All done!

完成之后,在工程目录下就会生成 web 目录。

编译 Web 版本

编译 Web 版本只需要一行命令

1
flutter build web

编译成功后,在工程的 build 目录下面有个 web 目录。

Nginx 配置

在 Nginx 的配置文件中,增加一个 server 配置即可。

1
2
3
4
5
6
7
8
9
10
server {
listen 8083;
server_name localhost;
location / {
# web目录的路径
root \project-path\build\web;
# 默认页面
index index.html index.htm;
}
}

启动 Nginx(关于 Windows 上面操作 Nginx 的命令,可以参考笔者的 减少跨域中的OPTIONS请求) ,然后在浏览器中访问 http://localhost:8083/index.html 即可。


学习犹如爬山,适当的咬紧牙关冲刺一把,就能一览众山小

MySQL主键值被我用完了

发表于 2021-06-12 | 分类于 Server , Tools |

简介

对于关系表,有个很重要的约束,就是任意两条记录不能重复。不能重复是指能够通过某个字段唯一区分出不同的记录,这个字段被称为 主键。

对表的主键要求比较关键的一点是:记录一旦插入到表中,主键最好不要再修改。

选取表的主键一个基本原则是:没有特殊情况,不使用任何业务相关的字段(如证件号码、手机号码等)作为主键。

本篇主要说明下面两个问题:

  • 主键值范围越界了会发生什么?
  • 不设置主键行吗?

看完正文之后,可以在本篇的【小结】部分找到答案。

推荐下面几篇关于 MySQL 的文章:

  • MySQL8-0-15在Win10上的折腾记
  • 微服务-MySQL基本操作
  • 导入MySQL数据库文件

数值类型的边界值

C 标准库 limits.h 中定义了一些数据类型的值的范围、限制值,也就是说该类型的值不可以大于该上界或者小于该类型值的下界。

咱们先看下下面这个 C 语言例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <limits.h>

int main() {
// 65535
printf("unsigned short max value: %u\n", USHRT_MAX);
unsigned short yy = USHRT_MAX;
printf("xx = %hu\n", yy); //65535
printf("xx + 1 = %hu\n", yy+1); //0
printf("xx + 2 = %hu\n", yy+2); //1

printf("unsigned short sizeof: %u\n", sizeof(unsigned short));

return 0;
}

编译运行,可以得到如下结果:

1
2
3
4
5
unsigned short max value: 65535
xx = 65535
xx + 1 = 0
xx + 2 = 1
unsigned short sizeof: 2

从结果来看,无符号的 short 数据类型的最大值是 65535,当然最小值就是 0,即范围是 0 ~ 65535。

在这里,还是要再提一下关于格式化打印里面的一些说明:

  • i% 和 d%,都可以用来表示有符号的十进制整数,一般 ld% 用来表示 long 类型的;

  • o% 用来表示八进制整数,x%(X%)用来表示十六进制整数;

  • 对 short 类型使用 h 前缀,因此 %hd 表示以十进制显示 short 整数,%ho 表示以八进制显示 short 整数;

  • %u 用来表示无符号整数,即 unsigned int 类型数据;

  • h% 和 l% 前缀都可以同 u% 结合使用表示无符号整数。如 %lu 表示打印 unsigned long 类型、%hu 表示打印 unsigned short 类型;

  • 打印 long 数值,%ld 打印 long 数值,%lx 表示以十六进制格式打印长整数,%lo 表示以八进制格式打印长整数;

实践

经过上面的说明之后,接下来我们就可以进入今天的正题了。

创建数据库 play_db

1
2
3
CREATE DATABASE play_db;

USE play_db;

设置主键并自增

创建表 table0

1
CREATE TABLE table0(id SMALLINT unsigned AUTO_INCREMENT PRIMARY KEY, name char(10)) AUTO_INCREMENT=65535;

SMALLINT 是 MySQL 支持的一种数据类型,占用 2 个字节,有符号的取值范围是 (-32 768,32 767),无符号的取值范围是 (0,65 535)。

看下创建的表情况

1
DESC table0;

插入数据

1
INSERT INTO table0 (name) VALUES ('veryitman');

可见,此时 id 默认值已经是 65535了,试图再插入一条数据。

1
INSERT INTO table0 (name) VALUES ('.com');

那么另外一个问题来了,如果我们不设置主键呢?

不设置主键无自增

创建表 table1

TINYINT unsigned,表示范围 0 ~ 255

1
2
3
4
5
CREATE TABLE table1(id TINYINT unsigned, name char(10));

INSERT INTO table1 (id, name) VALUES (255, 'veryitman');

INSERT INTO table1 (id, name) VALUES (256, 'haha');

此时会报错:ERROR 1264 (22003): Out of range value for column 'id' at row 1

如果不设置主键, InnoDB 会自动帮你创建一个不可见的、长度为 6 字节的 row_id,而且这个 row_id 是由 InnoDB 维护全局的 dictsys.row_id,每次插入一条数据时都会让全局 row_id 加一(未定义主键的表会使用全局 row_id 作为主键 id)。

如果全局 row_id 一直涨,直到涨到2的48次方-1时,这时候再加一就会让低 48 位的 row_id 都为 0,此时如果再插入一条数据,它拿到的 row_id 就是 0,这样的话就有可能存在主键冲突的。

所以创建表的时候,最好设置主键。

设置自增但无主键

创建表 table2

1
CREATE TABLE table2(id TINYINT unsigned AUTO_INCREMENT, name char(10)) AUTO_INCREMENT=255;

报错信息如下:

1
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key

错误信息的意思是,只能有一个自增列,它必须被定义为键。

小结

1、在设计数据表时,尽量根据业务需求来选择合适的字段类型。数据库表的自增 ID 达到上限之后,再申请时它的值就不会在改变了,继续插入数据时会导致报主键冲突错误。

一个库或者表数据尽量不要太多,根据自己的业务合适设置即可。如果数据较多要进行分库分表,分布式环境下要注意主键生成问题,做到主键唯一;

2、为了避免一些不必要的麻烦和隐性错误,设计表都应该设置主键。

附录

MySQL支持的数值类型


不积跬步,无以至千里;不积小流,无以成江海。

走上项管之路

发表于 2021-05-23 | 分类于 项目管理 |

简介

在几年前,我还不知道项目管理究竟为何物,而如今自己却与它紧紧相连。

跟大家分享一下自己是如何走上项目管理之路的,希望能对你将来的职业规划有点帮助。

开发之路

本人大学的专业是搞电子信息的。大学的生涯让我学到了很多高深莫测的知识,也让我结识了一帮志同道合的朋友,有幸能和他们一起在实验室里面搞研究。当然大学最重要的是让我获得了文凭,为将来找工作埋下了重要的基础。

我是从 2009 年真正开始接触移动端编程的,那个时候 Android 编程慢慢的在一线城市火了起来,朋友建议我趁现在有时间赶紧学一学:“这可能是将来让你辉煌腾达的首选之路”。听着他中肯的建议,仿佛自己已经走上了人生之颠,比当年哥伦布发现新大陆还 TM 还喜出望外。然鹅我不禁乐极生悲,Android 是啥?

经过一番了解和学习,终于认识了 Android。从 Java 编程到 Android 编程,我开始了自我修炼之路。那个时候我就在想,这个玩意挺好玩的,这辈子我都要学习它,玩转它!现在想想那个时候的自己还是挺理想的一个人,呵呵!

毕业后不久,就有模有样的干起了 Android 开发的勾当,这一干就是 5 年。

从一个意气风发的骚年变成了真正的码农,飘逸的头发也渐渐的稀疏了起来。

直到有一天,遇到一个项目,大佬(项目技术总监)告诉我说:“团队目前没有 iOS 开发的人员,你是否愿意为自己多增加一种技能?”。

看着大佬诚挚的眼神,年少轻狂的我想都没想就答应了。现在回想起自己那种很自信的样子,挺傻的。

从那以后,我便开始了没日没夜的走上了熟悉和学习 iOS 编程之路,苦并快乐着,可能是因为自己的勤奋感动了上苍,很快就上手了 iOS 开发,并做得如火如荼,一发不可收拾。

在 iOS 开发的同时,我并没有放弃对 Android 开发的学习和进步,两手都要抓尽量都要硬。在后续的编程道路里,我越来越发现编程是相通的,只要你肯努力,就没有克服不了的困难。唯一要克服的就是你的懒惰和借口。

慢慢地我又开始接触了 Java 后端编程,移动端游戏开发等工作。唯一遗憾的是没有接触过前端编程,当全栈工程师的梦想也就破灭了。

任何人都有选择的权力,在某个风和日丽的早上我毅然选择要放弃编程之路,准备踏上项目管理之路。

有很多人会说,你是因为年龄的问题才想着转管理路线的吧!

我只能说,年龄只是很小的一部分因素。毕竟这个世界上还有很多事情,我还没有经历过,也想在有生之年换一种求生之路。

项目管理之路

能从编程之路转到项管之路,跟当时的项目有关,对我来说也是一个机会。

走上项管的历程,让我想起了电影《功夫》里面的一个片段:

“唉,小弟,小弟,别走啊”

“哇,不得了,不得了啊!

你有道灵光从天灵盖喷出来,你知道吗?

年纪轻轻的就有一身横练的筋骨,简直百年一见的练武奇才啊!

如果有一天,让你打通任督二脉,那还不风龙上天哪?!

正所谓:我不入地狱,谁入地狱。警恶惩奸,维护世界和平这个任务,就交给你了,好吗?”

“唔”

。。。

当然了,电影中的片段存在一定忽悠的成分。这里想表达的是:当机会来临的时候,做一下对比和衡量,适合就上吧!

我很感谢当初让我转行的大佬,给了我很多专业的建议,让我在这条路上能够有个很好的开始。

在转行后,自己也是积极的学习项目管理知识,虽没有悬梁刺股倒也会闻鸡起舞。

在项目管理的职业生涯中,并非是一帆风顺的,跟编程一样需要不断的学习。让项目成功,是项目管理中的重要使命。

这里要给在座的各位提个醒,项目管理不是让你来颐指气使管人的,而是让你能够把握大局,营造氛围让大家创造一个又一个成功的项目。

在团队中,你应该坚持公平公正的原则,维持项目的可持续健康发展,做好服务大众的角色和义务!

~

今天就到这吧,后续我会分享更多关于自己在项目管理中的实际案例给大家~


任何事都没有表面看起来的那么简单,既然选择了你认为适合自己的就坚持下去!终可见!

AS、VSCode中实时显示每行代码的修改记录

发表于 2021-04-12 | 分类于 Tools |

简介

阅读下面内容开始之前,假设你已经在使用 git 管理自己的代码了。

阅读本篇,可以看到如下内容:

1、如何在 Android Studio 中实时显示每行代码的提交历史记录;

2、如何在 Android Studio 中查看单个文件的修改历史记录;

3、如何在 Visual Studio Code 中实时显示每行代码的提交历史记录;

plugin 之功

工欲善其事必先利其器,在使用 IDE 工具的时候找到适合自己的插件可以让你的工作事半功倍。

大家可以在 在 AS 中安装如下两个插件:

  • Git
  • GitToolBox

安装方式很简单,File/Setttings/Plugins,搜索插件名称直接安装完成重启 AS 即可。

在 VSCode 中安装如下插件:

  • GitLens

使用快捷键 Ctrl + Shift + P,输入 Install Extensions,再搜索 GitLens 安装即可。

安装完成之后,就可以如下分别在 VSCode 和 AS 中的效果了,如下图所示:

VSCode 显示效果:

AS 显示效果:

AS 设置 GitToolBox

可以在 File/Setting 中对 GitToolBox 进行相关的设置,如下图:

AS 查看单文件修改记录

按照下面截图的步骤操作即可查看,例如查看 main.dart 的修改记录。

Show History 后,在打开的页面中即可查看该文件的修改记录。

如果你习惯使用 git 命令行操作,也可以结合 git log --pretty=oneline 文件名 和 git show COMMITID 的方式来查看。

命令行使用 code

我们有时候想在命令行中直接使用 VSCode 打开当前目录,可以安装 code 命令,在 macos 上面使用 CMD+Shift+ P 打开输入框然后输入 shell command,如下图所示。

安装成功后,就可以在命令行中使用 code 命令了

1
2
3
4
5
6
7
8
# 打开 VSCode
code

# 使用 VSCode 打开当前目录
code .

# 使用 VSCode 新建文件
code custom.txt

一味的妥协和让步只会让事情愈发以控制,直至灾难来临~

微服务: Token 相关的重构

发表于 2021-01-17 | 分类于 Server |

简介

在上一篇 微服务-Token的处理 中,写了一个 MSAuthTokenUtil 类,用来生成、刷新、校验 token,该类的方法都是 static 的。后续想了一下,还是将其改为普通的组件较好,在最新代码中对其做了两个较大的重构。

重构为组件

将其改名为 MSAuthTokenHelper,并将其中的所有 static 方法改为实例方法,用注解 @Component 修饰。

1
2
3
4
5
@Slf4j
@Component
public class MSAuthTokenHelper {
///....
}

在所有使用到 MSAuthTokenHelper 的地方,增加对应的自动注入后调用即可。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private MSAuthTokenHelper tokenHelper;

@Autowired
public void setTokenHelper(MSAuthTokenHelper tokenHelper) {
this.tokenHelper = tokenHelper;
}

@Override
public MSResponse refreshUserToken(String token) {
// ...

String refreshToken = tokenHelper.refreshToken(token);

if (null != refreshToken) {
String userID = tokenHelper.userIDfromToken(token);
}

// ...
}

这里顺便提一下,@Controller,@Service,@Repository 以及 @Component 的区别以及联系,如下表所示:

注解 含义
@Component 最普通的组件,可以被注入到spring容器进行管理
@Repository 作用于持久层
@Service 作用于业务逻辑层
@Controller 作用于表现层(spring-mvc的注解)

@Controller,@Service,@Repository 都继承了 @Component 的功能,可以看这几个注解的源码得知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// Repository 注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
@AliasFor(
annotation = Component.class
)
String value() default "";
}

/// Controller 注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}

/// Service 注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}

当一个类被 @Component 所注解,那么就意味着同样可以用 @Repository, @Service, @Controller 来替代它,同时这三个注解会具备有更多的功能,而且功能各异,可以根据自己的需要使用不同的注解来表示不同的业务和逻辑。具体可以参考 Spring/Spring-Boot 学习 @Controller,@Component,@Service,@Repository的异同 这篇文章,写的很清楚了。

从配置文件读取 token 的配置

在之前的文章中,分享过如何通过 SpringBoot 的 @ConfigurationProperties 注解来读取配置文件,可以参考 微服务-ConfigurationProperties配置 这篇文章。

在 properties 文件中,新增如下的配置信息:

1
2
3
4
5
6
7
8
9
# Auth token config
# --------------------------------------------
msconfig.authtoken.claims_jwtsid=restful_api
msconfig.authtoken.claims_subject=admin
msconfig.authtoken.claims_audience=client
#token 过期时间24小时(24 * 60 * 60 * 1000)
msconfig.authtoken.token_expire_time=86400000
#密钥盐
msconfig.authtoken.token_secret=token123

这些配置信息对应的 model 是 MSAuthTokenPropertyConfig,如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Setter
@Getter
@Component
@ConfigurationProperties(prefix = "msconfig.authtoken")
public class MSAuthTokenPropertyConfig {

private String claims_jwtsid;
private String claims_subject;
private String claims_audience;
private long token_expire_time;//token 过期时间24小时
private String token_secret;//密钥盐
}

在 MSAuthTokenHelper直接使用即可,关键代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@Component
public class MSAuthTokenHelper {

private MSAuthTokenPropertyConfig authTokenPropertyConfig;

@Autowired
public void setAuthTokenPropertyConfig(MSAuthTokenPropertyConfig authTokenPropertyConfig) {
this.authTokenPropertyConfig = authTokenPropertyConfig;
}

public String generateToken(String userID) {
String token = "";

long tokenExpireTime = authTokenPropertyConfig.getToken_expire_time();
String jwtsid = authTokenPropertyConfig.getClaims_jwtsid();
String subject = authTokenPropertyConfig.getClaims_subject();
String audience = authTokenPropertyConfig.getClaims_audience();
String tokenSecret = authTokenPropertyConfig.getToken_secret();

//...

return token;
}
}

微服务: Token的处理

发表于 2021-01-03 | 分类于 Server |

简介

在 Win10-安装-Redis 和 微服务-SpringBoot-集成-Redis 分别介绍了如何安装和使用 Redis,今天继续结合 Redis,聊聊 token 授权登录的事情。

今天聊的主角是 JWT,聊完 JWT 之后再结合实例实现用户 token 登录。

JWT 介绍

JWT,JSON Web Token 的缩写,基于 RFC 7519 标准。

下面内容来自 jwd.io,如下:

1
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

JWT 定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息可以被验证和信任(因为它是数字签名的)。

JWT 可应用于但不仅限于下面的几种场景:

1、跨域认证

JWT 是一种比较流行的跨域认证解决方案,JWT 的诞生并不是解决 CSRF 跨域攻击,而是解决跨域认证的难题。

A 网站和 B 网站是同一家公司的关联服务,现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,这应该如何实现呢?客户端保存 Token,每次请求都发回给服务器即可。

2、授权(Authorization)

用户一旦登录成功后,后续用户的每个请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的 JWT 的一个特性,因为它的开销很小,并且可以轻松地跨域使用。授权,是使用 JWT 的最常见的场景之一。

3、信息交换(Information Exchange)

对于安全的在各方之间传输信息而言,JWT 是一种很好的方式。JWT 可以被签名,例如,用公钥/私钥对,可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,还可以验证内容没有被篡改。

可以参考阮一峰老师的 JSON Web Token 入门教程,更多详细的介绍可以参考 jwd.io 的相关资料。

使用 JWT

Spring Boot 集成 jjwt

本文以集成 https://github.com/jwtk/jjwt 为例。如果你有兴趣也可以试着去使用 https://github.com/auth0/java-jwt,它是 JWT 的另一个 Java 实现。

截止到该文发布,在 maven repository 仓库中 jjwt 最新版本是 0.9.1

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

修改了哪些文件

本次涉及修改和新增的文件如下:

  • 【修改】MSUserSigninService.java:登录服务的接口;
  • 【修改】MSUserSigninServiceImpl.java:登录服务的接口实现;
  • 【修改】MSSigninController.java:登录的Controller;
  • 【新增】MSAuthTokenUtil.java:token工具类;
  • 【新增】MSAuthConfigurer.java:token配置管理;
  • 【新增】MSAuthInterceptor.java:自定义拦截器;

具体的实现步骤为:

  • 写 token 工具类,实现 token 的生成,校验等工作即 MSAuthTokenUtil.java;
  • 写自定义拦截器,即 MSAuthInterceptor.java,该类实现了 HandlerInterceptor 接口;
    • 拦截客户端相关的 API 请求,对相关的接口进行token的校验;
    • 有了统一的拦截器不需要在每个 Controller 或者对应的 Service 中去做 token 的判断;
  • 写自定义拦截器的配置管理类即 MSAuthConfigurer.java,该类实现了 WebMvcConfigurer 接口;
  • 增加 token 登录的 API,并实现 Redis 缓存 token 的逻辑;

实例演练

用户登录完成后,根据 userID 生成 token,将 token 保存到 Redis 中按照 userID 为 key 来进行存储的。

MSAuthInterceptor.java 是自定义的拦截器,在该拦截器中获取请求的 token 并进行相关的校验。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Component
public class MSAuthInterceptor implements HandlerInterceptor {
private static final String REQUEST_TOKEN_KEY = "token";

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestMethod = request.getMethod();

if ("OPTIONS".equalsIgnoreCase(requestMethod)) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
// 请求的Header中拿
String token = request.getHeader(REQUEST_TOKEN_KEY);
// Header中拿不到token
if (null == token) {
String[] tokens = request.getParameterValues("token");
if (null != tokens && tokens.length > 0) {
token = tokens[0];
}
}

if (MSAuthTokenUtil.verifyToken(token)) {
return true;
}

PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try {
writer = response.getWriter();
Map<String, Object> result = new HashMap<>(2);
result.put("code", 400);
result.put("msg", "用户令牌token无效");
result.put("data", null);
writer.print(result);
} catch (IOException e) {

} finally {
if (null != writer) {
writer.close();
}
}

return false;
}
}

拦截器的配置在 MSAuthConfigurer.java 中进行管理,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class MSAuthConfigurer implements WebMvcConfigurer {

private MSAuthInterceptor authInterceptor;

public MSAuthConfigurer(MSAuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 如下路径不做拦截
List<String> excludePaths = new ArrayList<>();
excludePaths.add("/signup/**"); //注册
excludePaths.add("/signin/name/**"); //用户名登录
excludePaths.add("/signin/get/token/**"); //获取token
excludePaths.add("/signout/**"); //登出
excludePaths.add("/static/**"); //静态资源
excludePaths.add("/assets/**"); //静态资源

// 除了 excludePaths 外的请求地址都做拦截
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludePaths);

WebMvcConfigurer.super.addInterceptors(registry);
}
}

接下来重点说一下 MSAuthTokenUtil.java 里面如何生成 token 的,MSAuthTokenUtil.java 主要是完成生成、检验、刷新 token 等工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static String generateToken(String userID) {
String token = "";

Date date = new Date();
// 过期时间
Date expireDate = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME);

token = Jwts.builder().setId(JWTSID)
.setSubject(SUBJECT)
.setAudience(AUDIENCE)
.setIssuedAt(date)
.setExpiration(expireDate)
.claim(CLAIMS_USERID, userID)
.signWith(SignatureAlgorithm.HS256, TOKEN_SECRET)
.compact();

log.info("generateToken token: " + token);

return token;
}

根据用户ID 生成 token,其中 claim(CLAIMS_USERID, userID) 是用于自定义字段的,便于解析 token 时获取相关的信息。

当我们调用用户名+密码登录的时候,会生成对应的 token,然后将该 token 保存到 Redis 中。下次调用 token 登录的接口时,会从 Redis 中取出对应的 token 信息进行校对,校对通过就返回成功,否则返回失败无法登录。

在 MSSigninController.java 分别实现了获取 token、刷新 token,token 登录三个接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RequestMapping(value = "/get/token", method = RequestMethod.GET)
@ApiOperation(value = "获取token", httpMethod = "GET", notes = "获取登录")
@ApiImplicitParams({
@ApiImplicitParam(name = "userID", value = "userID", required = true)
})
public MSResponse getToken(@RequestParam(value = "userid") String userID) {
MSResponse response = userSigninService.fetchUserToken(userID);

return response;
}

@RequestMapping(value = "/token", method = RequestMethod.GET)
@ApiOperation(value = "Token登录", httpMethod = "GET", notes = "Token登录")
@ApiImplicitParams({
@ApiImplicitParam(name = "userID", value = "userID", required = true),
@ApiImplicitParam(name = "token", value = "token", required = true)
})
public MSResponse siginWithToken(@RequestParam(value = "userid") String userID, @RequestParam(value = "token") String token) {
MSResponse response = userSigninService.signinUsingToken(userID, token);

return response;
}

@RequestMapping(value = "/refresh/token", method = RequestMethod.GET)
@ApiOperation(value = "刷新Token", httpMethod = "GET", notes = "Token刷新")
@ApiImplicitParams({
@ApiImplicitParam(name = "token", value = "token", required = true)
})
public MSResponse refreshToken(@RequestParam(value = "token") String token) {
MSResponse response = userSigninService.refreshUserToken(token);

return response;
}

为了方便使用了 GET 方式进行网络请求。后续可以改为 POST 请求。

登录逻辑都在 MSUserSigninServiceImpl.java 中,大家可以自行去看源码,这里不再赘述。

API 调用效果

启动 MySQL,启动 Redis,再启动项目即可。

用户登录成功后,调用 /get/token API,如下:

调用 /token 进行登录的 API,如下:

调用 refresh/token API 如下:

待办事项

  • token 配置信息放置到配置文件中;2021.01.17 Done 微服务-Token-相关的重构;
  • Redis 中设置 token 的过期时间;
  • 调用刷新 token 的 API 后更新 Redis 中 token 的有效时间;
  • 刷新 token、使用 token 登录的 API 修改为 POST 方式;
  • Token 的加密,减少 Token 登录的数据库查询次数;

只有弱者才去争取公平,这句话虽然残忍但很现实~

解决bash profile不生效的方法

发表于 2020-12-13 | 分类于 Tools , MacOS |

我喜欢把 macOS 上面的一些环境变量(主要是一些开发套件如 Java、Android、Flutter 等)放到 ~/.bash_profile 这个文件中,But…

自从在 macOS 上面安装了 ohmyz.sh 之后,每次重启电脑或者新开终端的时候,我在 ~/.bash_profile 这个文件中的所有命令都无法使用了。除非再次执行下面的命令:

1
source ~/.bash_profile

通过网友们的群力群策,还是很快找到了解决方案。

在 ~/.zshrc 文件最后增加一行:source ~/.bash_profile 即可完美解决。

如果你发现本机 ~/ 目录下面没有 .zshrc 这个文件,可以新建一个,命令如下:

1
2
3
cd ~/

touch .zshrc

如果你是用的是 macOS Catalina(10.15) 版本的系统,其实系统已经用 Zsh 取代了 Bash,成为操作系统的默认 Shell。相比 Bash 来说,Zsh 也拥有许多更强大的功能:

  • 更智能的自动补全;

  • 命令选项提示;

  • 更丰富的主题,等等。

可以使用下面的命令查询一下当前你的系统默认 Shell 是哪个,如下:

1
ps -p $$

我在 macOS 终端上执行,结果如下:

1
2
PID TTY        TIME CMD
692 ttys000 0:00.91 -zsh

网上还有些朋友说在 ~/.profile 中增加 source ~/.bash_profile 也可以解决问题,但我亲测后发现对于自己当前的 macOS 并没有生效。


目标一旦确立,遇到任何困难请不要轻易放弃~

导入MySQL数据库文件

发表于 2020-12-13 | 分类于 Server , DB , Tools |

简介

user.sql 是我在 Windows 10 系统中写的数据库文件,下面的举例都是基于这个系统和文件进行操作的。

其他系统如 macOS,操作命令也基本一样,只是文件路径不一样罢了。

~接下来给大家介绍两种导入数据库文件的方法。

1、先进后导式

这种方式是先进入 MySQL,然后再导入数据库文件

进入 MySQL

1
mysql -u username -p password

其中 username、password 分别是你连接 MySQL 的用户名和密码;

进入之后,开始导入数据库文件

1
source sql-file-path

其中 sql-file-path 是你数据库文件的路径,例如

1
source E:/work/db/user.sql

如果执行导入操作,出现如下错误提示:

No database selected

可以先创建同名的数据库如数据库名称为 user

1
2
3
CREATE DATABASE user;

USE user;

执行完成后,再执行 source 操作即可。

2、同进同导式

这种方式是进入 MySQL 的同时也导入数据库文件

1
mysql -u username -p password -D data < sql-file-path

同理,username、password 分别是你连接 MySQL 的用户名和密码,后面的参数 -D 一定要大写, sql-file-path 是你数据库文件的路径,例如:

1
mysql -u username -p password -D data < E:/work/db/user.sql

当然我们也可以先不输入 password,等命令行提示输入密码的时候再输入也可以,如下:

1
mysql -u username -p -D data < E:/work/db/user.sql

同理,如果遇到类似第 1 种方法的错误,请先创建对应的数据库即可。

还有其他姿势,大家自行探索吧~


需求只是客户需要的一小部分而已,而不是需求等于需要~

微服务: SpringBoot 集成 Redis

发表于 2020-12-05 | 分类于 Server |

简介

忙碌了一周,感觉只有此刻写文字的时间才是最惬意的。最近工作比较忙,文章更新的较慢还望大家多多见谅🙇‍!

在阅读下面的内容之前,希望你已经安装并且做好了学习 Spring Boot 集成 Redis 的准备了。

如果你还没有搭建 Redis 环境,Win10-安装-Redis 这篇文章或许可以帮到您。

在 Spring Boot 1.x 版本中默认的 Redis 客户端是 Jedis 实现的,Spring Boot 2.x 版本中默认客户端是用 Lettuce 实现的。可以从加入的依赖包中看出,Spring Boot 2.2.x 中的 spring-data-redis 仍旧包括了 Jedis 和 Lettuce,但是默认使用了 Lettuce(换句话说,如果你不想使用默认的 Lettuce 实现可以换成 Jedis 的实现),如下图:

Lettuce 和 Jedis 的都是连接 Redis Server 的客户端,简单异同点如下:

  • Jedis 在实现上是直连 Redis Server,多线程环境下非线程安全,除非使用连接池,为每个 Redis 实例增加物理连接;
  • Lettuce 是 一种可伸缩,线程安全,完全非阻塞的 Redis 客户端,多个线程可以共享一个 RedisConnection,它利用 Netty NIO 框架来高效地管理多个连接,从而提供了异步和同步数据访问方式,用于构建非阻塞的反应性应用程序;

本篇只分享在 Spring Boot 项目中如何集成 Jedis 实现的 Redis 客户端和简单使用,关于 Redis 的使用有很多应用场景,后续再做探讨和分享(Redis 确实很强大,值得我们深入学习和研究)。

加入依赖包

在 pom.xml 文件中添加 redis 的依赖,如下:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

运行项目后,会报如下错误:

1
2
3
4
5
6
org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'redisConnectionFactory' defined in class path resource
[org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.class]: Bean instantiation via factory method failed;
nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory]:
Factory method 'redisConnectionFactory' threw exception;
nested exception is java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig

此时,需要在你的 pom 文件中添加连接池 commons-pool2 的依赖。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>

Lettuce 需要依赖 Apache的 common-pool2(至少是2.2版本)提供连接池,本篇使用的版本是 2.6.0,具体可以参考 Connection-Pooling 的介绍。

配置 properties

配置 application.properties 文件,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ------------------------------------------
# 配置 redis for lettuce
# ------------------------------------------
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1

注意:因为我的工程中使用了多个 properties 文件用于区分不同的环境,所以你根据自己的配置文件来配置 Redis 即可。

真的是 Lettuce 嘛?

为了证明我们现在使用的是 Lettuce 实现的 Redis 客户端,我写了一段测试代码,放到登录的 MSSigninController 进行了测试(具体代码可以参考 GitHub),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private RedisTemplate<String, Object> redisTemplate;

@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

@GetMapping(value = "/redisconn")
public String redis() {
log.info(redisTemplate.getValueSerializer().toString() + ", " + redisTemplate.getHashValueSerializer().toString());
RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
log.info(connectionFactory.toString());
if (connectionFactory instanceof LettuceConnectionFactory) {
LettuceConnectionFactory lettuceConnectionFactory = (LettuceConnectionFactory) connectionFactory;
log.info(lettuceConnectionFactory.getHostName() + ", " + lettuceConnectionFactory.getPort());
}
return connectionFactory.toString();
}

运行项目,打开 http://localhost:8080/signin/redisconn 即可看到浏览器上面显示类似如下的输出信息。

1
org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory@5e91612a

同时,控制台输出如下内容:

1
2
3
4
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer@7fd8e94e, 
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer@7fd8e94e
org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory@34212775
127.0.0.1, 6379

从上面的信息可以看出:

  • Redis 的客户端实现默认使用的是 Lettuce;
  • 默认的序列化类是 JdkSerializationRedisSerializer,下面我们可以配置 RedisTemplate 来改变默认的序列化类;

配置 RedisTemplate

关于 RedisTemplate 的配置,配置的实例代码都在 MSRedisConfig 这个类中,这里不再粘贴代码,有需要的可以直接点击 GitHub 查看。

配置完成后重新运行工程,再次访问 http://localhost:8080/signin/redisconn,控制台输出如下内容:

1
2
3
4
org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer@3151bece, 
org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer@7248212a
org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory@6bcda66f
127.0.0.1, 6379

可见,我们已经成功改变了默认的序列化类。

实例

这次的例子,仍是在原来的工程 微服务-结合MySQL实现登录注册 的基础上面改造的,后续分享的内容基本都是在这个工程上面拓展。

用户使用用户名和密码登录,首先去 Redis 里面查找,查找到了直接返回不用去 MySQL 数据库中查找了,如果没有找到再去 MySQL 数据库中查找,查找成功后存储到 Redis 中,本次分享只是为了说明如何集成 Redis以及其简单使用,流程比较简单没有考虑其他因素和优化,仅供学习使用。

修改的逻辑部分,代码贴在下面,可以结合注释看一下很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@CrossOrigin(origins = {"*", "http://localhost:63344"})
@RequestMapping(value = "/name", method = RequestMethod.GET)
@ApiOperation(value = "用户名登录", httpMethod = "GET", notes = "用户名登录")
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "用户名", required = true),
@ApiImplicitParam(name = "userpwd", value = "密码", required = true)
})
public MSResponse sigin(@RequestParam(value = "username") String userName, @RequestParam(value = "userpwd") String userPwd) {
MSResponse response = new MSResponse();
MSUser user = null;
if (null == userName || null == userPwd || userName.length() <= 0 || userPwd.length() <= 0) {
MSUserResponseEnum responseEnum = MSUserResponseEnum.Login4SiginInvalidInfo;
response.setCode(responseEnum.getCode());
response.setMsg(responseEnum.getMsg());
} else {
/** 从Redis里面查找该用户 */
MSUser redisUser = (MSUser) redisTemplate.opsForValue().get(userName);
String query_user_pwd = "";
if (null != redisUser && userPwd.equals(redisUser.getAccountName())) { // Redis 里面有该用户信息
log.info("Redis 中找到了 " + userName);
MSUserResponseEnum rspEnum = MSUserResponseEnum.SUCCESS;
user = redisUser;
response.setCode(rspEnum.getCode());
response.setMsg(rspEnum.getMsg());
} else { // Redis 里面没有该用户信息
log.info("Redis 中没有找到 " + userName);
/** 查数据库的‘user’表中是否有该用户?*/
List<Map> query_users = userService.queryUserByUserName(userName);
if (query_users.isEmpty()) {// 没有该用户
MSUserResponseEnum responseEnum = MSUserResponseEnum.LoginNoSuchUser;
response.setCode(responseEnum.getCode());
response.setMsg(responseEnum.getMsg());
} else {// 有这个用户
Map user_map = query_users.get(0);
query_user_pwd = (String) user_map.get("accountPwd");
if (!query_user_pwd.equals(userPwd)) {
MSUserResponseEnum responseEnum = MSUserResponseEnum.LoginUserPwdError;
response.setCode(responseEnum.getCode());
response.setMsg(responseEnum.getMsg());
} else {
// 将查询出来的map对象使用FastJson转换为MSUser对象
user = JSON.parseObject(JSON.toJSONString(user_map), MSUser.class);
// 缓存到 Redis,使用userName作为key
String userNameKey = String.valueOf(user.getAccountName());
redisTemplate.opsForValue().set(userNameKey, user);
log.info("MySQL 中找到了 " + userName + ", 并存到 Redis 中");

MSUserResponseEnum rspEnum = MSUserResponseEnum.SUCCESS;
response.setCode(rspEnum.getCode());
response.setMsg(rspEnum.getMsg());
}
}
}
}

response.setResults(user);

return response;
}

连续两次登录请求 http://localhost:8080/signin/name?username=foobar&userpwd=foobar ,可以看到如下输出:

1
2
3
4
5
6
Redis 中没有找到 foobar
MySQL 中找到了 foobar, 并存到 Redis 中

... 省略其他日志

Redis 中找到了 foobar

打开 Redis 的 CLI,可以查询一下是否存储成功,

1
127.0.0.1:6379> get foobar

得到结果如下:

1
"{\"@class\":\"com.veryitman.user.model.MSUser\",\"userID\":1723068547,\"accountName\":\"foobar\",\"accountPwd\":\"foobar\",\"nickName\":\"foobar\",\"age\":20,\"gender\":1,\"motto\":\"\",\"phone\":\"\"}"

参考资料

  • Lettuce
  • Lettuce-Core
  • Lettuce-Connection-Pooling
  • Jedis

适当的赞美可以让对方做出好的改变~

Win10 安装 Redis

发表于 2020-11-15 | 分类于 Server , DB |

简介

关于微服务相关的分享,我都是在 Windows 10 系统下实践的,所以跟 Spring Boot 配套的软件也是在此系统上面安装的。

今天跟大家介绍一下如何在 Win10 下安装 Redis,该篇是 Spring Boot 使用 Redis 的处女篇,下篇结合之前登录场景举例如何在 Spring Boot 中使用 Redis。

往期 Win10 下面安装 MySQL 的文章:MySQL8-0-15在Win10上的折腾记 推荐给有需要的伙伴。

安装 Redis

Step1:下载

下载 Windows 版本的 Redis,点击这里 下载 Redis。

我下载的是 3.0.504 版本。

Step2:解压

解压下载的 zip 包到任意目录,如我的目录:

Step3:启动

命令行进入刚才解压文件的根目录下,然后执行如下命令即可,如下:

1
./redis-server.exe redis.windows.conf

看到如下界面表示成功。

这种方式一旦关闭命令行,Redis 服务就关闭了,所以需要将 Redis 安装成系统(Windows)服务。

Step4:日志文件

在 Redis 解压后的根目录,新建 Logs 目录,然后在该目录下新建 redis_log.txt 文件。

否则,在进行下面操作的时候会报 Redis service failed to start. 错误。

如果你按照该要求新建了还是报错,需要经过下面的操作解决。

在命令行中(如果使用的是 git bash 终端,需要在下面的命令前加上 winpty)下开启 CLI 模式,运行

1
./redis-cli.exe

然后再运行

1
2
3
shutdown

exit

重新启动服务即可。

Step5:设置 Redis 成 Windows 服务

另外打开一个命令行窗口,执行如下命令:

1
./redis-server.exe --service-install redis.windows-service.conf --loglevel verbose --service-name Redis

执行成功后,你可以在 Windows 的系统服务(CMD+R/services.msc)中找到该服务(服务名称为 Redis),如下图:

另外从下图可以看到其属性,对应的配置文件是 redis.windows-service.conf,对应上面注册服务时使用的配置文件,所以后续的自定义配置就需要修改这个文件,不要搞错了。

如果你使用 redis.windows.conf 那么这里就应该是这个文件。

Step6:启动服务

执行如下启动命令即可,如下:

1
./redis-server.exe --service-start

在服务列表中刷新一下,可以看到 Redis 正在运行。

注册服务成功后,就不需要像 Step3 那样开启服务了,在 Step3 中开启的服务窗口也可以关闭了。后续使用 Step6 方式启动即可,或者将该服务设置为开机自启动也可以。

如果在启动 redis 服务的时候报如下的错误:

1
[18236] 29 Nov 10:52:01.070 # HandleServiceCommands: system error caught. error code=1056, message = StartService failed: unknown error

说明你的 redis 服务已经启动了,可以查看 Windows 的服务(CMD+R/services.msc)中是否已经启动成功。

使用 Redis

Redis 相关命令

再次强调一下,如果使用了 git bash 终端执行下面的命令没有反应或者卡住很久的话,需要在下面命令前加上 winpty 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 安装服务
./redis-server.exe --service-install

// 卸载服务
./redis-server.exe --service-uninstall

// 开启服务
./redis-server.exe --service-start

// 停止服务
./redis-server.exe --service-stop

// 开启 CLI 模式
./redis-cli.exe -h 127.0.0.1 -p 6379

测试 Redis

在命令行开启 CLI (命令行界面 command-line interface)模式,任意设置一个变量,如下:


只有弱者才去争取公平,这句话虽然残忍但很现实~

<1234…20>

193 日志
16 分类
163 标签
© 2024 veryitman