0%

1
2
3
4
5
6
7
8
9
10
11
12
yum install -y ppp wget net-tools  perl

wget http://poptop.sourceforge.net/yum/stable/packages/pptpd-1.4.0-1.el6.x86_64.rpm
rpm -ivh pptpd-1.4.0-1.el6.x86_64.rpm

echo -e "localip 192.168.100.1\nremoteip 192.168.100.207-217" >>/etc/pptpd.conf
echo -e "ms-dns 192.168.100.1\nms-dns 192.168.100.1" >>/etc/ppp/options.pptpd
echo 'user1 pptpd 123456 *' >>/etc/ppp/chap-secrets
sed -i 's/net.ipv4.ip_forward = 0/net.ipv4.ip_forward = 1/g' /etc/sysctl.conf
sysctl -p
service pptpd restart
chkconfig pptpd on

参考:

https://www.cnblogs.com/blog-lhong/p/11712622.html

http://blog.chinaunix.net/uid-30076712-id-5522260.html

tracker_query_storage fail, error no: 28, error info: No space left on device

1 检查tracker.conf的配置

检查tracker.conf的配置项reserved_storage_space的值, 默认为4GB,检查剩余空间是否已不足4GB。 检查磁盘空间,看看文件存储路径所在分区空间是否不足 df -h

2 可能是其他节点空间不足

通过命令查看 /usr/local/bin/fdfs_monitor /etc/fdfs/storage.conf , 然后去节点检查磁盘空间.

在阅读本文之前,假设你的手机已经root,并且已经成功安装好了 XposedInstaller。

什么是Xposed

Xposed 是一个 Android 平台上的动态劫持框架,通过替换手机上的孵化器 zygote 进程为 Xposed 自带的 zygote,使其在启动过程中加载 XposedBridge.jar,模块开发者可以通过 jar 提供的 API 来实现对所有的 Function(这里可以理解为方法) 的劫持,在原 Function 执行的前后加上自定义代码。

1

Xposed框架是一款可以在不修改APK的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。Xposed理论上能够hook到系统任意一个Java进程,由于是从底层hook,所以需要root权限,并且每次更新都要重新启动,否则不生效 。

Xposed 和 Cydia Substrate

这两个框架都是app注入的利器,只是相对来说有各自特点罢了。Cydia Substrate是一个代码修改平台。它可以修改任何主进程的代码,不管是用Java还是C/C++(native代码)编写的。而Xposed只支持 hook system/bin/app_process中的Java函数。其实Cydia Substrate 与xposed 的hook原理是一样的,二者都可以作为Java Hook的框架,看使用习惯了(iOS的越狱用到的便是Cydia Substrate)。笔者最近研究主要以Xposed为主,暂不对Cydia Substrate进行详细的说明,后续有研究会继续以文字形式记录吧。

利用Xposed执行最简单的篡改操作

Xposed的运用太过广泛,作为这个系列的第一篇笔记,这里也介绍一种Xposed最简单的应用场景。

我们修改入门android逆向的项目,将 Hello World 修改为 Hello Xposed!

a. 随意在Android Studio里创建一个项目,在这里我的包名是:com.blues.cracktest 里面没有任何后续添加的代码,编译之后只有屏幕中间最经典的“Hello World!”;
b. 我们需要做的就是通过Xposed去修改这个“Hello World”,让它替换成任何我们想要它显示成为的东西,比如我期望是“Hello Xposed! ”
首先第一步先进行配置,在清单文件AndroidManifest里 Application作用域里加上如下配置:

1
2
3
4
5
6
7
8
9
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="hello xposed" />
<meta-data
android:name="xposedminversion"
android:value="82" />

第二步,在main目录下(java、res同级目录)创建assets文件夹,在该文件夹下新建一个xposed_init文件(该文件名称固定为xposed_init),该文件用于放置hook的入口,里面是一个路径(比如我的是com.example.androidrefirst.XposedInit,XposedInit就是我hook的入口,这个类后面会提及)。

第三步,新建一个类,这里就叫它XposedInit好了,让它实现(implements) IXposedHookLoadPackage 这个接口 ,重写 handleLoadPackage 方法(该方法用于获取需要hook到的类,里面会用到一个findAndHookMethod 用于hook对应的方法),我就直接show code吧:

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
package com.example.androidrefirst;

import android.os.Bundle;
import android.widget.TextView;

import java.lang.reflect.Field;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class XposedInit implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {
if (lpparam.packageName.equals("com.example.androidrefirst")) {
XposedHelpers.findAndHookMethod("com.example.androidrefirst.MainActivity", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
//不能通过Class.forName()来获取Class ,在跨应用时会失效
Class c = lpparam.classLoader.loadClass("com.example.androidrefirst.MainActivity");
Field field = c.getDeclaredField("textView");
field.setAccessible(true);
//param.thisObject 为执行该方法的对象,在这里指MainActivity
TextView textView = (TextView) field.get(param.thisObject);
textView.setText("Hello Xposed!!!!!!!");
}
});
}
}
}

原来的MainActivity长这样:

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
package com.example.androidrefirst;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
private EditText code;
private Button btn_verify;
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}

private void init() {
code = findViewById(R.id.editText);
btn_verify = findViewById(R.id.button);
btn_verify.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String verifyCode=code.getText().toString().trim();
if(verifyCode.equals("2019")){
Intent intent=new Intent(MainActivity.this,SuccessActivity.class);
startActivity(intent);
}else {
Toast.makeText(MainActivity.this,"验证码错误!", Toast.LENGTH_LONG).show();
}
}
});
textView=findViewById(R.id.textView1);
textView.setText("hello world!!!!!");
}
}

运行之后在 XposedInstaller 中启用我们的xposed模块 重启之后看效果。可以看到Hello Xposed!!!!!!!

参考:

