利他才能利己


  • 首页

  • 标签

  • 归档

  • 搜索

微服务: Admin与服务监测和管理

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

简介

Spring Boot Admin 并不是 Spring Boot 官方出品的,开源地址 SpringBoot Admin,伴随着 SpringBoot 的更新,Spring Boot Admin 也做了相对应的更新。Spring Boot 推出 2.X 版本时,Spring Boot Admin 也及时进行了更新。本篇使用的就是 Spring Boot Admin 2.2.1版本,最新版本是 2.2.2。 Spring Boot Admin 分为 client 和 server 端,server 端一般独立为一个服务,client 可以是你已经开发完成的服务,也可以是你即将要开发的服务。

对 Spring Boot Admin 2.x 版本的具体的更新内容和变化感兴趣的朋友,可以参考 changes-2.x.adoc 文档,这里不再赘述。

继上一篇 微服务-Actuator实现服务监测,我们说到使用 Actuator 组件可以实现对服务的监测,但是这种方式没有管理界面体验很不友好。本篇继续学习关于 Admin 的知识,该组件不仅囊括了 Actuator 的功能,还提供了管理界面,使用起来比较方便。

本篇主要内容,如下:

  • 通过 HTTP 方式集成和使用 spring-boot-admin-server;

  • 如何安全的使用 spring-boot-admin,这里又包含两个内容:

    • 如何将 spring-boot-admin-server 结合 spring-boot-security,实现安全管控以及如何配置;
    • 如何将 spring-boot-admin-client 结合 spring-boot-security,实现安全管控以及如何配置;

完整工程代码,请移步 Github 下载。后续会结合 spring-cloud 中的 Eureka 组件再次学习和分享使用 spring-boot-admin。

配置 Admin Server

为了学习和验证 Spring Boot Admin 的使用,我创建了一个 Admin-Server工程即 ms-admin,创建了两个 Admin-Client 工程即 ms-user 和 ms-admin-client,工程结构如下图:

ms-user 和 ms-admin-client 不同点在于:ms-user 使用了 spring-boot-security 模块,而 ms-admin-client 没有使用 spring-boot-security 模块,是一个相对比较简单的 SpringBoot 服务。

1、配置 ms-admin

在该 module 的 properties 文件中,配置如下:

1
2
3
4
5
spring.application.name=Admin-Server
# ---------------------------------
# 配置该服务的独立端口
# ---------------------------------
server.port=8087

注意:该服务的端口为 8087。

该 module 的 pom 文件,使用了 spring-boot 版本为 2.2.6.RELEASE,spring-boot-admin 版本为 2.2.1,这里一定要注意版本的匹配,否则工程运行会存在各种奇葩问题。这里教大家一个方法,如果你不知道如何匹配版本,直接使用 spring initializr 创建工程,然后下载创建的工程,看一下里面的 pom文件中各个模块的版本号就可以了。

修改一下启动类,增加 @EnableAdminServer 注解,如下:

1
2
3
4
5
6
7
8
9
10
11
12
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@EnableAdminServer
@SpringBootApplication
public class MZAdminApplication {

public static void main(String[] args) {
SpringApplication.run(MZAdminApplication.class, args);
}
}

2、配置 ms-admin-client

在该 module 的 properties 文件中,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring.application.name=Admin-Client
server.port=8083

# ------------------------
# Actuator config
# ------------------------
# 可以访问任意控制点
management.endpoints.web.exposure.include=*

# ------------------------------------------
# 配置 spring boot admin
# ------------------------------------------
spring.boot.admin.client.url=http://localhost:8087
management.endpoint.health.show-details=always

注意:该服务的端口为 8083。如果 admin-client 和 admin-server 没有部署在同一台服务器,这里的spring.boot.admin.client.url 一定要写上 admin-server 所在服务器的ip地址。

在该 module 的 pom 文件中,引入如下依赖即可,如下:

1
2
3
4
5
6
7
8
9
10
11
<!-- springboot admin client -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-client</artifactId>
<version>2.2.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

其余的不需要修改任何东西。

3、启动验证

分别启动 ms-admin、ms-admin-client,启动成功后,在浏览器中打开 http://localhost:8087 就可以看到如下效果,可以看到 ms-admin-client 被发现了。

点进去,可以看到 ms-admin-client 的运行状况。大家可以自己试试,我就不截图了。

配置带 security 的 Admin-Client

上面我们说了,ms-user 也是一个 Admin-Client,我也希望他能被监控和管理,但是 ms-user 有点特殊它使用了 security 模块,那么如何让他被 Admin-Server 发现呢,我们接着往下看。

PS:ms-user 模块其实是我之前一直在 微服务相关文章 中使用的工程模块,这里只是重构了一下工程结构并且修改了一下名称而已。

因为 ms-user 包含了比较多的内容,其 pom 文件也相对复杂一些,这里粘贴一部分(MySQL和Swagger相关配置就不写了)。

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
spring.application.name=User-Server

# ------------------------
# Actuator config
# ------------------------
# 可以访问任意控制点
management.endpoints.web.exposure.include=*
# 配置独立的端口
management.server.port=1012
# 自定义访问控制点的路径
management.endpoints.web.base-path=/monitor
management.endpoint.shutdown.enabled=true

# -----------------------------------------------
# Spring Security Default user name and password
# -----------------------------------------------
spring.security.user.name=admin
spring.security.user.password=admin
spring.security.user.roles=ACTUATOR_ADMIN

# ------------------------------------------
# 配置 spring boot admin
# ------------------------------------------
spring.boot.admin.client.url=http://localhost:8087
management.endpoint.health.show-details=always
# 告诉admin-server用户名和密码,否则监控不到
spring.boot.admin.client.instance.metadata.user.name=${spring.security.user.name}
spring.boot.admin.client.instance.metadata.user.password=${spring.security.user.password}

这里要特别注意, ms-user 使用了security并且设置了用户名和密码,一定要告诉 Admin-Server,否则无法被监控到。也就是说如下的配置必须写:

1
2
3
# 告诉admin-server用户名和密码,否则监控不到
spring.boot.admin.client.instance.metadata.user.name=${spring.security.user.name}
spring.boot.admin.client.instance.metadata.user.password=${spring.security.user.password}

紧接着我们启动 ms-user 这个服务,刷新一下浏览器,效果如下图:

可以看到 ms-user 这个服务被如愿以偿的发现了。

Admin-Server 使用 security

在 ms-admin 的 pom 文件中引入 security 模块,如下:

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

修改 properties 文件,增加用户名和密码的配置,完整配置如下:

1
2
3
4
5
6
7
8
9
10
11
spring.application.name=Admin-Server
# ---------------------------------
# 配置该服务的独立端口
# ---------------------------------
server.port=8087

# ---------------------------------
# security 配置
# ---------------------------------
spring.security.user.name=admin
spring.security.user.password=admin

配置完成后,我们分别重新启动一下 ms-admin、ms-admin-client 这两个服务。

再次打开 localhost:8087,会出现如下界面:

输入在 properties 文件中配置的用户名和密码,这里我配置的用户名和密码都是 admin,你也可以配置其他的。

输入之后可以看到如下界面,再次输入上面的用户名和密码即可。

初次进入显示效果如下截图,居然没有发现 ms-admin-client 这个服务。

这是因为没有在客户端配置进入 Admin-Server 的密码,安排上。

在 ms-admin-client 的 properties 中配置,如下:

1
2
3
4
5
6
7
8
# ------------------------------------------
# 配置 spring boot admin
# ------------------------------------------
spring.boot.admin.client.url=http://localhost:8087
management.endpoint.health.show-details=always
# 配置 admin server 的用户名和密码,否则注册不上
spring.boot.admin.discovery.instances-metadata.user.name=${spring.security.user.name}
spring.boot.admin.discovery.instances-metadata.user.password=${spring.security.user.password}

同理,在 ms-user 的配置文件 properties 中也增加相关的配置即可。

记得继承自 WebSecurityConfigurerAdapter 写一个配置类,可以参考 SpringBootAdminServerApplication 示例代码,不然客户端有可能无法注册成功。

重新启动 ms-admin 服务然后登录进去再启动客户端,就可以看到如下界面:

另外,Admin-Server 还可以集成 spring-boot-starter-mail 模块再简单的配置一下,就可以实现发送邮件的功能,这样服务的运行状态可以及时通知到对应的收件人了。


面朝大海,春暖花开。尚拙能成才,你没有比别人聪明,你只是更加努力罢了~

微服务: Actuator实现服务监测

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

简介

SpringBoot Actuator 可以帮助我们监控 SpringBoot 应用的运行情况,服务上线后如何监控和管理自己的服务是我们要考虑的一个重要问题。

Actuator 是 SpringBoot 的一个附加组件(需要自己手动集成),Actuator 能帮助应用程序实现在各个环境运行时的监控。可以使用 HTTP 的各个请求路径来进行健康检查、审计、收集引用的运行情况,对于应用状态、内存、线程、堆栈等等都可以进行监控,比较全面的监控了 SpringBoot 应用的整个生命周期。对于微服务的管理十分有意义,在企业级应用中,对系统进行运行状态监控是不可或缺的。