https://blog.csdn.net/micaaa/article/details/82706778

逆向工程

逆向工程(又称逆向技术),是一种产品设计技术再现过程,即对一项目标产品进行逆向分析及研究,从而演绎并得出该产品的处理流程、组织结构、功能特性及技术规格等设计要素,以制作出功能相近,但又不完全一样的产品。逆向工程源于商业及军事领域中的硬件分析。其主要目的是在不能轻易获得必要的生产信息的情况下,直接从成品分析,推导出产品的设计原理。
逆向工程可能会被误认为是对知识产权的严重侵害,但是在实际应用上,反而可能会保护知识产权所有者。例如在集成电路领域,如果怀疑某公司侵犯知识产权,可以用逆向工程技术来寻找证据。

这里我们以安卓项目开始探索逆向工程之路

逆向工具

工欲善其身,必先利其器。

反编译代码的工具下载:

反编译资源的工具:

  • APKTool: 本文重要工具,APK逆向工具,使用简单下载地址: http://ibotpeaches.github.io/Apktool/install/
    这里简单介绍下大概流程,首先把后缀为.apk的文件改为.zip的一个压缩文件,方便解压。dex2jar和jd-gui配套使用,用于逆向代码部分,APKTool用于逆向res文件夹下的图片布局等部分。

新建一个简单的项目

新建一个项目,名字是 Androidrefirst,实现的逻辑是在输入框内填写 2019之后 点击按钮跳转到成功页面。

主要代码如下

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
package com.example.androidrefirst;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
private EditText code;
private Button btn_verify;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}

private void init() {
code = findViewById(R.id.editText);
btn_verify = findViewById(R.id.button);
btn_verify.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String verifyCode=code.getText().toString().trim();
if(verifyCode.equals("2019")){
Intent intent=new Intent(MainActivity.this,SuccessActivity.class);
startActivity(intent);
}else {
Toast.makeText(MainActivity.this,"验证码错误!", Toast.LENGTH_LONG).show();
}
}
});
}
}

生成 apk安装包 app-release.apk

逆向 apk

解压apk得到 classes.dex 文件

需要用到的是dex2jar包里面的三个文件(当前是在windows环境下,Mac环境用对应的.sh文件):

d2j_invoke.bat
d2j-dex2jar.bat
lib
将这三个文件复制到一个空的文件夹内,将刚才.apk解压后的classes.dex文件也一起复制到这里

使用命令 d2j-dex2jar.bat classes.dex 得到 jar文件

使用 jd-gui.exe 打开jar文件看到代码

可以看到我们的代码

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
package com.example.androidrefirst;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
private Button btn_verify;

private EditText code;

private void init() {
this.code = (EditText)findViewById(2131230782);
this.btn_verify = (Button)findViewById(2131230755);
this.btn_verify.setOnClickListener(new View.OnClickListener() {
public void onClick(View param1View) {
if (MainActivity.this.code.getText().toString().trim().equals("2019")) {
Intent intent = new Intent((Context)MainActivity.this, SuccessActivity.class);
MainActivity.this.startActivity(intent);
} else {
Toast.makeText((Context)MainActivity.this, ", 1).show();
}
}
});
}

protected void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
setContentView(2131427356);
init();
}
}

对照着自己的手写的代码,已经差不离十了,对于想要代码思路的我们来说,到这里已经基本可以摸透他的逻辑。

反编译res资源部分

apktool下载后会有两个文件,一个.jar(例如apktool_2.3.3.jar 需要把名字改成apktool.jar) 一个apktool.bat 。

apktool d

同刚才一样在cmd命令下进入刚才文件夹(同样可以新建一个),连同我们刚才那个后缀为apk的安装包一起放入,输入如下命令

1
apktool d app-release.apk  # 此处app-release为apk名称

得到一个新的app-release(对应apk名称)文件夹

这个app-release文件夹下会得到若干文件,主要内容介绍如下:

  • AndroidManifest.xml:描述文件
  • res:资源文件
  • smail:反编译出来的所有代码,语法与java不同,类似汇编,是Android虚拟机所使用的寄存器语言
    到此我们想要的都有了。下一步就是实现我们想法的时候了。

修改原代码逻辑

我们只需要修改if后面的判断条件,设置为否即可if (!MainActivity.this.code.…),这样就成功绕过了条件约束。

接下来还有一部很重要那就是修改smali文件,找到MainActivity$1.smali这个文件用代码查看工具打开

找到这个if-eqz 修改成if-nez (nez对应为非,符号“!”),到这里要修改的部分都成功了,最后一步要做的就是重新打包了。当然对smali语法感兴趣的可以一起探讨学习。

重新打包

在apktool文件夹路径的cmd下输入:

1
2
apktool b [文件夹] -o test2.apk   #(test2为新apk名称,[文件夹]为对应的有修改需要打包的文件夹)
#例如:我当前就可以这样写 apktool b [E:\tools\apktool\app-release] -o test2.apk

至此,我们的目标apk文件已经生成,当然如果你想装到你自己手机上还需要重新签名一下。

重新签名

首先我们需要一个用于签名的.keystore文件,生成命令如下(这里我们假设生成的是demo.keystore)。

1
2
keytool -genkey -alias demo.keystore -keyalg RSA -validity 40000 -keystore demo.keystore
# (cmd到apktool文件夹下跟待签名的apk放同个文件夹内便于操作)

这里我们利用Java JDK提供的一个jarsigner进行签名,在刚才的cmd下继续操作,输入:

1
jarsigner -verbose -keystore demo.keystore test2.apk demo.keystore

以上。我们目的apk已经可以投入使用,如果需要更快更好的体验还需要进行一次字节对齐的操作(后续分析)。

参考:

https://blog.csdn.net/micaaa/article/details/82426710

什么是identityServer4

IdentityServer4是一个用于ASP.Net Core的OpenID Connect和OAuth 2.0框架

通过使用 IdentityServer4 你可以

认证服务

可以为你的应用(如网站、本地应用、移动端、服务)做集中式的登录逻辑和工作流控制。IdentityServer是完全实现了OpenID Connect协议标准。

单点登录登出(SSO)

在各种类型的应用上实现单点登录登出。

API访问控制

为各种各样的客户端颁发access token令牌,如服务与服务之间的通讯、网站应用、SPAS和本地应用或者移动应用。

联合网关

支持来自Azure Active Directory, Google, Facebook这些知名应用的身份认证,可以不必关心连接到这些应用的细节就可以保护你的应用

接下来我们用几个例子来快速入门IdentifyServer4

使用客户端授权模式 保护你的api资源

我们先来一个基础的应用场景,保护我们的Api资源

在这个场景中我们定义一个IdentityServer服务,一个API的客户端,一个要访问这个api的客户端。

验证是否成功是从IdentityServer获取一个访问令牌,然后用这个令牌来获得Api的访问权限。

IdentityServer

我们将要保护的资源定义在 Scopes 中,这是我们命名是 api1

1
2
3
4
5
6
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource> {
new ApiResource("api1", "我的 API")
};
}

定义能够访问api的客户端。
我们使用客户端密码(Client Secret)来认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",

// 没有交互性用户,使用 clientid/secret 实现认证。
AllowedGrantTypes = GrantTypes.ClientCredentials,

// 用于认证的密码
ClientSecrets =
{
new Secret("secret".Sha256())
},
// 客户端有权访问的范围(Scopes)
AllowedScopes = { "api1" }
}
};
}

Startup.cs中配置上IdentityServer

为了让 IdentityServer 使用你的 Scopes 和 客户端 定义,你需要向 ConfigureServices 方法中添加一些代码。你可以使用便捷的扩展方法来实现 —— 它们在幕后会添加相关的存储和数据到 DI 系统中:

1
2
3
4
5
6
7
8
9
10
public void ConfigureServices(IServiceCollection services)
{

services.AddControllers();
// 使用内存存储,密钥,客户端和资源来配置身份服务器。
services.AddIdentityServer().AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());

}

这个时候访问 http://localhost:5000/.well-known/openid-configuration 可以看到 发现文档

API

新建一个站点并添加控制器

1
2
3
4
5
6
7
8
9
10
[Route("identity")]
[Authorize()]
public class IdentityController : ControllerBase
{
[HttpGet()]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}

将 IdentityServer4.AccessTokenValidation NuGet 程序包添加到你的 API 项目

然后添加中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});


}
1
2
3
4
5
6
7
8
9
10
11
12
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme
, jwtOptions =>
{
jwtOptions.RequireHttpsMetadata = false;
jwtOptions.Authority = "http://localhost:5000";
jwtOptions.ApiName = "api1";
});
}

将站点启动地址设置为 http://localhost:5001

这个时候,启动访问 http://localhost:5001/identity 可以看到401错误信息,说明访问还没有授权。

创建客户端

我们来编写一个客户端来请求访问令牌,然后使用这个令牌来访问 API。为此你需要为你的解决方案添加一个控制台应用程序。

IdentityServer 上的令牌端点实现了 OAuth 2.0 协议,你应该使用合法的 HTTP 来访问它。然而,我们有一个叫做 IdentityModel 的客户端库,它将协议交互封装到了一个易于使用的 API 里面。

添加 IdentityModel NuGet 程序包到你的客户端项目中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static async System.Threading.Tasks.Task Main(string[] args)
{


var client = new HttpClient();
var tokenClient = new TokenClient(client, new TokenClientOptions
{
Address = "http://localhost:5000/connect/token",
ClientId = "client",
ClientSecret = "secret"
});
var response = await tokenClient.RequestClientCredentialsTokenAsync("api1");

Console.WriteLine(response.AccessToken);
client.SetBearerToken(response.AccessToken);
string resStr = await client.GetStringAsync("http://localhost:5001/identity");
Console.WriteLine(resStr);
Console.ReadLine();
}

我们同时启动三个项目,可以看到 http://localhost:5001/identity 返回的结果内容被正常的打印在控制台。
默认情况下访问令牌将包含 scope 身份信息,生命周期(nbf 和 exp),客户端 ID(client_id) 和 发行者名称(iss)

使用密码保护API资源

我们现在使用 客户端发送用户名和密码到令牌服务并获得一个表示该用户的访问令牌 access_token

添加用户

为了方便演示 我们直接使用IdentityServer中的TestUser 类型表示一个测试用户及其身份信息。让我们向配置类(如果你有严格按照顺序进行演练,那么配置类应该在 QuickstartIdentityServer 项目的 Config.cs 文件中)中添加以下代码以创建一对用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static List<TestUser> GetUsers()
{
return new List<TestUser>()
{
new TestUser
{
SubjectId="1",
Username="爱丽丝",
Password="password"
},
new TestUser
{
SubjectId="2",
Username="博德",
Password="password"
}
};
}

然后将测试用户注册到 IdentityServer

1
2
3
4
5
6
7
8
9
10
11
public void ConfigureServices(IServiceCollection services)
{

services.AddControllers();
// 使用内存存储,密钥,客户端和资源来配置身份服务器。
services.AddIdentityServer().AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());

}

AddTestUsers 扩展方法在背后做了以下几件事:

  • 为资源所有者密码授权添加支持
  • 添加对用户相关服务的支持,这服务通常为登录 UI 所使用(我们将在下一个快速入门中用到登录 UI)
  • 为基于测试用户的身份信息服务添加支持(你将在下一个快速入门中学习更多与之相关的东西)