Actuator 不仅支持 HTTP 方式访问,还支持 JMX(Java Management Extensions,即Java管理扩展)、SSH、Telnet 方式访问。Actuator 还支持端点扩展,扩展 Actuator 的端点(Endpoint) 可以自定义监控指标,实现更多的运维能力。

访问 spring-boot-actuator 给我们展现的是纯 JSON 格式的数据,对于非开发人员来说不够直观和不够人性化。Spring Boot Admin 就是用来解决这个问题的,使用 Spring Boot Admin 可以实现这些 JSON 接口数据的界面展现,方便运营人员查看和管理。本篇我们只学习 spring-boot-actuator 的使用,关于 Spring Boot Admin 下一篇再一起学习。

本篇主要内容:

  • 如何使用 spring-boot-actuator(算是简单入门);
  • 结合 spring-boot-security 增加对 spring-boot-actuator 的访问限制;

更多关于微服务相关文章,可以通过 微服务项目系列文章 学习。

实战

在你的工程的 pom.xml 文件中,加入如下依赖就可以引入 spring-boot-actuator 组件,如下:

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

等待加载完成(就是对应的jar包文件下载完成且ide加载jar正常),可以看到多了如下两个jar包。

actuator 是其功能实现,actuator-autoconfigure 是其自动配置的实现。

启动工程,然后在浏览器打开 http://localhost:8080/actuator,可以看到如下 json 数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8080/actuator/info",
"templated": false
}
}
}

你可以访问如下的 URL,如下:

1
2
http://localhost:8080/actuator/health
http://localhost:8080/actuator/info

在 Actuator 的 2.x 版本中,大多数端点默认被禁掉了,且默认端点增加了 /actuator 前缀。默认暴露的两个端点为 /actuator/health 和 /actuator/info,正是如上可以访问的两个 URL.

如果想开启所有的断点可以被访问,需要在工程的 properties 文件中,增加如下配置:

1
management.endpoints.web.exposure.include=*

再次访问 http://localhost:8080/actuator,就会有更多的URL可以被访问了。如 http://localhost:8080/actuator/env、http://localhost:8080/actuator/beans 等。

官方文档 Spring Boot Actuator: Production-ready Features 完整的介绍了相关的内容,大家可以自行查阅。