为资源所有者密码授权添加一个客户端定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Client
{
ClientId = "ro.client",

// 没有交互性用户,使用 clientid/secret 实现认证。
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

// 用于认证的密码
ClientSecrets =
{
new Secret("secret".Sha256())
},
// 客户端有权访问的范围(Scopes)
AllowedScopes = { "api1" }
}

使用密码授权请求一个令牌

客户端看起来跟之前 客户端凭证授权 的客户端是相似的。主要差别在于现在的客户端将会以某种方式收集用户密码,然后在令牌请求期间发送到令牌服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using (var client = new HttpClient())
{
var tokenClient = new TokenClient(client, new TokenClientOptions
{
Address = "http://localhost:5000/connect/token",
ClientId = "ro.client",
ClientSecret = "secret"
});
var response = await tokenClient.RequestPasswordTokenAsync("爱丽丝", "password", "api1");

Console.WriteLine(response.AccessToken);
client.SetBearerToken(response.AccessToken);
string resStr = await client.GetStringAsync("http://localhost:5001/identity");
Console.WriteLine(resStr);
Console.ReadLine();
}

当你发送令牌到身份 API 端点的时候,你会发现与客户端凭证授权
相比,资源所有者密码授权有一个很小但很重要的区别。访问令牌现在将包含一个 sub 信息,该信息是用户的唯一标识。sub 信息可以在调用 API 后通过检查内容变量来被查看,并且也将被控制台应用程序显示到屏幕上。

sub 信息的存在(或缺失)使得 API 能够区分代表客户端的调用和代表用户的调用

基于 OpenID Connect 的用户认证

OpenID Connect 1.0是OAuth 2.0协议之上的一个简单的身份层。 它允许客户端基于授权服务器执行的身份验证来验证最终用户的身份,以及以可互操作和类似REST的方式获取关于最终用户的基本配置文件信息。 OpenID Connect允许所有类型的客户端(包括基于Web的移动和JavaScript客户端)请求和接收关于认证会话和最终用户的信息。 规范套件是可扩展的,允许参与者使用可选功能,例如身份数据的加密,OpenID提供商的发现和会话管理

OpenID Connect 在OAuth2上构建了一个身份层,是一个基于OAuth2协议的身份认证标准协议。我们都知道OAuth2是一个授权协议,它无法提供完善的身份认证功能,OpenID Connect 使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,且可以适用于各种类型的客户端(比如服务端应用,移动APP,JS应用,winform应用),且完全兼容OAuth2,也就是说你搭建了一个OpenID Connect 的服务后,也可以当作一个OAuth2的服务来用。

csharp-dotnet-core-identityServer4-2020419134350

### 新建一个 MVC的Ids

IdentityServer提供了 可以快速创建UI的脚本 https://github.com/IdentityServer/IdentityServer4.Quickstart.UI

然后在start.cs中

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Quickstart.UI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace IdentityServer4DotNet.ClientAuth.Ids4s
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();

// configures IIS out-of-proc settings (see https://github.com/aspnet/AspNetCore/issues/14882)
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});

// configures IIS in-proc settings
services.Configure<IISServerOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});

var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddTestUsers(TestUsers.Users);

// in-memory, code config
builder.AddInMemoryIdentityResources(Config.Ids);
builder.AddInMemoryApiResources(Config.Apis);
builder.AddInMemoryClients(Config.Clients);

// or in-memory, json config
//builder.AddInMemoryIdentityResources(Configuration.GetSection("IdentityResources"));
//builder.AddInMemoryApiResources(Configuration.GetSection("ApiResources"));
//builder.AddInMemoryClients(Configuration.GetSection("clients"));

// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();

}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();

app.UseRouting();

app.UseIdentityServer();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

注意现在的中的Clients添加一个client

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
   // mvc client, authorization code
new Client
{
ClientId = "mvc client",
ClientName = "ASP.NET Core MVC Client",

AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
ClientSecrets = { new Secret("mvc secret".Sha256()) },

RedirectUris = { "http://localhost:5002/signin-oidc" },

FrontChannelLogoutUri = "http://localhost:5002/signout-oidc",
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

AlwaysIncludeUserClaimsInIdToken = true,

AllowOfflineAccess = true, // offline_access
AccessTokenLifetime = 60, // 60 seconds

AllowedScopes =
{
"api1",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Address,
IdentityServerConstants.StandardScopes.Phone,
IdentityServerConstants.StandardScopes.Profile
}
},

新建一个 MVC 客户端

接下来你将向解决方案添加一个 MVC 应用程序,可以使用 ASP.NET Core “Web 应用程序” 模板来实现。 将应用程序配置为使用 5002 端口(可以查看概览部分以了解如何配置)。

为了能向 MVC 应用程序添加 OpenID Connect 认证支持,请添加如下 NuGet 程序包:

Microsoft.AspNetCore.Authentication.Cookies

Microsoft.AspNetCore.Authentication.OpenIdConnect

mvc的client的start.cs如下

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Threading.Tasks;
using IdentityModel;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace IdentityServer4DotNet.ClientAuth.MVCClient
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc client";
options.ClientSecret = "mvc secret";
options.SaveTokens = true;
options.ResponseType = "code";

options.Scope.Clear();
options.Scope.Add("api1");
options.Scope.Add(OidcConstants.StandardScopes.OpenId);
options.Scope.Add(OidcConstants.StandardScopes.Profile);
options.Scope.Add(OidcConstants.StandardScopes.Email);
options.Scope.Add(OidcConstants.StandardScopes.Phone);
options.Scope.Add(OidcConstants.StandardScopes.Address);
options.Scope.Add(OidcConstants.StandardScopes.OfflineAccess);
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCookiePolicy();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

现在我们多启动项目,看到mvcclient自动跳转到 ids网站,输入testuser的账号密码之后看到如下界面

csharp-dotnet-core-identityServer4-2020579280

Yes,Allow