从上面的访问路径可以看出,默认访问 Actuator 所有的监控点路径都在/actuator/*,是否可以自定义呢?是的,可以自定义这个前缀,只需要在 properties 中配置如下内容即可:

1
management.endpoints.web.base-path=/monitor

再次访问上面的 URL,就需要将 actuator/ 换成 monitor/ 了。

安全

虽然 Actuator 提供了很方便的监控和管理服务的能力,但是试想一下让任何其他的服务都可以访问,会存在一些安全隐患,SpringBoot 提供了 security 模块,可以帮助我们更加安全的使用 Actuator。集成 security 也很简单,下面我们具体说说如何使用吧。

首先,需要在 pom 文件中,添加如下依赖。

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

其次,配置工程的 properties 文件,加入如下内容:

1
2
3
spring.security.user.name=actuator-self
spring.security.user.password=actuator-admin
spring.security.user.roles=ACTUATOR_ADMIN

用户名(name)和密码(password),可以根据自己的实际情况自定义。

最后,添加一个配置类 MSActuatorSecurityConfig,该类继承 WebSecurityConfigurerAdapter.

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
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class MSActuatorSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint())
.hasRole("ACTUATOR_ADMIN")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
.permitAll()
.antMatchers("/")
.permitAll()
.antMatchers("/**")
.authenticated()
.and()
.httpBasic();
}
}

下次再次访问 http://localhost:8080/monitor,就会提示你输入用户名和密码了。

下图是使用 Chrome 浏览器显示的效果。

输入配置的用户名和密码登录就可以访问到数据了。

另外为了安全,可以单独设置 Actuator 的管理端口并配置不对外网开放,如下配置独立的端口 1102,然后可以在防火墙上做下限制即 1102 端口仅用于内网访问即可。

在 properties 中增加如下配置,如下:

1
2
# 配置独立的端口
management.server.port=1012

使用效果:


人无千日好,花无百日红,早时不算计,过后一场空。~

微服务: 结合MySQL实现登录注册

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

简介

继 MySQL8.0.15在Win10上的折腾记 和 微服务: MySQL基本操作 后,由于个人原因没能继续创作,从今天开始补上,继续前进💪。

本篇是 微服务系列 的第 N 篇,本篇结合 MySQL 和 SpringBoot 实现用户登录注册,算是一个小实战也是对之前知识点的一个小结。

在阅读本篇内容之前,需要大家做好如下几件事情:

  • 在本机安装了 MySQL,并且可以正常使用;如果你用的是 Windows10,可以参考 MySQL8.0.15在Win10上的折腾记 这篇文章;
  • 已经搭建好了 SpringBoot 的开发环境;可以参考 微服务: 想办法让项目运行起来 来搭建环境;
  • 能使用 SpringBoot 正确连接 MySQL,实现简单的增删改查(CRUD);可以参考 微服务: MySQL基本操作 这篇文章;
  • 理解跨域问题,请务必先阅读 跨域和OPTIONS这对欢喜冤家;
  • 树立信心,坚持不懈,不怕困难;

本篇文章主要内容:

  • 网页端,提供注册、登录的入口,请求服务端提供的 API,请求采用 Ajax;
  • 服务端,提供注册、登录的 API,处理用户注册和登录的请求,并能正确读写 MySQL 数据库;

代码全部放在 Github,有需要的朋友们可以自行获取。

登录注册流程图

本篇分享的用户注册和登录流程比较简单,看图说话。

1568529858131

前端效果

本篇中的前端页面很简单,我本人对前端的认知是个二半吊子,现学现卖,主要是配合接口做一些实例,没啥技术含量。使用前端开发,给我最大的感觉就是方便,简洁。

1、注册页面

1568529858131

注册成功会自动跳转到登录页面。

2、登录页面

1568529858131

登录失败会自动跳转到注册页面。

页面和逻辑的源代码对应分别如下

1568529858131

1 -> ms_login.html 是登录页面的 HTML 文件;

2 -> ms_signup.html 是注册页面的 HTML 文件;

3 -> ms_user.js 是 Ajax 的请求逻辑的 JavaScript 代码。

配置 MySQL 的连接

既然要使用 MySQL,首先要在工程中配置跟 MySQL 相关的连接参数。

在工程的 application.properties 文件中,配置下面参数即可。

1
2
3
4
5
6
7
# jdbc
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# charset=utf8mb4
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mzc_user?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
# 设置为你自己 MySQL 的密码
spring.datasource.password=root

具体的这些参数含义在 微服务: MySQL基本操作 这篇文章中有详细的讲解。

编写 API

源文件 MSSigninController 实现了登录的 API,核心代码如下:

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
@RestController
@RequestMapping(value = "signup") // 注意这里不要在signup前后加"/"
public class MSSignupController {

@Autowired
private MSUserService userService;

@CrossOrigin(origins = {"*"})
@PostMapping(value = "/name")
public MSResponse signup(@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) {
MSResponseEnum signupError = MSResponseEnum.SignupInvalidInfo;
response.setMsg(signupError.getMsg());
response.setCode(signupError.getCode());
} else {
// 创建user表
userService.createUserTable();
// 检查用户数据库的‘user’表中是否有该用户?
List<Map> query_users = userService.queryUserByUserName(userName);
if (null == query_users || query_users.isEmpty()) {// 没有该用户的数据
user = MSUserUtil.createUser(userName, userPwd);
// 插入一条用户数据到数据表中
userService.addUser(user);
response.setCode(MSResponseEnum.SUCCESS.getCode());
response.setMsg(MSResponseEnum.SUCCESS.getMsg());
} else {// 用户数据库的‘user’表中有该用户信息
// 返回错误信息:该用户已经注册过了
MSResponseEnum signupError = MSResponseEnum.SignupHasExistUser;
response.setMsg(signupError.getMsg());
response.setCode(signupError.getCode());
}
}

response.setResults(user);

return response;
}
}

源文件 MSSignupController 实现了注册的 API,核心代码如下:

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
@RestController
@RequestMapping(value = "signin") // 注意这里不要在signin前后加"/"
public class MSSigninController {

@Autowired
private MSUserService userService;

@CrossOrigin(origins = {"*", "http://localhost:8082"})
@RequestMapping(value = "/name", method = RequestMethod.GET)
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) {
MSResponseEnum responseEnum = MSResponseEnum.Login4SiginInvalidInfo;
response.setCode(responseEnum.getCode());
response.setMsg(responseEnum.getMsg());
} else {
// 检查用户数据库的‘user’表中是否有该用户?
List<Map> query_users = userService.queryUserByUserName(userName);
if (query_users.isEmpty()) {// 没有该用户
MSResponseEnum responseEnum = MSResponseEnum.LoginNoSuchUser;
response.setCode(responseEnum.getCode());
response.setMsg(responseEnum.getMsg());
} else {// 有这个用户
Map user_map = query_users.get(0);
String query_user_name = (String) user_map.get("accountName");
// 没有对应的用户名
if (!query_user_name.equals(userName)) {
MSResponseEnum responseEnum = MSResponseEnum.LoginNoSuchUser;
response.setCode(responseEnum.getCode());
response.setMsg(responseEnum.getMsg());
} else {// 查询到了该用户
// 将查询出来的map对象使用FastJson转换为MSUser对象
user = JSON.parseObject(JSON.toJSONString(user_map), MSUser.class);
MSResponseEnum rspEnum = MSResponseEnum.SUCCESS;
response.setCode(rspEnum.getCode());
response.setMsg(rspEnum.getMsg());
}
}
}

response.setResults(user);

return response;
}
}

相比于之前的代码逻辑,现在的代码无非就是加入了操作数据库的逻辑。代码比较简单,看注释就可以看明白。

操作数据库的代码,都在 MSUserServiceImpl 这个实现类中。我就不一一的粘贴代码了。有兴趣的朋友可以自行到 Github 下载。

验证

首先,启动工程,让工程在本地正常运行起来。如果你还没有搭建 SpringBoot 的开发环境,建议参考 微服务-想办法让项目运行起来 这篇文章。

然后在浏览器打开注册页面,建议使用 Chrome 浏览器操作。如下图,我使用 foobar 用户名和 foobar 密码进行注册。

1568529858131

点击注册按钮,成功后用终端登录 MySQL 数据库,看看是否有数据。如下图,可以看到数据库和对应的表。

1568529858131

可以从下图看出,数据已经成功的写入数据库中了。

1568529858131

最后,打开登录界面,输入用户名称和密码都是 foobar 的就可以登录成功,说明从数据库读取数据也是正常的。

今天就说这么多吧,下次再见!


从现在做起,一切都还来得及。

OC坑集之block作为函数参数

发表于 2020-02-27 | 分类于 iOS |

对于 OC(Objective-C,苹果推出的自家编程语言) 的争论一直不休,对于一门语言,我觉得只要能解决你的问题就够了。

评论一门编程语言的好坏之前,需要我们真正的去用它,OC 孰好孰坏,需要你自己去尝试。

OC 坑集,主要记载使用 OC 开发项目遇到的问题,尽量简洁明了,希望大家喜欢。

block 语句块可以作为函数的参数, 在项目中都经常被作为回调来使用。

如果你还不熟悉 block 语法,请自行 google,这里不讲 block 语法。我们下面直接进入主题。

创建一个 OC 类

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface FoolBar : NSObject

- (void)makeFool:(void(^)(NSString *))callback;

@end
1
2
3
4
5
6
7
8
9
10
11
#import "FoolBar.h"

@implementation FoolBar

- (void)makeFool:(void (^)(NSString *))callback
{
NSLog(@"makeFool | callback.");
callback(@"This is a block parameter.");
}

@end

main.m

1
2
3
4
5
6
7
8
9
10
11
12
#import <Foundation/Foundation.h>
#import "FoolBar.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
FoolBar *fb = [[FoolBar alloc] init];
[fb makeFool:^(NSString *) {

}];
}
return 0;
}

大家看到这里,应该觉得没什么问题。

我刚开始也是行事冲冲的,但编译器给了个大大的 Error, 大致意思是参数丢失。

经过一番思想上面的斗争,终于解决了问题。

在声明带有 block 语句的方法时,需要带上参数名称或者调用者自己写上参数名称。

1
- (void)makeFool:(void(^)(NSString *param))callback;

重新给它整上,如下:

1
2
3
4
5
6
7
8
9
10
11
12
#import <Foundation/Foundation.h>
#import "FoolBar.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
FoolBar *fb = [[FoolBar alloc] init];
[fb makeFool:^(NSString *param) {
NSLog(@"param = %@", param);
}];
}
return 0;
}

使用 block 作为函数参数的时候,建议使用 typedef 定义 block.

1
typedef void (^OnFoolCallback)(NSString *name);

定义和实现

1
- (void)productFool:(OnFoolCallback)callback;
1
2
3
4
5
- (void)productFool:(OnFoolCallback)callback
{
NSLog(@"productFool | callback.");
callback(@"This is a block parameter: mark.");
}

测试和使用,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>
#import "FoolBar.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
FoolBar *fb = [[FoolBar alloc] init];
[fb makeFool:^(NSString *param) {
NSLog(@"param = %@", param);
}];

[fb productFool:^(NSString *name) {
NSLog(@"name = %@", name);
}];
}

return 0;
}

盼望疫情早日结束,每个人都能回归到正常的生活当中去!

OpenSSL: 实战-RSA分段解密

发表于 2020-01-05 | 分类于 C/C++ |

简介

本篇是继 OpenSSL: 简单易上手的RSA加解密 后的补充篇,实战篇。

在实际项目中,并没有像上篇文章写的那么简单,实际情况要复杂的多。万变不离其宗,抽丝剥茧,复杂事务的背后一定是有其本质原因和原理的存在,而我们就是挖掘原理,探索本质的福尔摩斯。

今天这篇文章,带领大家参与到实际项目中运用RSA加解密,在阅读下面内容之前,期望大家可以下载 openssl 的源码,或者下载我 上篇文中 的代码示例。

我下载的是 openssl-source-1.1.0f 这个版本的源码,正好对应我从 precompiled-openssl 下载的编译版本。

1

项目概述

该项目的开发语言仍然采用C语言来实现,我们借助 openssl 来模拟实际项目中的案例。

服务端使用 RSA 加密原始数据,然后采用 Base64 编码该加密数据经过 HTTP 传输给到客户端;

客户端接收到该数据,先使用 Base64 解码数据,然后再使用 RSA 解密数据,最终得到原始数据。

这里特别注意,客户端收到的数据大小可能会大于 128 字节,我们知道 RSA 加密明文最大长度 117 字节,而解密的最大值是 128 字节,所以超过该大小需要分段解密数据。

大概流程图如下:

1

很简单的一个项目,对吧,接着往下看吧 :)-

解个小惑

也许有些朋友会问,为毛 RSA 加密的明文大小是 117 字节,而解密的最大字节数是 128 字节,两者一样不是更好吗,至少好理解呀?

得出上面结论的前提是我们RSA密钥长度是 1024 位即 128 字节(1024/8=128),同理如果是 512 位的密钥,那么最大的 RSA 解密字节长度应该是(512/8)64 字节,最大加密的明文长度是(64-11)53 字节。

在 openssl 源码中,我们可以看到如下代码:

1
# define RSA_PKCS1_PADDING_SIZE  11

在 rsa_sign.c 文件中可以看到 RSA_sign 函数:

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
int RSA_sign(int type, const unsigned char *m, unsigned int m_len,
unsigned char *sigret, unsigned int *siglen, RSA *rsa)
{
int encrypt_len, encoded_len = 0, ret = 0;
unsigned char *tmps = NULL;
const unsigned char *encoded = NULL;

if (rsa->meth->rsa_sign) {
return rsa->meth->rsa_sign(type, m, m_len, sigret, siglen, rsa);
}

/* Compute the encoded digest. */
if (type == NID_md5_sha1) {
/*
* NID_md5_sha1 corresponds to the MD5/SHA1 combination in TLS 1.1 and
* earlier. It has no DigestInfo wrapper but otherwise is
* RSASSA-PKCS1-v1_5.
*/
if (m_len != SSL_SIG_LENGTH) {
RSAerr(RSA_F_RSA_SIGN, RSA_R_INVALID_MESSAGE_LENGTH);
return 0;
}
encoded_len = SSL_SIG_LENGTH;
encoded = m;
} else {
if (!encode_pkcs1(&tmps, &encoded_len, type, m, m_len))
goto err;
encoded = tmps;
}

if (encoded_len > RSA_size(rsa) - RSA_PKCS1_PADDING_SIZE) {
RSAerr(RSA_F_RSA_SIGN, RSA_R_DIGEST_TOO_BIG_FOR_RSA_KEY);
goto err;
}
encrypt_len = RSA_private_encrypt(encoded_len, encoded, sigret, rsa,
RSA_PKCS1_PADDING);
if (encrypt_len <= 0)
goto err;

*siglen = encrypt_len;
ret = 1;

err:
OPENSSL_clear_free(tmps, (size_t)encoded_len);
return ret;
}

可以看出,RSA_PKCS1_PADDING 这种填充模式是占用了 11 个字节的,那么 127+11 正好也是 128 字节。

每次RSA加密的明文的长度是受RSA填充模式限制的,如下表:

填充方式 输入 输出 备注
RSA_PKCS1_PADDING 必须比RSA钥模长(modulus) 短至少11个字节, 也就是RSA_size(rsa) – 11,对于1024bit的密钥,RSA_size(rsa)=128字节,即明文为128-11=117字节;如果输入的明文过长,必须切割,然后填充。 和modulus一样长 最常用的填充方式
RSA_PKCS1_OAEP_PADDING RSA_size(rsa) – 41 和modulus一样长 最优非对称填充OAEP,安全性是最高的
RSA_NO_PADDING 可以和RSA钥模长一样长,如果输入的明文过长,必须切割,然后填充。 和modulus一样长 -

这里注意下面结论:

  • 在不同的padding模式下,使用相同长度的密钥可以加密的数据最大长度不同;
  • 在不同密钥长度下,使用相同的padding模式可以加密的数据最大长度也不同;

可以阅读 rfc2313 中关于 PKCS #1: RSA Encryption Version 1.5 的部分。

开战

实战代码主要在 main.c 文件中的 example_rsa3() 函数中。

原始数据是字符串 www.veryitman.com,如下还包括了公私钥。

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
// 原始数据为字符串:www.veryitman.com
unsigned char plainText[] = "www.veryitman.com";

unsigned char publicKey[] = "-----BEGIN PUBLIC KEY-----\n"
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrPgCMJW17JN2DW7tZFk/FB6pU\n"
"pLvLOo6G/EuND8XZptffXbyiY2VscMRhP+kKVeaLO9HuEYR3Zl78x8oR6prytstc\n"
"/MueersWDxh4iGSHsZXGxA41hXrXLRElrSTRc43ea18o0zMxZoVZiR2JFt7QcgM+\n"
"T6eOrvj59MhXv9O46QIDAQAB\n"
"-----END PUBLIC KEY-----\n";

unsigned char privateKey[] = "-----BEGIN RSA PRIVATE KEY-----\n"
"MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKs+AIwlbXsk3YNb\n"
"u1kWT8UHqlSku8s6job8S40Pxdmm199dvKJjZWxwxGE/6QpV5os70e4RhHdmXvzH\n"
"yhHqmvK2y1z8y556uxYPGHiIZIexlcbEDjWFetctESWtJNFzjd5rXyjTMzFmhVmJ\n"
"HYkW3tByAz5Pp46u+Pn0yFe/07jpAgMBAAECgYBj1YH8MtXhNVzveEuBZMCc3hsv\n"
"vdq+YSU3DV/+nXN7sQmp77xJ8CjxT80t5VS38dy2z+lUImJYOhamyNPGHkC2y84V\n"
"7i5+e6ScQve1gnwHqRKGBjtSCaYOqm9rTDECCTT1oMU26sfYznWlJqMrkJp1jWn7\n"
"aAwr+3FcX2XhD74ZAQJBAN34Y6fmHLRPv21MsdgGqUjKgyFvJfLUmtFFgb6sLEWc\n"
"k22J3BAFAcNCTLYHFZwMhL/nwaw9/7rIUJD+lcl6n3cCQQDFfrN14qKC3GJfoBZ8\n"
"k9S6F7Ss514DDPzIuenbafhoUjZDVcjLw9EmYZQjpfsQ3WdNICUKRrDHZay1Pz+s\n"
"YkKfAkB+OKfaquS5t/t/2LPsxuTuipIEqiKnMjSTOfYsidVnBEFlcZZc2awF76aV\n"
"f/PO1+OJCO2910ebXBtMSCi++GbDAkEAmc7zNPwsVH4OnyquWJdJNSUBMSd/sCCN\n"
"PkaMOrVtINHmMMq+dvMqEBoupRS/U4Ma0JYYQsiLJL+qof2AOWDNQQJAcquLGHLT\n"
"eGDDLluHo+kkIGwZi4aK/fDoylZ0NCEtYyMtShQ3JmllST9kmb9NJX2gMsejsirc\n"
"H6ObxqZPbka6UA==\n"
"-----END RSA PRIVATE KEY-----\n";

对数据进行私钥加密,示例如下:

1
2
3
4
5
6
7
// 私钥加密
int encrypted_length = private_key_encrypt(plainText, len, privateKey, encrypted_str);
if (-1 == encrypted_length)
{
printf("Private Encrypt failed\n");
exit(0);
}

私钥加密之后,进行 Base64 编码:

1
2
3
4
5
6
7
8
9
10
char *base64_content;
size_t encrypted_str_length = strlen(encrypted_str);
int encode_res = mzc_base64_encode(encrypted_str, encrypted_str_length, &base64_content);
if (0 != encode_res)
{
printf("Base64 encode failed\n");
exit(0);
}
printf("Base64 encode content: %s\n\n", base64_content);
printf("Base64 encode content's length: %i\n\n", strlen(base64_content));

至此,上面两个步骤就模拟完成了服务端加密的过程。下面我们来继续模拟客户端解密的过程。

首先,对 Base64 编码之后的数据进行 Base64 解码。

1
2
3
4
5
6
7
8
9
10
char *base64DecodeOutput;
size_t decode_output_length;
int decode_res = mzc_base64_decode(base64_content, &base64DecodeOutput, &decode_output_length);
printf("base64 decode content: %s\n\n", base64DecodeOutput);
printf("base64 decode content's length: %i\n\n", decode_output_length);
if (0 != decode_res)
{
printf("Base64 decode failed\n");
exit(0);
}

看一下打印结果:

1
base64 decode content's length: 160

很明显,长度要大于 128,需要进行分段处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 最大解密长度
#define RSA_MAX_DECRYPT_SIZE 128

// 每段解密的长度
int chunk = 0;
unsigned char tmp_dstr[RSA_MAX_DECRYPT_SIZE];
memset(tmp_dstr, '\0', sizeof(tmp_dstr));

// (数据被)分段解密(公钥解密)
while (chunk <= decode_output_length)
{
int decrypted_length = public_key_decrypt(base64DecodeOutput, RSA_MAX_DECRYPT_SIZE, publicKey, tmp_dstr);
memcpy(decrypted_str, tmp_dstr, decrypted_length);
printf("Current decrypted content length =%d\n", decrypted_length);
if (-1 == decrypted_length)
{
printf("Public Decrypt failed\n");
exit(0);
}
chunk += decrypted_length;
}

printf("......\n\n");
printf("Final decrypted string =%s\n", decrypted_str);

输出结果:

1
2
3
......

Final decrypted string =www.veryitman.com

至此整个过程简单模拟结束。

大家如果感兴趣的话,可以实现分段加密的过程。我就不再演示这个过程了,后续加入到源代码中去。


问君能有几多愁,恰似一江春水向东流。

OpenSSL: 简单易上手的RSA加解密

发表于 2019-12-29 | 分类于 C/C++ |

简介

这篇文章是继 OpenSSL: 用VS2017创建C工程、OpenSSL: 完成VS2017的配置 两篇文章的第3篇,前两篇分别跟大家分享了如何在 visual studio2017 中创建C工程,配置OpenSSL的开发环境以及使用 openssl 开源库实现base64的编解码。

本篇跟大家分享如何使用 openssl 开源库实现 RSA 的加密和解密,不过这一篇是对简单的数据进行加解密,下一篇继续跟大家分享复杂一点的 RSA 加解密过程以及实例代码。

2019 即将被画上句号,自己还有很多愿望没有实现。一拿起书就感觉困,一拿起手机觉得黑夜里阳光明媚,无药可救了呀!

获取公、私钥

在 precompiled-openssl 获取的压缩包中,解压可以在 bin64或者bin(32位操作系统使用) 目录下面找到 openssl.exe 文件,双击打开即可。

生成私钥

执行下面代码,如下:

1
genrsa -out rsa_private_key.pem 1024

此时在 bin64 目录下会生成 rsa_private_key.pem 文件,即私钥文件;

将 RSA 私钥转换成 PKCS8 格式,执行如下代码:

1
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt

生成公钥

执行下面代码,如下:

1
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

此时在 bin64 目录下会生成 rsa_public_key.pem 文件,即公钥文件;

用VS Code打开这两个文件,可以看到具体内容,在我的机器上生成的公钥:

1
2
3
4
5
6
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrPgCMJW17JN2DW7tZFk/FB6pU
pLvLOo6G/EuND8XZptffXbyiY2VscMRhP+kKVeaLO9HuEYR3Zl78x8oR6prytstc
/MueersWDxh4iGSHsZXGxA41hXrXLRElrSTRc43ea18o0zMxZoVZiR2JFt7QcgM+
T6eOrvj59MhXv9O46QIDAQAB
-----END PUBLIC KEY-----