然后我们看到看到跳转到了

csharp-dotnet-core-identityServer4-20205792942

到此我们的 基本 OpenID Connect 的用户认证完成

尝试一个外部认证

接下来我们以Google身份认证为例,添加对Google的支持

首先我们需要在Google的开发人员控制台申请 OAuth 2.0 客户端 ID
回调地址填写 http://localhost:5000/signin-google

然后在ids中添加nuget引用 Microsoft.AspNetCore.Authentcation.Google 到项目中
然后我们在中间件中添加引用

1
2
3
4
5
6
7
8
9
10
11
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to http://localhost:5000/signin-google
options.ClientId = "629456249245-4vf1gvlltllq592coo46hrc8ofqa8cg6.apps.googleusercontent.com";
options.ClientSecret = "XhUa6NODSIGOtSPBuGrTeeeW";
});

注意:在 ASP.NET Core Identity 中使用外部认证的时候,SignInScheme 必须设置为 Identity.External,而不是 IdentityServerConstants.ExternalCookieAuthenticationScheme。

再次运行项目可以看到登录页面是多了个Google按钮。

通过认证后,你可以看到现在的身份信息是由 Google 数据提供的了。

更多实验

你可以使用 QQ认证,微信认证等来实验 IdentityServer4的神奇之处。

当然 你也可以在 https://github.com/IdentityServer/IdentityServer4/tree/master/samples 中看到更多开源的例子。

什么是Quartz

 Quartz.Net是根据Java的Quartz用C#改写而来,源码在https://github.com/quartznet/quartznet 。主要作用是做一些周期性的工作,或者定时工作。比如每天凌晨2点对前一天的数据统计。

Quartz 核心概念

我们需要明白 Quartz 的几个核心概念,这样理解起 Quartz 的原理就会变得简单了。

  1. Job 表示一个工作,要执行的具体内容。

  2. JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。

  3. Trigger 代表一个调度参数的配置,什么时候去调。
    触发器常用的有两种:SimpleTrigger触发器和CronTrigger触发器。

    • SimpleTrigger:能是实现简单业务,如每隔几分钟,几小时触发执行,并限制执行次数。

      1
      2
      3
      4
      5
      var trigger = TriggerBuilder.Create()
      .WithSimpleSchedule(x => x.WithIntervalInSeconds(2).WithRepeatCount(5))//间隔2秒 执行6次
      .UsingJobData("key1", 321)
      .WithIdentity("trigger", "group")
      .Build();
    • CronTrigger:Cron表达式包含7个字段,秒 分 时 月内日期 月 周内日期 年(可选)。

      • csharp-dotnet-core-Quartz-202041712646

      • csharp-dotnet-core-Quartz-20204171272

      • 举例

        1
        2
        3
        4
        5
        6
        var trigger = TriggerBuilder.Create()
        .WithCronSchedule("0 0 0 1 1 ?")// 每年元旦1月1日 0 点触发
        .UsingJobData("key1", 321)
        .UsingJobData("key2", "trigger-key2")
        .WithIdentity("trigger4", "group14")
        .Build();
  1. Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。

代码案例

以WebApi项目举例,用VS脚手架功能新建WebApi项目。

注册ISchedulerFactory的实例

1
2
3
4
5
6
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();//注册ISchedulerFactory的实例。
}

Controller中

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Quartz;

namespace QuartzNet.Web.Controllers
{
[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
private readonly ISchedulerFactory _schedulerFactory;
private IScheduler _scheduler;
public ValuesController(ISchedulerFactory schedulerFactory)
{
this._schedulerFactory = schedulerFactory;
}
[HttpGet]
public async Task<string[]> Get()
{
//1、通过调度工厂获得调度器
_scheduler = await _schedulerFactory.GetScheduler();
//2、开启调度器
await _scheduler.Start();
//3、创建一个触发器
var trigger = TriggerBuilder.Create()
.WithSimpleSchedule(x => x.WithIntervalInSeconds(2).RepeatForever())//每两秒执行一次
.Build();
//4、创建任务
var jobDetail = JobBuilder.Create<MyJob>()
.WithIdentity("job", "group")
.Build();
//5、将触发器和任务器绑定到调度器中
await _scheduler.ScheduleJob(jobDetail, trigger);
return await Task.FromResult(new string[] { "value1", "value2" });
}
}
}

MyJob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using Quartz;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace QuartzNet.Web
{
public class MyJob : IJob//创建IJob的实现类,并实现Excute方法。
{
public Task Execute(IJobExecutionContext context)
{
return Task.Run(() =>
{
using (StreamWriter sw = new StreamWriter(@"error.log", true, Encoding.UTF8))
{
sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss"));
}
});
}
}
}

可以看到在error.log中输出结果

1
2
3
4
5
6
2020-04-17 11-55-45
2020-04-17 11-55-47
2020-04-17 11-55-49
2020-04-17 11-55-51
2020-04-17 11-55-53
2020-04-17 11-55-55

你可以给Job增加参数

  1. 在Trigger中添加参数值
1
2
3
4
5
6
var trigger3 = TriggerBuilder.Create()
.WithSimpleSchedule(x =>x.WithIntervalInSeconds(2).RepeatForever())//间隔2秒 一直执行
.UsingJobData("key1", 321) //通过在Trigger中添加参数值
.UsingJobData("key2", "123")
.WithIdentity("trigger2", "group1")
.Build();
  1. 在Job中添加参数值
1
2
3
4
5
IJobDetail job = JobBuilder.Create<MyJob>()
.UsingJobData("key1", 123)//通过Job添加参数值
.UsingJobData("key2", "123")
.WithIdentity("job1", "group1")
.Build();

在job中获取参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
var jobData = context.JobDetail.JobDataMap;//获取Job中的参数

var triggerData = context.Trigger.JobDataMap;//获取Trigger中的参数

var data = context.MergedJobDataMap;//获取Job和Trigger中合并的参数

var value1= jobData.GetInt("key1");
var value2= jobData.GetString("key2");

var dateString = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss");return Task.Run(() =>
{
using (StreamWriter sw = new StreamWriter(@"error.log", true, Encoding.UTF8))
{
sw.WriteLine(dateString);
}
});
}
}

当Job中的参数和Trigger中的参数名称一样时,用 context.MergedJobDataMap获取参数时,Trigger中的值会覆盖Job中的值。

  1. 上面那种情况只能适应那种,参数值不变的情况。假如有这种情况,这次的参数值是上一次执行后计算的值,就不能使用上面方法了。如 每两秒实现累加一操作,现在初始值是0,如果按照上面那种获取值的操作,一直都是0+1,返回值一直都是1。为了满足这个情况,只需要加一个特性[PersistJobDataAfterExecution]。
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
[PersistJobDataAfterExecution]//更新JobDetail的JobDataMap的存储副本,以便下一次执行这个任务接收更新的值而不是原始存储的值
public class MyJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
var jobData = context.JobDetail.JobDataMap;
var triggerData = context.Trigger.JobDataMap;
var data = context.MergedJobDataMap;

var value1 = jobData.GetInt("key1");
var value2 = jobData.GetString("key2");
var value3 = data.GetString("key2");

var dateString = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss");
Random random = new Random();

jobData["key1"] = random.Next(1, 20);//这里面给key赋值,下次再进来执行的时候,获取的值为更新的值,而不是原始值
jobData["key2"] = dateString;

return Task.Run(() =>
{
using (StreamWriter sw = new StreamWriter(@"C:\Users\Administrator\Desktop\error.log", true, Encoding.UTF8))
{
sw.WriteLine($"{dateString} value1:{value1} value2:{value2}");
}
//context.Scheduler.DeleteJob(context.JobDetail.Key);
//context.Scheduler.Shutdown();
});
}
}

上面的例子是在api中使用Quartz

你也可以在 startup.cs中注册代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public void ConfigureServices(IServiceCollection services)
{
services.AddQuartz(typeof(QuartzJob));

//........
}


public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
QuartzService.StartJobs<QuartzJob>();
//........
}

或者在 HostedService中注册代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HostedService : IHostedService
{
private JobManager _jobManager;
public HostedService( JobManager jobManager)
{
_jobManager = jobManager;
}

public Task StartAsync(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(async () =>
{
await _jobManager.Start();
}, cancellationToken);
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(async () =>
{
await _jobManager.Stop();
}, cancellationToken);
}
}

参考:

https://www.cnblogs.com/MicroHeart/p/9402731.html
https://www.cnblogs.com/dangzhensheng/p/10496278.html

什么是Consul

Consul是一个服务网格(微服务间的 TCP/IP,负责服务之间的网络调用、限流、熔断和监控)解决方案,它是一个一个分布式的,高度可用的系统,而且开发使用都很简便。它提供了一个功能齐全的控制平面,主要特点是:服务发现、健康检查、键值存储、安全服务通信、多数据中心。

Consul是用Go开发的分布式服务协调管理的工具,它提供了服务发现,健康检查,Key/Value存储等功能,并且支持跨数据中心的功能。consul提供的一些关键特性:

  • service discovery:consul通过DNS或者HTTP接口使服务注册和服务发现变的很容易,一些外部服务,例如saas提供的也可以一样注册。
  • health checking:健康检测使consul可以快速的告警在集群中的操作。和服务发现的集成,可以防止服务转发到故障的服务上面。
  • key/value storage:一个用来存储动态配置的系统。提供简单的HTTP接口,可以在任何地方操作。
  • multi-datacenter:无需复杂的配置,即可支持任意数量的区域。

Consul的应用

服务注册与发现

其实服务注册与发现的原理很简单。

当我们在本机运行Consul时,他会自动监听8500端口;然后我们通过一个开源类库(这个开源类库可以在nuget上检索到,文章下面会介绍),调用其下不同的方法来向这个Consul进程发送TCP消息,来注册服务或者发现服务。

Consul进程在接收到注册消息时,就把注册的服务信息存储到本地磁盘或内存(因为我没有具体去调查Consul存储数据是否使用了数据库,但我们都知道数据库的数据也是保存在本地磁盘的,所以,它肯定是把数据存进磁盘或者内存中了)。

数据中心

Consul存储数据的地方,官方为其命名为数据中心,也就是上面说的保存我们注册的服务信息的本地磁盘或者内存。

Consul提供负载均衡的集群

Consul的集群也很好理解,在我们成功启动Consul以后,它除了监听8500端口以外,它还监听了一个8031端口。

这个8031端口就是用于Consul集群相互通信的。

我们都知道集群是要两台以上的电脑的,所以,我们就必须找到两台或以上的电脑安装Consul中间件。

然后,使用Consul的命令行,将两台电脑连接到一起,这样集群就形成了。

在集群内每台电脑上安装的Consul中间件,我们统称为服务器代理(Agent);当集群启动后,会在多个代理服务器之间选举出一个Leader。

选举Leader自然就是服务器代理之间的通信了,也就是通过上面提到的8031端口通信的。

选举了Leader,服务器代理就可以将自身的负载信息发送给Leader了,这样客户端调用Consul检索服务数据时,就可以去性能最优的那台机器上获取信息了。(注:这个就是举例说明,并非Consul的负载均衡的真实处理模式)

代码例子

服务注册与发现

我们新建一个 api Service