私钥,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKs+AIwlbXsk3YNb
u1kWT8UHqlSku8s6job8S40Pxdmm199dvKJjZWxwxGE/6QpV5os70e4RhHdmXvzH
yhHqmvK2y1z8y556uxYPGHiIZIexlcbEDjWFetctESWtJNFzjd5rXyjTMzFmhVmJ
HYkW3tByAz5Pp46u+Pn0yFe/07jpAgMBAAECgYBj1YH8MtXhNVzveEuBZMCc3hsv
vdq+YSU3DV/+nXN7sQmp77xJ8CjxT80t5VS38dy2z+lUImJYOhamyNPGHkC2y84V
7i5+e6ScQve1gnwHqRKGBjtSCaYOqm9rTDECCTT1oMU26sfYznWlJqMrkJp1jWn7
aAwr+3FcX2XhD74ZAQJBAN34Y6fmHLRPv21MsdgGqUjKgyFvJfLUmtFFgb6sLEWc
k22J3BAFAcNCTLYHFZwMhL/nwaw9/7rIUJD+lcl6n3cCQQDFfrN14qKC3GJfoBZ8
k9S6F7Ss514DDPzIuenbafhoUjZDVcjLw9EmYZQjpfsQ3WdNICUKRrDHZay1Pz+s
YkKfAkB+OKfaquS5t/t/2LPsxuTuipIEqiKnMjSTOfYsidVnBEFlcZZc2awF76aV
f/PO1+OJCO2910ebXBtMSCi++GbDAkEAmc7zNPwsVH4OnyquWJdJNSUBMSd/sCCN
PkaMOrVtINHmMMq+dvMqEBoupRS/U4Ma0JYYQsiLJL+qof2AOWDNQQJAcquLGHLT
eGDDLluHo+kkIGwZi4aK/fDoylZ0NCEtYyMtShQ3JmllST9kmb9NJX2gMsejsirc
H6ObxqZPbka6UA==
-----END PRIVATE KEY-----

紧接着我会使用这对公私钥进行编码。

RSA 实战

RSA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。

1973年,在英国政府通讯总部工作的数学家克利福德·柯克斯(Clifford Cocks)在一个内部文件中提出了一个相同的算法,但他的发现被列入机密,一直到1997年才被发表。

对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到目前为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。

1983年麻省理工学院在美国为RSA算法申请了专利。这个专利2000年9月21日失效。由于该算法在申请专利前就已经被发表了,在世界上大多数其它地区这个专利权不被承认。

新建头文件 mzc_rsa.h,声明几个函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma once

/* 公钥解密 */
int public_key_decrypt(unsigned char *enc_data, int data_len, unsigned char *key, unsigned char *decrypted);

/* 私钥加密 */
int private_key_encrypt(unsigned char *data, int data_len, unsigned char *key, unsigned char *encrypted);

/* 公钥加密 */
int public_key_encrypt(unsigned char *data, int data_len, unsigned char *key, unsigned char *encrypted);

/* 私钥解密 */
int private_key_decrypt(unsigned char *enc_data, int data_len, unsigned char *key, unsigned char *decrypted);

对应的实现文件 mzc_rsa.c,代码如下:

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
60
61
62
63
64
65
66
67
#include "mzc_rsa.h"

#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/rsa.h>
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/buffer.h>

#include <stdio.h>

const int PADDING = RSA_PKCS1_PADDING;

RSA *createRSA(unsigned char *key, int public_token)
{
RSA *rsa = NULL;
BIO *keybio;
keybio = BIO_new_mem_buf(key, -1);
if (keybio == NULL)
{
printf("Failed to create key BIO");
return 0;
}
if (public_token)
{
rsa = PEM_read_bio_RSA_PUBKEY(keybio, &rsa, NULL, NULL);
}
else
{
rsa = PEM_read_bio_RSAPrivateKey(keybio, &rsa, NULL, NULL);
}
if (rsa == NULL)
{
printf("Failed to create RSA");
}

return rsa;
}

int public_key_decrypt(unsigned char *enc_data, int data_len, unsigned char *key, unsigned char *decrypted)
{
RSA *rsa = createRSA(key, 1);
int result = RSA_public_decrypt(data_len, enc_data, decrypted, rsa, PADDING);
return result;
}

int private_key_encrypt(unsigned char *data, int data_len, unsigned char *key, unsigned char *encrypted)
{
RSA *rsa = createRSA(key, 0);
int result = RSA_private_encrypt(data_len, data, encrypted, rsa, PADDING);
return result;
}

int public_key_encrypt(unsigned char *data, int data_len, unsigned char *key, unsigned char *encrypted)
{
RSA *rsa = createRSA(key, 1);
int result = RSA_public_encrypt(data_len, data, encrypted, rsa, PADDING);
return result;
}

int private_key_decrypt(unsigned char *enc_data, int data_len, unsigned char *key, unsigned char *decrypted)
{
RSA *rsa = createRSA(key, 0);
int result = RSA_private_decrypt(data_len, enc_data, decrypted, rsa, PADDING);
return result;
}

注意这里使用的 RSA 对齐模式是 RSA_PKCS1_PADDING。

在测试的主函数(main)中,使用明文 www.veryitman.com 作为示例进行加密和解密操作,我在下面只列出关键实现。具体代码可以去 Github 下载,注意 clone rsa-feature 这个分支。

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
60
61
/* 公钥加密, 私钥解密 */
int main()
{
//原始数据为字符串:www.veryitman.com
unsigned char plainText[] = "www.veryitman.com";

unsigned char publicKey[] = "-----BEGIN PUBLIC KEY-----\n"
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrPgCMJW17JN2DW7tZFk/FB6pU\n"
"pLvLOo6G/EuND8XZptffXbyiY2VscMRhP+kKVeaLO9HuEYR3Zl78x8oR6prytstc\n"
"/MueersWDxh4iGSHsZXGxA41hXrXLRElrSTRc43ea18o0zMxZoVZiR2JFt7QcgM+\n"
"T6eOrvj59MhXv9O46QIDAQAB\n"
"-----END PUBLIC KEY-----\n";

unsigned char privateKey[] = "-----BEGIN RSA PRIVATE KEY-----\n"
"MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKs+AIwlbXsk3YNb\n"
"u1kWT8UHqlSku8s6job8S40Pxdmm199dvKJjZWxwxGE/6QpV5os70e4RhHdmXvzH\n"
"yhHqmvK2y1z8y556uxYPGHiIZIexlcbEDjWFetctESWtJNFzjd5rXyjTMzFmhVmJ\n"
"HYkW3tByAz5Pp46u+Pn0yFe/07jpAgMBAAECgYBj1YH8MtXhNVzveEuBZMCc3hsv\n"
"vdq+YSU3DV/+nXN7sQmp77xJ8CjxT80t5VS38dy2z+lUImJYOhamyNPGHkC2y84V\n"
"7i5+e6ScQve1gnwHqRKGBjtSCaYOqm9rTDECCTT1oMU26sfYznWlJqMrkJp1jWn7\n"
"aAwr+3FcX2XhD74ZAQJBAN34Y6fmHLRPv21MsdgGqUjKgyFvJfLUmtFFgb6sLEWc\n"
"k22J3BAFAcNCTLYHFZwMhL/nwaw9/7rIUJD+lcl6n3cCQQDFfrN14qKC3GJfoBZ8\n"
"k9S6F7Ss514DDPzIuenbafhoUjZDVcjLw9EmYZQjpfsQ3WdNICUKRrDHZay1Pz+s\n"
"YkKfAkB+OKfaquS5t/t/2LPsxuTuipIEqiKnMjSTOfYsidVnBEFlcZZc2awF76aV\n"
"f/PO1+OJCO2910ebXBtMSCi++GbDAkEAmc7zNPwsVH4OnyquWJdJNSUBMSd/sCCN\n"
"PkaMOrVtINHmMMq+dvMqEBoupRS/U4Ma0JYYQsiLJL+qof2AOWDNQQJAcquLGHLT\n"
"eGDDLluHo+kkIGwZi4aK/fDoylZ0NCEtYyMtShQ3JmllST9kmb9NJX2gMsejsirc\n"
"H6ObxqZPbka6UA==\n"
"-----END RSA PRIVATE KEY-----\n";

unsigned char encrypted_str[128];
unsigned char decrypted_str[128];

// 需要初始化,否则解密出来的字符串会有多余的乱码
memset(encrypted_str, '\0', sizeof(encrypted_str));
memset(decrypted_str, '\0', sizeof(decrypted_str));

size_t len = strlen((const char *)plainText);
printf("Encrypted length =%d\n", len);

// 公钥加密
int encrypted_length = public_key_encrypt(plainText, len, publicKey, encrypted_str);
if (encrypted_length == -1)
{
printf("Private Encrypt failed\n");
exit(0);
}

// 私钥解密
int decrypted_length = private_key_decrypt(encrypted_str, encrypted_length, privateKey, decrypted_str);
if (decrypted_length == -1)
{
printf("Public Decrypt failed\n");
exit(0);
}

printf("Decrypted Text =%s\n", decrypted_str);
printf("Decrypted Length =%d\n", decrypted_length);

return 0;
}

今天到此为止,累了,睡觉了😜。


活着不是靠泪水搏取同情,而是靠汗水获得掌声~

Android中值得说的Handler之更新UI

发表于 2019-12-21 | 分类于 Android |

最近在看电视剧《庆余年》,被调皮的编剧逗得爱不释手,范闲同志从“潜龙勿用”直到大殿醉酒背诵唐诗300首,让众官瞠目结舌,也算是“飞龙在天”了,这一集(第27集)看的那叫一个过瘾。尤其范闲那句“我醉欲眠君且去,去你妈的…”,差点让我喷饭。

推荐大家周末可以看看,算是休闲一下吧!

简介

个人总是感觉 Android中更新 UI 很让人纠结!特此小结一下,算是抛砖引玉。

读这篇文章之前,假设你已经明白多线程、Handler 如何使用。

在文章的最后,附录一张草图,主要用于说明 Handler、Message、MessageQueue、Looper 之间的关系。

更新UI的骚操作

1、在 onCreate() 方法中开启线程更新 UI

直接上例子,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MasterActivity extends Activity { 
TextView tv = null; Button btn = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());
tv = (TextView)findViewById(R.id.text);
/*onCreate中开启新线程,更新UI。没有报错或者异常信息!*/
btn = (Button)findViewById(R.id.btn);
Thread thread = new Thread(new Runnable() {
@Override public void run() {
System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());
tv.setText("update UI is success!");
btn.setText("update UI is success!");
}
});
thread.start();
}

随便折腾,不会报错也不会报任何异常!

以为开启的线程和 UI 线程(主线程)是同一个线程,但是很不幸,他们的线程id风牛马不相及!

大家可以查一下 Android 源码,这个主要是因为在加载 Activity 的时候,还没有触发检查单线程的模型(即子线程不可以更新UI)。

如果你不相信的话,可以在上面的线程里面 while true,那么一定会报错的。

2、在 Activity 生命周期方法中更新 UI

如 Activity 的 onResume、onStart、反正是以 on 开头的回调方法中在非主线程中更新 UI,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onRestart() {
super.onRestart(); /*onRestart中开启新线程,更新UI*/
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());
tv.setText("update UI is success!");
btn.setText("update UI is success!");
}
});
thread.start();
}

不好意思,按下返回按钮在启动程序,或者按 Home 键再启动程序,就这么折腾几下,就会包异常!

异常信息如下:

1
2
UI.view.ViewRoot$CalledFromWrongThreadException: 
Only the original thread that created a view hierarchy can touch its views.

大概意思是:只有在主线程中才可以进行更新 UI 的操作。

这个时候,大家都应该想到 postInvalidate() 这个方法了。修改实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Override protected void onRestart() { 
super.onRestart(); /*onRestart中开启新线程,更新UI*/
Thread thread = new Thread(new Runnable() {
@Override public void run() {
System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());
tv.postInvalidate(); btn.postInvalidate();
tv.setText("update UI is success!");
btn.setText("update UI is success!");
}
});
thread.start();
}

postInvalidate() 方法,源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void postInvalidate() { 
postInvalidateDelayed(0);
}

public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
if (mAttachInfo != null) {
Message msg = Message.obtain();
msg.what = AttachInfo.INVALIDATE_MSG;
msg.obj = this;
mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
}

可以看出,postInvalidate() 本质是使用了 Handler 处理消息的机制!该方法可以在子线程中直接用来更新UI。对应的还有一个方法 **invalidate()**,稍候再说!

3、在 Button 的事件中开启线程,更新 UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MasterActivity extends Activity {
TextView tv = null; Button btn = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());
tv = (TextView)findViewById(R.id.text);
btn = (Button)findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Thread thread = new Thread(new Runnable() {
@Override public void run() {
System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());
tv.setText("update UI is success!");
btn.setText("update UI is success!");
}
});
thread.start();
}
});
}

Sorry,报错!即使你加上 postInvalidate() 方法,也会报这个错误。

1
UI.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

1

4、使用 Handler 结合多线程更新 UI

a. 开启一个线程,在 run 方法中通知 Handler

b. Handler 中使用 handleMessage 方法更新 UI

5、Handler 和 invalidate 方法结合多线程更新 UI

方法 invalidate 主要用在主线程中(即UI 线程中),不可以用于子线程。如果在子线程中需要使用 postInvalidate 方法。

Android 的 API 有说明:

public void invalidate () Since: API Level 1 Invalidate the whole view.

If the view is visible, onDraw(Canvas) will be called at some point in the future.

This must be called from a UI thread. To call from a non-UI thread, call postInvalidate().

看看该方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void invalidate() {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
}
if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
final ViewParent p = mParent;
final AttachInfo ai = mAttachInfo;
if (p != null && ai != null) {
final Rect r = ai.mTmpInvalRect;
r.set(0, 0, mRight - mLeft, mBottom - mTop); // Don't call invalidate -- we don't want to internally scroll // our own bounds p.invalidateChild(this, r); } } }
}
}
}

invalidate 方法如果你直接在主线程中调用,是看不到任何更新的。需要与 Handler 结合!

Android 在 onDraw 事件处理绘图,而 invalidate() 函数可以再一次触发 onDraw 事件,然后再一次进行绘图动作。实例代码如下:

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
public class MasterActivity extends Activity {
static int times = 1;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView( new View(null) {

Paint vPaint = new Paint(); //绘制样式物件
private int i = 0; //弧形角度

@Override
protected void onDraw (Canvas canvas) {
super.onDraw(canvas);
System.out.println("this run " + (times++) +" times!");

// 设定绘图样式
vPaint.setColor( 0xff00ffff ); //画笔颜色
vPaint.setAntiAlias( true ); //反锯齿
vPaint.setStyle( Paint.Style.STROKE );

// 绘制一个弧形
canvas.drawArc(new RectF(60, 120, 260, 320), 0, i, true, vPaint );

// 弧形角度
if( (i+=10) > 360 ) {
i = 0;
}

// 重绘, 再一次执行onDraw 程序
invalidate();
}
});
}
}

经过测试,发现 times 一直在被 ++,说明 onDraw 被多次调用,并且一直在画图!

Android 的 API 有时候让人看的很郁闷很无语…..关于 invalidate 的使用,还待探索。革命尚未成功,同志仍需努力!

小结

附录: Handler、Message、MessageQueue、Looper 之间的关系

1

这里说明

  • Looper 使用无限循环取出消息,是有 UI OS 控制的;

  • UI 线程是非安全的,即不要在子线程中更新 UI;

  • Looper 取出来的消息,Handler 可以通过 what、obj 等量来区别分别获取属于自己的消息,所以推荐使用这些内置变量。


天生我材必有用,千金散尽还复来。

微服务: MySQL基本操作

发表于 2019-12-07 | 分类于 Server , DB |

简介

本篇是 微服务系列 的第 N 篇,从本篇开始我们就要接触和学习数据库相关的知识了。

数据库我们选择 MySQL 数据库,免费且资料多,大部分公司都会使用该数据库。我是在 Windows 10 上面搭建的微服务开发环境,同理,MySQL 数据库也是在这个系统上面安装的并且使用的是 MySQL8.0 版本,如果你还没有安装或者在安装 MySQL8.0 遇到问题可以参考 MySQL8.0.15在Win10上的折腾记 这篇文章。

今天主要跟大家分享如何使用 SpringBoot 结合 JDBC 连接和操作 MySQL 数据库,主要还是以实例为主。

JDBC、ODBC、MySQL Connectors

在说实例之前,带大家先了解一下标题中几个名词的概念。

JDBC(Java database connectivity,数据连接),是 Sun 公司编的一堆类和方法,都封装在 java.sql 包中,可以利用这些类和方法来把你的 Java 程序和任意的数据库连通。即通过使用 JDBC,Java 开发人员可以将 SQL 语句传送给几乎任何一种数据库。

对应的还有 ODBC( Open Database Connectivity, 开放数据库互连),ODBC 是 Microsoft 提出的数据库访问接口标准。开放数据库互连定义了访问数据库 API 的一个规范,Microsoft 的 ODBC 文档是用 C 语言描述的,许多实际的 ODBC 驱动程序也是用 C 语言写的。ODBC 提供了对 SQL 语言的支持,用户可以直接将 SQL 语句送给 ODBC。

JDBC 和 ODBC 都是用来连接数据库的启动程序,两者具有数据库独立性甚至平台无关性。

MySQL Connectors 是 MySQL 数据库的驱动(程序),有对各种语言的支持。mysql-connector-java(也称之为 Connector/J 或者 mysql-connector-jdbc) 是 MySQL-Connectors 的 Java 版本的一个实现 ,用它可以连接 MySQL 系统。

同理,还有 mysql-connector-python、mysql-connector-c、 mysql-connector-ODBC、mysql-connector-jdbc 等,如下图(来自 MySQL Connectors 官网):

1568529858131

可以使用下面的图简单描述一下JDBC和数据连接的示意图,如下图:

1568529858131

一图胜千言,Java 应用程序可以通过 JDBC 和数据库驱动程序连接、访问、操作数据库(MySQL、Oracle等)。

配置 pom 文件

我还是用 Github 代码仓库的例子,大家可以自行 Clone 代码。

打开工程的 pom.xml 文件,增加 mysql-connector-java 和 spring-boot-starter-jdbc 依赖,从上面内容大家应该可以理解为什么要加入这两项了吧!