在 startup.cs 中的 Configure 增加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
string ip = Configuration["ip"];//部署到不同服务器的时候不能写成127.0.0.1或者0.0.0.0,因为这是让服务消费者调用的地址
int port = int.Parse(Configuration["port"]);//获取服务端口
var client = new ConsulClient(ConfigurationOverview); //回调获取
var result = client.Agent.ServiceRegister(new AgentServiceRegistration()
{
ID = "ServerNameFirst" + Guid.NewGuid(),//服务编号保证不重复
Name = "MsgServer",//服务的名称
Address = ip,//服务ip地址
Port = port,//服务端口
Check = new AgentServiceCheck //健康检查
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后反注册
Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳间隔(定时检查服务是否健康)
HTTP = $"http://{ip}:{port}/api/Health",//健康检查地址
Timeout = TimeSpan.FromSeconds(5)//服务的注册时间
}
});
1
2
3
4
5
6
7
private static void ConfigurationOverview(ConsulClientConfiguration obj)
{
//consul的地址
obj.Address = new Uri("http://192.168.1.37:8500");
//数据中心命名
obj.Datacenter = "dc1";
}

客户端调用

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
static List<string> Urls = new List<string>();

static void Main(string[] args)
{
Console.WriteLine("开始输出当前所有服务地址");
Catalog_Nodes().GetAwaiter().GetResult();
//Console.WriteLine(HelloConsul().GetAwaiter().GetResult());
Console.WriteLine("开始随机请求一个地址服务地址");
int index = new Random().Next(Urls.Count);
string url = Urls[index];

Console.WriteLine("请求的随机地址:" + url);
HttpClient client = new HttpClient();
var result = client.GetAsync(url).Result;
//string result = HttpClientHelpClass.PostResponse(url, param, out string statusCode);
Console.WriteLine("返回状态:" + result.StatusCode);
Console.WriteLine("返回结果:" + result.Content);
Console.ReadLine();
}
public static async Task Catalog_Nodes()
{
var client = new ConsulClient(ConfigurationOverview);
var nodeList = await client.Agent.Services();
var url = nodeList.Response.Values;

foreach (var item in url)
{
string Address = item.Address;
int port = item.Port;
string name = item.Service;
Console.WriteLine($"地址:{Address}:{port},name:{name}");
Urls.Add($"http://{Address}:{port}/weatherforecast");
}
}

private static void ConfigurationOverview(ConsulClientConfiguration obj)
{
//consul的地址
obj.Address = new Uri("http://192.168.1.37:8500");
//数据中心命名
obj.Datacenter = "dc1";
}

运行即可看到 我们已经成功调用了Consul,也成功的获取到了服务信息。

参考:
https://www.cnblogs.com/shanyou/archive/2015/08/09/4714838.html
https://www.cnblogs.com/kiba/p/11941731.html
https://www.cnblogs.com/yanbigfeg/p/9199590.html