1
2
3
4
5
6
7
8
9
10
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

等待 Maven 下载完成这两项依赖。

配置 properties

打开 application-dev.properties 文件,增加如下内容:

1
2
3
4
5
6
7
8
9
10
11
# jdbc
# spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# JDBC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 连接的数据库
# spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mzc_user
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mzc_user?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
# 数据库用户名
spring.datasource.username=root
# 数据库密码
spring.datasource.password=pwd123

其中 mzc_user 是我创建的数据库文件名称,大家要根据自己的实际情况做相关的修改。配置 properties 注意以下几个问题。

1、spring.datasource.driver-class-name 的配置

如果配置值为 com.mysql.jdbc.Driver,会报下面的警告:

1
2
3
Loading class `com.mysql.jdbc.Driver'. This is deprecated. 
The new driver class is `com.mysql.cj.jdbc.Driver'.
The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.

意思是这个类已经过时了,请使用最新的 jdbc.driver 修改数据源配置,如下:

1
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

2、spring.datasource.url 的配置

关于这个url 的配置,官方有更加详细的配置说明,可以参考 Connection URL Syntax 和 Configuration Properties 两篇文档。

如果配置为 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mzc_user,会一直报如下错误:

1
2
The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. 
You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.

解决这个问题,有如下3种解决方案。

1、指定时区

1
2
// 北京时间东八区
serverTimezone=GMT%2B8

注意,%2B 是 + 的编码格式。
如果你设置 serverTimezone=UTC,连接不报错, 但在用java代码插入到数据库时间的时候却出现了问题。

比如在 Java 代码里面插入的时间为 2018-06-24 17:29:56,但是在数据库里面显示的时间却为 2018-06-24 09:29:56,这样就有了8个小时的时差。UTC 代表的是全球标准时间 ,而我们使用的时间是北京时区也就是东八区,领先 UTC 八个小时。

1
2
3
4
5
// 京时间东八区
serverTimezone=GMT%2B8

// 或者使用上海时间
serverTimezone=Asia/Shanghai

2、修改 MySQL 配置文件

我在 Windows 10 找到 MySQL 配置文件 my.ini ,如果你没有找到可以参考 MySQL8.0.15在Win10上的折腾记 这篇文章,非 Windows 系统可能配置文件时 my.cnf。

在配置文件中,增加默认时区配置:

1
default-time-zone='+08:00'

在 my.ini 或者 my.cnf 文件的 [mysqld] 下面增加上面配置,示例如下:

1
2
3
4
5
6
7
8
9
[mysqld]

# Set default time-zone
default-time-zone='+08:00'

# The TCP/IP Port the MySQL Server will listen on
port=3306

# ...

修改配置文件之后,重启MySQL服务就可以了。

3、修改数据库, 配置全局时区

切换到名称为 mysql 的数据库,然后执行下面的语句:

1
2
3
4
show variables like '%time_zone%';

# 设置全局时区,即时生效,作用于所有session
set global time_zone='+8:00';

1568529858131

设置完成后,可以使用 show variables like '%time_zone%'; 看看是否修改成功。

你也可以执行,但是只能对当前的 session 生效,示例如下:

1
2
# 设置当前session时区,即时生效,但仅作用于当前session
set time_zone='+8:00';

我个人使用的是上述中的第1种方法,我也推荐大家使用这种方法,简单且无副作用。

这里有一篇坑记 jdbc mysql connector 6 时区问题,关于设置 time_zone 的坑,大家可以看看。

万事俱备,只欠代码

配置完成之后,我们可以写点测试代码了。

本节的代码主要集中在 MSUserService、MSUserServiceImpl、MSDBTests 中,其中 MSUserService 是接口,定义了一些数据库操作的方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.List;
import java.util.Map;

public interface MSUserService {

int createUser(MSUser user);

int deleteByUid(Integer uid);

List<Map> queryUserByUid(Integer uid);

List<Map> queryUserByUserName(String userName);

int deleteUserTable();
}

而 MSUserServiceImpl 则是 MSUserService 的实现类, MSDBTests 主要是测试类,用来测试操作 MySQL 数据表的如创建表、查询、删除等操作。大家自行 Clone 代码去看实现即可,我就不在这里占用篇幅贴代码了。

在本节代码中,新增加了两个注解的使用即 @Service 和 @Slf4j,@Slf4j 这个注解主要用来简化使用日志。而 @Service 注解承担了两个职责一是 Bean 的创建,二是将一个类标识为一个服务。后续我会再深入跟大家分享 @Service 这个注解,目前你只需要用上它就可以了。

在本节的例子中,用到的数据表(user)的数据结构大概如下:

1568529858131

启动 MySQL 服务,然后启动 MSDBTests 中的测试代码,就可以看到实际效果了。

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
@Test
public void createUserSQLTest() {
MSUser user = new MSUser();
user.setUserID(1);
user.setAccountName("mzc");
user.setAccountPwd("123");
user.setNickName("veryitman");
user.setMotto("foo");
user.setAge(25);
user.setGender(MSUser.GENDER_MALE);
user.setPhone("17122036530");

// 增加user数据
msUserService.createUser(user);
}

@Test
public void queryUserBySQLTest() {
// 根据user_id查询
List<Map> userJson = msUserService.queryUserByUid(1);
logger.info("MSBlog Test, userJson by query user's id: " + userJson.get(0));

// 根据user_name查询
userJson = msUserService.queryUserByUserName("mzc");
logger.info("MSBlog Test, userJson by query user's name: " + userJson.get(0));
}

今天就分享到这里,下次结合登录注册例子操作 MySQL 数据库。


做事情,不问能不能做成,要问应不应该做。

短地址原理

发表于 2019-11-24 | 分类于 Server |

简介

一个同事遇到一个问题,他说受限于第三方的服务,自己请求所带过去的字符串因为太长,无法从第三方服务获取信息,很是苦恼😴。后来我们商量了一下,最终借鉴短地址的思想解决了问题。道理很简单,因为我们请求中的附加字符串信息第三方服务只是透传回来给我们,我们只需要把这些附加信息进行字符压缩就可以了,这样请求带过去的字符串长度就满足了要求😊。

无论是写文章还是制作一些表格的时候,我会时常用到短地址。短地址的优势在于其短(字符少)、简洁,方便书写又不占位置。方便在社交网络和第三方平台上分享链接,投放广告。比如有人会在发朋友圈的时候带上短地址,还有一些营销短信里面也会带一些短地址链接。

有现成的短地址生成器,常用的短地址转换有 百度短网址转换、Google短网址转换 和 新浪短地址转换,可惜的是谷歌关闭了该服务,官方发言如下:

1
2
3
4
5
On March 30, 2018, we turned down support for goo.gl URL shortener. 
From April 13, 2018, only existing users were able to create short links on the goo.gl console.
Analytics data was available for up to one year, until March 30, 2019, when goo.gl was discontinued.
Previously created links will continue to redirect to their intended destination.
Please see this blog post for more details.

我经常使用百度的短地址服务,在国内访问它是最快的也能满足我的工作和学习需求,它也提供 API 服务可以参考 短网址生成接口文档 学习和了解。

我们先来了解一下 HTTP 协议中那些重定向的事。

HTTP 请求重定向

HTTP 中的 301、302、303、307、308 响应状态码,都表示重定向的响应。

其中,301、308 响应状态码表示永久重定向,302、303、307 表示临时重定向。

那我们来说一下什么是重定向?

重定向(Redirect)就是通过各种方法将各种网络请求重新定个方向转到其它位置(如:网页重定向、域名的重定向、路由选择的变化也是对数据报文经由路径的一种重定向)。

举个例子,你要去 A 机构办理一个证件,等你去了之后A机构的某人告诉你他们不再受理此事了,需要你去 B 机构办理,然后你就自己去了 B 机构。这个过程就类似于重定向。

那么,HTTP 协议中定义的这些30X响应状态码就好比 A机构的某人 它们告诉客户端,你需要访问另外一个地址了。

重定向做了两次 HTTP 请求, 第一次,客户端请求 A 服务器,A 响应告诉浏览器,你应该去 B 服务器访问。此时就去访问服务器 B,这个时候你可以看到浏览器中的网址变了,这就是第二次 HTTP 请求。

重定向过程:

Step-1、浏览器(客户端)发送 HTTP 请求;

Step-2、Web服务器A接收后发送 302 状态码响应,并在响应头中把对应的 Location 给浏览器;

Step-3、浏览器收到服务器返回的 302 响应码,就自动再发送一个新的 HTTP 请求(请求URL是新的 Location 中的地址);

Step-4、Web服务器(可能是 A 也可能是其他服务器)根据此请求寻找资源并发送给浏览器,最终展示给用户。

关于 Location 可以看下面的截图(这是一个重定向的 HTTP 请求示例),它是被放在响应头中的,其值是需要重定向的网址 http://www.veryitman.com,从这个过程来看可以看出重定向是客户端(浏览器)行为。

还有个叫转发的技术,这个和重定向是不一样的,转发是服务器行为,还拿上面的 A 机构例子来说,你要去 A 机构办理一个证件,等你去了之后 A 机构的某人告诉你他们不再受理此事了,但是 A 机构可以自己协调资源帮你完成而不需要你去其他机构办理了。这个过程就类似于转发。

模拟重定向

现在使用 SpringBoot 模拟一下重定向,我用的 SpringBoot 是 2.2.0.RELEASE 版本。

①、新建 SpringBoot Web 工程,可以参考 微服务-想办法让项目运行起来 这篇文章。

②、修改 pom 文件,增加 fastjson,示例如下:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

③、新建 MSTestRedirectController 文件,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping(value = "testredirect")
public class MSTestRedirectController {

@GetMapping(value = "/access/web")
public String redirect() {
return "redirect:/testredirect/index/realweb?parameter=coming";
}

@ResponseBody
@GetMapping(value = "/index/realweb")
public String real(HttpServletRequest request) {
return "redirect happened:" + JSON.toJSONString(request.getParameterMap());
}
}

这里要注意几个问题:

  • 既然是重定向,该 Controller 不能使用 @RestController 注解而要使用 @Controller 注解;

  • 这里使用了关键字 redirect 实现重定向;

④、启动工程,并在浏览器访问下面的网址

1
http://localhost:8080/testredirect/access/web

可以看到浏览器发生了重定向,截图如下:

除了上面的方法可以实现重定向外还可使用 HttpServletResponse 的 sendRedirect 方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping(value = "/access/web2")
public String redirect2(HttpServletResponse response) {
try {
// 方法1:自定义状态码方式
// String url = "http://localhost:8080/testredirect/index/realweb?parameter=coming";
//response.setHeader("Location", url);
//response.sendError(301);

// 方法2:sendRedirect,默认返回的状态码是 302
response.sendRedirect("/testredirect/index/realweb?parameter=coming");
} catch (IOException e) {
e.printStackTrace();
} finally {
return "";
}
}

请求短地址过程

我拿网址 http://www.veryitman.com/ 来举实例,使用百度短地址服务。

在 百度短地址页面 去生成 http://www.veryitman.com/ 对应的短地址,如下图所示:

得到短地址是 https://dwz.cn/hnmau4Zs 复制该地址拷贝到浏览器(我用的是 Chrome 浏览器)的地址栏中,并打开 Chrome 的审查视图。切换到 Network 选项,此时回车打开短网址。

HTTP 发送了 GET 请求(红色1),请求地址是 https://dwz.cn/hnmau4Zs ,服务器(百度的短地址服务)返回给 Chrome 浏览器 302 状态码,浏览器发现是该重定向码就再次用 Location 里面包含的地址发送了第二次请求即重定向请求。

你也可以使用微博的短地址服务,提醒一点,微博的短地址请求返回码是 301 而百度返回的是 302 响应码。302 状态码允许各种各样的重定向,一般情况下都会实现为到 GET 的重定向,但是不能确保 POST 会重定向为 POST,302 表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B;而 301 状态码表明目标资源被永久的移动到了一个新的 URI,任何未来对这个资源的引用都应该使用新的 URI。

短地址原理

刚开始我很好奇,为什么我把长地址 A 转换为短地址 B,然后用 B 去访问居然还是 A 地址的内容,浏览器是怎么做到的?

通过上面的请求过程示例,相信大家应该大概理解了请求短地址的原理了。

在百度短地址服务中,我们将 http://www.veryitman.com/ 转换为 https://dwz.cn/hnmau4Zs ,此时百度短地址服务维持了 短-长 地址的映射关系了而且是唯一的,当我们去访问 https://dwz.cn/hnmau4Zs ,其实请求的是百度短地址服务,该服务将短地址对应的长地址(放在响应头的 Location 中)返回给我们的浏览器,并返回 302 状态码,此时浏览器就重定向到了 http://www.veryitman.com/ 这个网址上了。

简单总结一下其步骤如下:

Step-1、用户在浏览器里输入 https://dwz.cn/hnmau4Zs 这个网址去访问;

Step-2、浏览器解析 DNS,获取该域名对应的 IP 地址;

Step-3、获取到 IP 后,浏览器发送 HTTP GET 请求查询 hnmau4Zs 并获取到 https://dwz.cn/hnmau4Zs 对应的长地址;

Step-4、HTTP 通过 302 状态码转到去请求对应的长地址 http://www.veryitman.com/ 上面了。

我把 http://www.veryitman.com/ 放到百度和微博的短地址生成分别是:

1
2
// 百度短地址
https://dwz.cn/hnmau4Zs
1
2
// 微博段地址
http://1t.click/aMtD

可以看出百度生成较复杂,首先协议变成了 HTTPS,其次生成代码是 8 位(hnmau4Zs),而微博生成的是 4 位(aMtD)代码。

短地址码一般都是由26个大写字母 A-Z 、26个小写字母 a-z 和10个数字 0-9 共62个字符随机组合而成,那么可以这样来生成短地址码,我们定义一个62进制,把这62个字符按照10进制数转成62进制数,那么就可以得到每个字符对应的62进制数了。同理,将短地址还原的时候把62进制转换为对应的10进制就可以了。

根据上面算法,可以看出百度可以支持 62^8 个短地址,微博可以支持 62^4 个短地址。

关于短地址生成的算法,大家可以用SpringBoot自己撸一个或者去网上找找别人已经实现的。


活着不是靠泪水搏取同情,而是靠汗水获得掌声~

OpenSSL: 完成VS2017的配置

发表于 2019-11-23 | 分类于 C/C++ |

简介

在 OpenSSL: 用VS2017创建C工程 中跟大家分享了如何在 Visual Studio 2017 中创建 C 工程和注意事项以及遇到问题的解决方案,今天我们开始在 Visual Studio 2017 中搭建 OpenSSL 的开发环境。

很多人在配置 OpenSSL 环境中遇到了不少问题,甚至还有放弃的。说实话,我也差点奔溃了:(,配置这些鬼东西确实有点复杂,说一句沃茨奥一点也不过分。

希望这篇文章能和你不期而遇,能够帮到你!写文不易,如果文中有任何错误之处还望大家不吝赐教。

下载OpenSSL

使用 OpenSSL 有两种方式,一是自己下载其源码按照配置去编译,然后使用编译后的库;另一种是使用别人已经编译好的库文件。

我使用的是第二种方法,在 precompiled-openssl 下载的 OpenSSL Library,下载完成后,解压。

设置VS2017

下面分几个步骤完成设置。

1、拷贝文件到工程中

在工程目录下面新建 vendor-openssl 目录,用来存放 OpenSSL 的头文件和库文件,便于管理,你也可以取个其他的名称。

进入解压后的 openssl-1.1.0f-vs2017 目录,复制 lib64 和 include64 到 vendor-openssl 目录下,如下图所示:

2、添加OpenSSL头文件

打开工程的属性设置,打开方法如下:

按照上述截图的1、2、3、4点击 Edit,出现下面的编辑框,如下图:

选择 include64目录,如下截图所示:

点击OK即可完成头文件的添加。

3、添加OpenSSL库路径

同理,打开属性设置,然后选择 Linker/General,如下图所示:

点击Edit,选择 OpenSSL 的库路径,如图:

4、添加库名称

同理,打开属性设置,在 Linker/Input 中设置,如图:

点击Edit,输入下面库的名称(编译后的OpenSSL库文件):

书写完每一个都要换行继续写下一个库名称,直到写完为止,如图:

点击弹框的OK按钮,关闭所有弹框。

base64编解码

在资源管理器视图中,新建源文件,分别添加 mzc_base64.h、mzc_base64.c 和 main.c 三个文件,如下图:

这里我只给出测试的代码(main.c源码),具体的编解码代码,大家可以去 GitHub 查看。

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
#include <stdio.h>
#include <string.h>

#include "mzc_base64.h"

int main()
{
//Encode To Base64
char *base64EncodeOutput;

//Take the web's url of my blog as example:http://veryitman.com
char *text = "http:\/\/veryitman.com";

mzc_base64_encode(text, strlen(text), &base64EncodeOutput);
printf("Base64 encode output: %s\n", base64EncodeOutput);

//Decode From Base64
char* base64DecodeOutput;
size_t test;
const char *base64Encode_str = "aHR0cDovL3ZlcnlpdG1hbi5jb20=";
mzc_base64_decode(base64Encode_str, &base64DecodeOutput, &test);
printf("Base64 decode output: %s %d\n", base64DecodeOutput, test);

return 0;
}

在 Visual Studio 2017 中使用快捷键 Ctrl+F5 运行工程报错,报错如图:

需要拷贝 openssl-1.1.0f-vs2017 目录中的bin64里面的内容:

把这些文件放到可执行文件的目录中,如图:

再次 Ctrl+F5 就可以正常执行了。

注意:点击 源码文件 可以获取本文的代码。


活着不是靠泪水搏取同情,而是靠汗水获得掌声~

<1…456…20>

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