在实际的云服务生产环境中,因为可以按量付费,经常会遇到硬盘不够了需要扩容的情况。

  1. 在云平台对对应的硬盘扩容操作

  2. 进入主机系统内,查看服务器的磁盘信息
    fidk -l 之后 找到 硬盘/dev/sda

  3. 使用分区工具parted分区
    parted /dev/sda
    然后使用: print free 查看 Free Space

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Model: Msft Virtual Disk (scsi)
    Disk /dev/sda: 322GB
    Sector size (logical/physical): 512B/4096B
    Partition Table: msdos
    Disk Flags:

    Number Start End Size Type File system Flags
    32.3kB 1049kB 1016kB Free Space
    1 1049kB 1075MB 1074MB primary xfs boot
    2 1075MB 136GB 135GB primary lvm
    136GB 322GB 186GB Free Space
  4. 使用mkpart开始分区
    mkpart test 186GB 100% 然后看到 /dev/sda3

  5. 使用fdisk调理分区为LVM
    fdisk /dev/sda然后依次t(change a partition's system id) 3 l(type L to list all codes) 8e(8e Linux LVM) w

  6. 使用pvcreate把/dev/sda3搞成物理卷
    pvcreate /dev/sda3

  7. 划分到 卷组
    vgextend centos /dev/sda3

  8. 划分到逻辑卷
    lvextend -l +100%FREE /dev/mapper/centos-home

  9. 保存此次操作
    xfs_growfs /dev/mapper/centos-home

如果你的pv在create的时候没有使用分区是话可以使用 pvresize 调整一个卷组中的物理卷的大小

1
pvresize /dev/sdc

然后添加到 lv中

lvextend -l +100%FREE /dev/mapper/testvg-testlv

最后 resize 一下

resize2fs /dev/mapper/testvg-testlv

说明:当有在操作系统时发现磁盘空间不足时,可以通过增加磁盘空间大小来满足,但是以哪种方式来增加可能有不同方法,现在针对操作系统在安装采用LVM方式来动态调整磁盘空间大小,

知识点解释:

LVM是逻辑盘卷管理(Logical VolumeManager)的简称,它是Linux环境下对磁盘分区进行管理的一种机制,LVM是建立在硬盘和分区之上的一个逻辑层,来提高磁盘分区管理的灵活性。通过LVM系统管理员可以轻松管理磁盘分区,如:将若干个磁盘分区连接为一个整块的卷组(volumegroup),形成一个存储池。管理员可以在卷组上随意创建逻辑卷组(logicalvolumes),并进一步在逻辑卷组上创建文件系统。管理员通过LVM可以方便的调整存储卷组的大小,并且可以对磁盘存储按照组的方式进行命名、管理和分配

Linux的LVM非常强大,可以在生产运行系统上面直接在线扩展硬盘分区,可以把分区umount以后收缩分区大小,还可以在系统运行过程中把一个分区从一块硬盘搬到另一块硬盘上面去等等,简直就像变魔术,而且这一切都可以在一个繁忙运行的系统上面直接操作,不会对你的系统运行产生任何影响,很安全。

LVM使用有局限性。虽然能很方便的扩容和缩容磁盘的空间(扩容磁盘大小,文件不丢失),但是一旦出现问题,数据丢失,想要恢复数据就有点困难!

三步:

  • 创建一个物理分区-搞成物理卷
  • 制作成一个卷组
  • 划分成逻辑卷
    linux-centos7-LVM-2020415144543
  1. 如果在安装系统选择是以LVM方式安装的话,可以通过已经安装好后系统磁盘查询
1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 477M 0 477M 0% /dev
tmpfs 488M 0 488M 0% /dev/shm
tmpfs 488M 6.6M 481M 2% /run
tmpfs 488M 0 488M 0% /sys/fs/cgroup
/dev/mapper/centos-root 50G 1.9G 49G 4% /
/dev/sda1 1014M 162M 853M 16% /boot
/dev/mapper/centos-home 74G 33M 74G 1% /home
/dev/sdb1 2.0T 81M 1.9T 1% /data
tmpfs 98M 0 98M 0% /run/user/0

以上说明 centos-root 和centos-home 是通过LVM方式来配置的
2. 使用df -T -h命令查看操作系统的文件类型

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# df -T -h
Filesystem Type Size Used Avail Use% Mounted on
devtmpfs devtmpfs 477M 0 477M 0% /dev
tmpfs tmpfs 488M 0 488M 0% /dev/shm
tmpfs tmpfs 488M 6.6M 481M 2% /run
tmpfs tmpfs 488M 0 488M 0% /sys/fs/cgroup
/dev/mapper/centos-root xfs 50G 1.9G 49G 4% /
/dev/sda1 xfs 1014M 162M 853M 16% /boot
/dev/mapper/centos-home xfs 74G 33M 74G 1% /home
/dev/sdb1 ext4 2.0T 81M 1.9T 1% /data
tmpfs tmpfs 98M 0 98M 0% /run/user/0

说明:通过查询发现操作系统的文件格式是:xfs

  1. vgdisplay:查看卷组名称及卷组使用情况
    VG Size –总共的空间大小

  2. lvdisplay:查看当前逻辑卷的空间状态

  3. 现在感觉 /dev/centos/home的73.99G不够用,想扩容增加10G新插入一块10G的硬盘;

    • fdisk -l 查看 得知新硬盘是 /dev/sdc

      1
      2
      3
      4
      Disk /dev/sdc: 10.7 GB, 10737418240 bytes, 20971520 sectors
      Units = sectors of 1 * 512 = 512 bytes
      Sector size (logical/physical): 512 bytes / 4096 bytes
      I/O size (minimum/optimal): 4096 bytes / 4096 bytes
    • 格式化/dev/sdc

      fdisk /dev/sdc 然后依次 n p 回车 回车 w

    • 将 /dev/sdc配置为LVM格式

      fdisk /dev/sdc 然后依次 t(change a partition’s system id) l(type L to list all codes) 8e(8e Linux LVM) w

    • 查询配置为LVM格式后结果如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      Disk /dev/sdc: 10.7 GB, 10737418240 bytes, 20971520 sectors
      Units = sectors of 1 * 512 = 512 bytes
      Sector size (logical/physical): 512 bytes / 4096 bytes
      I/O size (minimum/optimal): 4096 bytes / 4096 bytes
      Disk label type: dos
      Disk identifier: 0xd87afe50

      Device Boot Start End Blocks Id System
      /dev/sdc1 2048 20971519 10484736 8e Linux LVM
    • 将新加的分区 /dev/sdc1 创建为物理卷

      root@localhost ~]# pvcreate /dev/sdc1
      Physical volume “/dev/sdc1” successfully created.

    • .给卷组‘centos ’扩容,将物理卷 /dev/sdc1 扩展至‘centos ’卷组

      vgextend centos /dev/sdc1

    • 此时卷组‘centos ’有10G空余空间,及 /dev/sdc1,将其全部扩展至 /home

      lvextend -l +100%FREE /dev/mapper/centos-home

    • 用xfs_growfs 命令把文件写入系统中,使扩容生效

      xfs_growfs /dev/mapper/centos-home

    • 关于把文件系统缩小!

      如果文件系统是xfs格式的,fs文件系统只支持增大分区空间的情况,不支持减小的情况(谨记谨记谨记)

      硬要减小的话,只能在减小后将逻辑分区重新通过mkfs.xfs命令重新格式化才能挂载上,这样的话这个逻辑分区上原来的数据就丢失了。记得备份原来数据。

      也就是说当执行过 xfs_growfs 之后 是无法在不丢失数据是情况下 执行lvreduce -L -10G /dev/mapper/centos-home 缩小分区的。

本次扩容指令汇总:

  1. 创建分区 #fdisk /dev/sdb

  2. 创建物理卷 #pvcreat /dev/sdb1

  3. 查看卷组名称及使用情况 #vgdisplay

  4. 将物理卷扩展到卷组 #vgextend cl /dev/sdb1 (此处‘cl’是卷组名称)

  5. 将卷组中空闲空间扩展到 /home #lvextend -l +100%FREE /dev/mapper/cl-home

  6. 刷新文件系统是扩容生效 xfs_growfs /dev/mapper/centos-root

  7. 若是ext4文件格式使用扩容生效 #resize2fs /dev/mapper/centos-home

解决群晖高温自动关机的问题

群晖总是在超过60度的时候shutdown 可以参考使用 这篇帖子 来解决

具体步骤

  1. 使用ssh到群晖上 打开 /usr/syno/etc.defaults/scemd.xml 文件

  2. 查找文本

    1
    2
    找到如下内容的文本 关键字`temperature` 和 `SHUTDOWN` 
    <disk_temperature fan_speed="99%00hz" action="SHUTDOWN">61</disk_temperature>
  3. 把其中的 61 或者 58 的数字 修改成90

  4. 保存文件 然后重启就好了