0%

国庆假期玩了下Arch Linux,发现这货跟Ubuntu之流相差甚远,甚难调教,而且安裝过程全命令行,会有各种问题,各种知识。。。


-– 安装引导器
-– —————————

  • GRUB

    • BIOS

      1
      2
      3
      # pacman -S grub os-prober
      # grub-install --recheck /dev/<目标磁盘>
      # grub-mkconfig -o /boot/grub/grub.cfg
    • UEFI:—如果BIOS是UEFI的,就要用下面的命令安装grub了

  • 其他引导器


+++ 分区

+++ +++++++++++++++++
在本节,用户须在 MBR 和 GPT 之间做出选择。通常建议在 UEFI 系统中使用 GPT,在 BIOS 系统中使用 MBR。注意部分 UEFI 系统不支持 MBR 引导,部分 BIOS 系统不支持 GPT 引导。

  1. -–
    -– 分区方案
    -– —————————
    基本要求:

    • 至少一个分区(类型代码:8300)

    特殊要求:

    • BIOS + GPT + Grub:BIOS 引导分区(类型代码:ef02;大小 ≥ 1 MiB)

    • UEFI:UEFI 系统分区(类型代码:ef00;大小 ≥ 256 MiB)

    • 系统休眠:交换分区(类型代码:8200;大小 ≥ 2×内存大小)

    一般建议:

    • 若安装目标内存 ≤ 2GB,添加一个交换分区

    • 为 /home 分配一个分区

    一个例子:

    1
    2
    3
    4
    5
    6
    7
    $ lsblk /dev/sda -o NAME,FSTYPE,SIZE,MOUNTPOINT    # 名称,文件系统,大小,挂载点
    NAMEFSTYPESIZEMOUNTPOINT
    sda120G
    ├─sda1vfat512M/boot/EFI
    ├─sda2ext436G/
    ├─sda3ext479.5Ghome
    └─sda4swap4G[SWAP]
  2. -–
    -– 分区
    -– —————————
    确定目标磁盘及目标分区:

    分区用软件:

    • 通用:**partedcfdisk**、sfdisk

    • 仅 GPT:cgdisk_、_sgdisk

    • 仅MBR:fdisk

具体用法:

1.利用 fdisk 创建 MBR 分区:
$ fdisk /dev/sda

2.利用 cgdisk 创建 GPT 分区:

$ cgdisk /dev/sda

3.利用 parted创建 GPT 分区:

parted是一个可以分区并进行分区调整的工具,他可以创建,破坏,移动,复制,调整ext2 linux-swap fat fat32 reiserfs类型的分区,可以创建,调整,移动Macintosh的HFS分区,检测jfs,ntfs,ufs,xfs分区。

    使用方法:_parted [options] [device [command [options…]…]]_

    options

    -h  显示帮助信息

    -l  显示所有块设备上的分区

    device

    对哪个块设备进行操作,如果没有指定则使用第一个块设备

    command [options…]

    check partition  

    对分区做一个简单的检测

    cp [source-device] source dest  

    复制source-device设备上的source分区到当前设备的dest分区

mklabel label-type

    创建新分区表类型,label-type可以是:”bsd”, “dvh”, “gpt”,  “loop”,”mac”, “msdos”, “pc98”, or “sun” 一般的pc机都是msdos格式,如果分区大于2T则需要选用gpt格式的分区表。

mkfs partition fs-type

    在partition分区上创建一个fs-type文件系统,fs-type可以是:”fat16”, “fat32”, “ext2”, “linux-swap”,”reiserfs” 注意不支持ext3格式的文件系统,只能先分区然后用专有命令进行格式化。

mkpart part-type [fs-type] start end

    创建一个part-type类型的分区,part-type可以是:”primary”, “logical”, or “extended” 如果指定fs-type则在创建分区的同时进行格式化。start和end指的是分区的起始位置,单位默认是M。

linux大于2T的磁盘使用GPT分区方式 - 吾心 - 51CTO技术博客
http://leeforget.blog.51cto.com/6950397/1375908

使用parted划分GPT分区 - 好脑袋和烂笔头 - 开源中国社区
https://my.oschina.net/guol/blog/61424

分区工具比较多,推荐 parted 或者 cfdisk,后者有个类似图形化一样的界面很方便。我用的是 parted,表问我为什么,逼格高=。=

parted /dev/sda

(parted) mklabel msdos

(parted) mkpart primary ext4 1M 500M

(parted) set 1 boot on

(parted) mkpart primary ext4 500M 50G

(parted) mkpart primary linux-swap 50G 54G

(parted) mkpart primary ext4 54G 100%

解释一下,先说一下 parted 的基本用法

(parted) mkpart part-type fs-type start end

进入 parted 交互界面后使用 mkpart 创建,后面跟上 4 个参数,分别是 分区类型、文件系统类型、起始点、结束点,分区类型就主分区还是逻辑分区,起始结束点使用 MB、GB 方便计算你懂的。

使用 parted 对 /dev/sda 设备进行分区,分区表 为 MS-DOS 即 MBR 分区结构。共分了4个区,个人习惯~

挂载点 大小 说明

-—————————————————————–

/boot 1-500M 用于挂载 /boot 分区,设置为 Bootable。

/ 500M-50G 用于挂载 / 分区

swap 50G-54G 用于交换分区(Swap)

/home 54G-100% 剩余空间用于挂载 /home分区

分完区后进行格式化

mkfs.ext4 /dev/sda1

mkfs.ext4 /dev/sda2

mkfs.ext4 /dev/sda4

mkswap /dev/sda3

挂载分区

mount /dev/sda2 /mnt

mkdir /mnt/{boot,home}

mount /dev/sda1 /mnt/boot

mount /dev/sda4 /mnt/home

swapon /dev/sda3

分区方案:

类型 大小 类型代码
/dev/sda1 EFI 系统分区 512M ef00
/dev/sda2 Linux ext4 任意 8300
/dev/sda3 Linux 交换分区 适量 8200

 4.利用 cfdisk创建 GPT 分区:

 cfdisk有一个终端图形界面,本质和parted一样,详细用法如下:

CentOS分区cfdisk 
http://blog.csdn.net/zhoutong12589/article/details/8210405


安装并配置 bootloader

我的主板是BIOS主板,这里采用的 bootloader 是Grub;安装 grub 包,并执行 grub-install 已安装到 MBR:
# pacman -S grub
# grub-install –target=i386-pc –recheck /dev/sdb

注意:须根据实际分区自行调整 /dev/sdb, 切勿在块设备后附加数字,比如 /dev/sdb1 就不对。
由于我的硬盘上还有另外一个操作系统windows 7,为了检测到该系统并写到grub启动项中,还需要做下面的操作。

# pacman -S os-prober
# grub-mkconfig -o /boot/grub/grub.cfg

卸载分区并重启系统

离开 chroot 环境并重启操作系统
# exit
# reboot

安装图形界面
桌面环境我用的是XFCE4,在安装之前需要装几个其它的东西。
安装xorg-server

# pacman -S xorg-server xorg-server-utils xorg-xinit

安装显卡驱动
我的笔记本是Intel的集成显卡和英伟达的独立显卡,这地方没弄清楚怎么回事,就安装了两个驱动;这里有一些相关描述:https://wiki.archlinux.org/index.php/NVIDIA\_(简体中文)

# pacman -S xf86-video-intel
# pacman -S xf86-video-nouveau
安装LXDM
LXDM是个桌面管理器,用来登录系统及启动XFCE桌面。

# pacman -S lxdm
# systemctl enable lxdm.service
安装XFCE4

# pacman -S xfce4

经过以上步骤,可以通过下面的命令来启动XFCE4了:

startxfce4


设置 root 用户密码
passwd root

添加用户
虽然你也可以直接用 root 用户,但是毕竟不安全,貌似有些软件还不能直接用 root ?

useradd -m -g users -G wheel -s /bin/bash ikke
passwd ikke

安装 sudo
要使用 sudo 命令提权的话需要安装 sudo 并且做相应配置

pacman -S sudo
打开 /etc/sudoers 文件,找到 root ALL=(ALL) ALL 并依葫芦画瓢添加 ikke ALL=(ALL) ALL 即可。

安装 bootloader
一般都是用 grub。

pacman -S grub
grub-install /dev/sda

由于我的硬盘上还有另外一个操作系统windows 7,为了检测到该系统并写到grub启动项中,还需要做下面的操作。

# pacman -S os-prober
# grub-mkconfig -o /boot/grub/grub.cfg

无线网设置

无线的话注意了,需要安装几个包不然无法使用。
pacman -S wpa_supplicant dialog


图形界面安装

下面开始安装图形界面

Xorg

首先是装Xorg

  1. pacman -S xorg-server xorg-xinit

触摸板驱动

  1. pacman -S xf86-input-synaptics

显卡驱动

下面安装显卡驱动
intel

  1. pacman -S xf86-video-intel

英伟达

  1. pacman -S xf86-video-nv

ATi

  1. pacman -S xf86-video-ati

我这里是双显卡,我只安装了intel的驱动

这个时候startx是不能进入x界面的,不过不用担心,请继续看后续教程。

GNOME桌面

gnome桌面只要安装gnome包即可,还有一个gnome-extra包可以提供额外的常用软件和几个游戏,你可以安装时选择你要的软件,没有必要全选,当然也可以不装这个包,我这里只选了gnome-tweak-tool这个工具

  1. pacman -S gnome gnome-extra

然后安装gdm登录管理器

  1. pacman -S gnome gdm

将gdm设置为开机自启动,这样开机时会自动载入桌面

  1. systemctl enable gdm

Deepin 桌面

Deepin 桌面安装:

pacman -S deepin deepin-extra lightdm

#软件

pacman -S file-roller evince gedit thunderbird gpicview

pacman -S unrar unzip p7zip

-– 说明,据说Deepin桌面和Gnome不兼容,安装Deepin就要卸载Gnome,以及重新配置网络等

KDE5桌面

安装plasma5

  1. 基础包
  2. pacman -S plasma
  3. 完整包
  4. pacman -S plasma-meta
  5. 最简安装(仅有桌面软件)
  6. pacman -S plasma-desktop

然后是登录管理器SDDM

  1. pacman -S sddm

将SDDM设置为开机自启动

  1. systemctl enable sddm

XFCE4桌面

安装LXDM
LXDM是个桌面管理器,用来登录系统及启动XFCE桌面。

# pacman -S lxdm
# systemctl enable lxdm.service

安装XFCE4

# pacman -S xfce4

经过以上步骤,可以通过下面的命令来启动XFCE4了:

startxfce4

LXDE桌面

安装LXDM管理器和LXDE桌面:
# pacman -S lxdm lxde

设置lxdm开机启动:
# systemctl enable lxdm

startx 启动图形界面

如果你不想开机自动进入桌面,可以使用startx启动桌面,如果要使用startx,就不用安装登录管理器。

  1. pacman -S xorg-xinit

对你的startx配置文件进行设置

  1. nano /etc/X11/xinit/xinitrc

文件最后有这样一段,删掉或者注释掉这些内容

  1. twm &
  2. xclock -geometry 50x50-1+1 &
  3. xterm -geometry 80x50+494+51 &
  4. xterm -geometry 80x20+494-0 &
  5. exec xterm -geometry 80x66+0+0 -name login

如果你使用的是gnome桌面,在最后添加

  1. exec gnome-session

如果是kde5,则添加

  1. exec startkde

然后保存文件,这时候,你已经可以使用startx来进入桌面了。
如果想要每个用户进入不同的桌面,你可以以用户身份登录,为用户复制一份单独的配置文件

  1. cp /etc/X11/xinit/xinitrc ~/.xinitrc

然后编辑~/.xinitrc即可

创建新用户

  1. useradd -m -G wheel -s /bin/bash 用户名

用户密码

  1. passwd 用户名

安装sudo

为安全期间,我们可以用sudo来使用root权限

  1. pacman -S sudo

将用户加入sudo组
/etc/sudoers加入这个:

  1. 用户名 ALL=(ALL) ALL

也可以去掉#%wheel ALL=(ALL) ALL这一行前面的#

中文字体

你需要中文字体才能使用gnome-terminal

  1. pacman -S wqy-microhei

现在你已经可以使用你的系统了,输入reboot重启后系统即可自动进入gdm界面,然后进入桌面

安装常用字体:
# pacman -S ttf-dejavu wqy-zenhei wqy-microhei

后续优化

yaourt

Yaourt是archlinux方便使用的关键部件之一,但没有被整合到系统安装中的工具。建议在装完系统重启之后,更新完pacman和基本系统之后,就安装这个工具。
最简单安装Yaourt的方式是添加Yaourt源至您的 /etc/pacman.conf,在文件最后加入:

  1. [archlinuxcn]
  2. #The Chinese Arch Linux communities packages.
  3. SigLevel = Optional TrustAll
  4. Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch

然后

  1. pacman -Syu yaourt

中文输入法

这里安装ibus作为中文输入法

  1. sudo pacman -S ibus
  2. sudo pacman -S ibus-pinyin

在~/.bashrc里面加入

  1. export GTK_IM_MODULE=ibus
  2. export XMODIFIERS=@im=ibus
  3. export QT_IM_MODULE=ibus

然后在设置里启用输入法

你也可以安装fcitx:

  1. sudo pacman -S fcitx-im fcitx-configtool

同样的,在~/.bashrc写入

  1. export GTK_IM_MODULE=fcitx
  2. export QT_IM_MODULE=fcitx
  3. export XMODIFIERS=“@im=fcitx”

然后执行

  1. gsettings set \
  2. org.gnome.settings-daemon.plugins.xsettings overrides \
  3. "{'Gtk/IMModule':<'fcitx'>}"

安装搜狗输入法

  1. yaourt -S fcitx-sogoupinyin

然后进入fcitx设置进行配置即可

chrome

前面已经安装了yaourt,这里只要

  1. yaourt -S google-chrome

解压软件

需要图形化的解压软件可以这样:

  1. sudo pacman -S p7zip file-roller unrar

文件系统支持

要支持制作fat文件系统,安装dosfstools,默认内核只能读取ntfs,要支持ntfs读写,安装ntfs-3g。

  1. sudo pacman -S ntfs-3g dosfstools

无线AP

需要安装create-ap才能使用gnome3设置里的创建热点选项

  1. sudo pacman -S create_ap

美化

此处仅为gnome美化
这里先展示一下美化后的样子

gnome-tweak-tool

如果你安装了gnome-extra,那么这个工具已经被安装了,否则的话

  1. sudo pacman -S gnome-tweak-tool

图标包

这里我使用的numix-circle图标包,这个图标包在aur里,直接用yaourt即可

  1. yaourt -S numix-circle-icon-theme-git

然后在gnome-tweak-tool里启用主题

gtk主题

gtk主题我选择了arc主题

  1. yaourt -S gtk-theme-arc-git

然后在gnome-tweak-tool里启用

gnome-shell主题

首先在gnome-tweak-tool里的拓展里启用User themes

然后安装主题,这里我是用的贴吧的@Air_Wawei的Air主题,并自己做了些修改。

首先下载主题 然后解压,将Air文件夹放到/usr/share/themes/文件夹里,在gnome-tweak-tool里启用主题

gdm背景

输入以下指令

  1. curl -L -O http://archibold.io/sh/archibold
  2. chmod +x archibold
  3. ./archibold login-backgroung 你的背景的地址

重启后gdm就会变成你要的背景

gnome-shell拓展

shell拓展请进入https://extensions.gnome.org/自行按照说明安装

screenfetch

screenfetch可以在终端里输出你的系统logo和状态。

可以用pacman安装:

  1. pacman -S screenfetch

要让screenfetch在打开终端是自动输出,在~/.bashrc里加入

  1. screenfetch

dock

要获得像苹果osx一样的dock可以用docky或者dash-to-dock
docky的话sudo pacman -S docky即可,docky不支持wayland。
dash-to-dock是gnome拓展,请在https://extensions.gnome.org/自行按照说明安装
我这里装了docky,这是我的docky配置


一些优化

SATA 启用 AHCI 模式

SATA 有两种工作模式:原生的 AHCI 模式提供更好的性能(如热插拔和 NCQ 支持)、模拟的 IDE 模式提供更好的兼容性。一般主板出厂默认将 SATA 模式设置为 IDE 模式,但如今先进的 Linux 和 Windows 都早已原生支持 AHCI,所以我们最好打开 AHCI 模式以优化性能。

Arch Linux 在安装好以后,内核镜像默认没有载入 AHCI 驱动模块。修改 /etc/mkinitcpio.conf,添加ahci 到 MODULES 变量:

然后重建内核镜像,重新启动后 AHCI 驱动就会加载:

在主板 UEFI 或 BIOS 中,将 SATA 模式从 IDE(或 PATA Emulation 等等),设置为 AHCI(或 Native等等)。需要注意的是,如果你还在用 Windows XP,它需要安装 AHCI 驱动才行。Windows Vista 及以后的版本则不需要担心这个问题(但如果你是在 Windows 安装完成后才启用 AHCI 模式,因为安装期间 Windows 会自动禁用未使用的存储驱动程序,你需要参考 KB922976(Windows Vista/7)或KB2751461(Windows 8)来启用 AHCI 驱动程序)。

设置好以后,你可以从 dmesg 命令的输出里,找到 AHCI 和 NCQ 成功启用的证据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ dmesg
...
SCSI subsystem initialized
libata version 3.00 loaded.
ahci 0000:00:1f.2: version 3.0
ahci 0000:00:1f.2: irq 24 for MSI/MSI-X
ahci 0000:00:1f.2: AHCI 0001.0300 32 slots 6 ports 6 Gbps 0x10 impl SATA mode
ahci 0000:00:1f.2: flags: 64bit ncq led clo pio slum part ems apst
scsi host0: ahci
scsi host1: ahci
scsi host2: ahci
scsi host3: ahci
scsi host4: ahci
scsi host5: ahci
...
ata5.00: 976773168 sectors, multi 16: LBA48 NCQ (depth 31/32), AA
...

优化系统启动速度

Arch Linux 的 systemd-analyze 是个很不错的工具,利用它你可以很直观地观察到系统启动的时间都花到哪儿去了:

1
2
$ systemd-analyze
Startup finished in 6.857s (firmware) + 3.157s (loader) + 1.870s (kernel) + 8.157s (userspace) = 20.044s

我注意到打开 AHCI 后,内核和用户空间的载入速度明显提高了,总启动时间从约 30 秒缩短到 20 秒,效果非常明显。

用下面这个命令,可以了解到是什么东西启动最慢:

此外,还可以把启动过程绘制成 SVG 图表供你审阅(用 GNOME 的图片预览或 Chrome 浏览器都可以打开),这个图表中你还可以观察到是否有启动慢的组件影响到了依赖它的组件的启动:

1
$ systemd-analyze plot > plot.svg

Arch Linux 安装指南[2016.01] / 安装问题 / Arch Linux 中文论坛
https://bbs.archlinuxcn.org/viewtopic.php?id=1037

ArchLinux 安装笔记 | 雾里看花
https://blog.ikke.moe/posts/archlinux-installation-notes/

General recommendations (简体中文) - ArchWiki
https://wiki.archlinux.org/index.php/General\_recommendations\_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)

Main page (简体中文) - ArchWiki
https://wiki.archlinux.org/index.php/Main\_page\_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)

Zsh (简体中文) - ArchWiki
https://wiki.archlinux.org/index.php/Zsh\_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)

-–zsh 就是传说中超级强大,要用来替代Bash shell的存在

  没错,又是受够了迅雷、旋风的各种奇葩减速(哥哥我还买了了VIP!),IDM 对协议支持又太少还有事没事提示你不是正版三天两头闹着要更新。于是我想起来之前看到过的 Aria2,虽然之前也只是略有耳闻,但听名字就很牛掰的样子。下面文章以 Windows 系统为基础,不过基本内容都是通用的,Linux 用户也可以看下。

  

认识 Aria2

  Aria2 是一个轻量级多协议和多源命令行下载实用工具。它支持 HTTP / HTTPS, FTP, SFTP, bt 和 Metalink。通过内置 Aria2 可以操作 json - rpc 和 xml - rpc。对,Aria2 没有 GUI 图形界面,只有粗糙的命令行界面!但这也正是 Aria2 之轻快好省所在。

  • 轻:绿色便携,不写注册表,无 GUI,占用小,全速下载时占用大约为 10M。
  • 快:这货在我这里的(移动 4M)的下载速度居然稳定在 608 kb/s,逆天到破物理带宽限制了么?!!
  • 好:多协议支持,多脚本/扩展,多平台,完全免费,可定制型极强
  • 省:不仅占用小,存储空间也小,不到 5M。

  官方下载链接:http://sourceforge.net/projects/aria2/files/stable/

初步部署 Aria2 简易版

  本小节参考卡饭论坛:http://bbs.kafan.cn/thread-1686205-1-1.html

新建几个有关文件

  上面已经提供了下载链接,根据平台/系统位数(32bit/64bit)下载相应文件即可。(存放路径最好是英文/数字)然后在目录下以新建文本文档的方式新建几个文件,方便之后的使用。

  • Aria2.log(运行日志)
  • aria2.session(下载历史)
  • aria2.conf(配置文件)
  • HideRun.vbs (用来隐藏命令行窗口)

  如此一来,目录下就应该有这几个文件:

  而后用记事本修改配置文件 aria2.conf,有中文的地方要根据实际情况修改

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

dir``=默认下载目录(例:D:\Inbox)

log=日志文件存放目录(例:D:\Aria2\Aria2.log)

input-``file``=记录下载历史文件目录(例:D:\Aria2\aria2.session)

save-session=存放下载历史文件目录(例:同上)

save-session-interval=60

force-save=``true

log-level=error

max-concurrent-downloads=5

continue``=``true

max-overall-download-limit=0

max-overall-upload-limit=50K

max-upload-limit=20

connect-timeout=120

lowest-speed-limit=10K

max-connection-per-server=10

max-``file``-not-found=2

min-``split``-size=1M

split``=5

check-certificate=``false

http-no-cache=``true

bt-``enable``-lpd=``true

follow-torrent=``true

enable``-dht6=``false

bt-seed-unverified

rpc-save-upload-metadata=``true

bt-``hash``-check-seed

bt-remove-unselected-``file

bt-request-peer-speed-limit=100K

seed-ratio=0.0

enable``-rpc=``true

pause=``false

rpc-allow-origin-all=``true

rpc-listen-all=``true

rpc-save-upload-metadata=``true

rpc-secure=``false

daemon=``true

disable-ipv6=``true

enable``-mmap=``true

file``-allocation=falloc

max-download-result=120

force-sequential=``true

parameterized-uri=``true

  Tips:如果有时 Aria2 不能启动,清空 aria2.session 里面的内容就行了。

  而后修改 HideRun.vbs,将 Aria2c.exe 与配置文件 Aria2.conf 链接,并实现无命令行启动。那么日后打开 Aria2 就双击 HideRun.vbs 这个文件而不是双击 aria2c.exe。

1

CreateObject(``"WScript.Shell"``).Run "(程序所在目录 例:D:\Aria2\aria2c.exe) --conf-path=aria2.conf"``,0

Web 前端控制

  如此这般,Aria2 就配置好了,如果要添加开机自启动将 HideRun.vbs 的快捷方式拖入启动文件夹建立计划任务就 OK 了。那么问题就来了,这么一个看不见摸不着的软件怎么使用?别急,即使没有 GUI,Aria2 也还是有 Web 端控制界面的,目前比较知名的有 Aria2 Web UIYAAW

Tips:其他控制界面/扩展/脚本可能会需要填写 JSON-RPC Path,默认为: http://localhost:6800/jsonrpc

  简易版的 Aria2 至此就部署完毕,你可以在 Web 控制前段方便地添加下载链接/bt种子了。如要想要进一步使用 Aria2,往下读吧。

进阶 Aria2

配置文件 aria2.conf 详解

更多参数请参考官方说明文档:http://aria2.sourceforge.net/manual/en/html/aria2c.html

网友翻译的部分内容:  http://sydi.org/posts/linux/aria2c-usage-sample-cns.html#fn.1

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

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

dir``=~``/downloads

disk-cache=32M

file``-allocation=none

continue``=``true

check-intergrity=``false

help=

max-concurrent-downloads=1

max-connection-per-server=5

min-``split``-size=10M

split``=5

disable-ipv6=``true

input-``file``=``/etc/aria2/aria2``.session

save-session=``/etc/aria2/aria2``.session

enable``-rpc=``true

rpc-allow-origin-all=``true

rpc-listen-all=``true

listen-port=51413

enable``-dht=``false

enable``-peer-exchange=``false

peer-``id``-prefix=-TR2770-

user-agent=Transmission``/2``.77

seed-ratio=0

bt-seed-unverified=``true

bt-save-metadata=``true

新建链接任务进阶

  直接添加链接下载文件:

1

http://www.url.com/file.zip

  从多个地址源下载同一个文件(用空格隔开):

1

http://www.url1.com/file.zip www.url2.com/file.zip

  使用 n 个线程下载文件(”x2” 就是 2 个线程): 

1

-x2 http://www.url.com/file.zip

  从 txt 文本文档中获取下载链接下载文件:

1

-i http://www.url.com/url.txt

新建任务链接调用代理下载
  • 为所有的连接设置代理服务器(Set proxy server to use all protocols(HTTP(S)/FTP))

1

all-proxy=``'http://proxy:8080' http:``//url``.com``/file``.zip

Tip: -all-proxy 选项会被具体的代理选项重载: -http-proxy, -https-proxy, -ftp-proxy 。

  •   只为 HTTP 设置代理服务器(Set proxy server to be in HTTP only)

1

http-proxy=``'http://proxy:8080' http:``//www``.url.com``/file``.zip

  •   设置需要验证的代理服务器(Use proxy that requires authentication)

1

http-proxy=``'http://proxy:8080' --http-proxy-user=``'username' --http-proxy-``passwd``=``'password' http:``//www``.url.com``/file``.zip

1

http-proxy=``'http://username:password@proxy:8080' http:``//www``.url.com``/file``.zip

Tip:用户名和密码需要是 percent-encoded 格式。比如,如果用户名是 myid@domain, 那么 percent-encoded 格式就是 myid%40domain 。

搭配脚本/扩展

迅雷离线(需会员账号)

旋风离线

百度网盘

其他脚本

0 Asp.Net Core 项目实战之权限管理系统(0) 无中生有

1 Asp.Net Core 项目实战之权限管理系统(1) 使用AdminLTE搭建前端

2 Asp.Net Core 项目实战之权限管理系统(2) 功能及实体设计

3 Asp.Net Core 项目实战之权限管理系统(3) 通过EntityFramework Core使用PostgreSQL

4 Asp.Net Core 项目实战之权限管理系统(4) 依赖注入、仓储、服务的多项目分层实现

5 Asp.Net Core 项目实战之权限管理系统(5) 用户登录

6 Asp.Net Core 项目实战之权限管理系统(6) 功能管理

7 Asp.Net Core 项目实战之权限管理系统(7) 组织机构、角色、用户权限

8 Asp.Net Core 项目实战之权限管理系统(8) 功能菜单的动态加载

github源码地址

http://www.cnblogs.com/fonour/p/5848933.html

学习的最好方法就是动手去做,这里以开发一个普通的权限管理系统的方式来从零体验和学习Asp.net Core。项目的整体规划大致如下:

技术路线

  • Asp.net Core Mvc
  • EntityFrameworkCore
  • Bootstrap AdminLTE
  • PostgreSQL

实现功能

  • 组织机构管理
  • 角色管理
  • 用户管理
  • 功能管理
  • 权限管理

1、安装Visual Studio 2015,我这里安装的是专业版。

2、安装.NET Core SDK相关

需要安装**Visual Studio 2015 update3和NET Core 1.0.0 - VS 2015 Tooling**

打开Visual Studio 2015,选择项目保存路径,输入项目名称及解决方案名称,新建一个Asp.Net Core Web Application (.NET Core)项目。

1

为了更好的理解Asp.net Core的一些东西,我这里选择了空模板

2

创建好的项目是长这个样子的

3

解决方案中相关文件大致说明

  • wwwroot      存放js,css,images等静态资源
  • Program.cs   应用程序入口
  • Startup.cs    应用程序相关启动项配置,包含ConfigureServices和Configure两个方法,前者负责服务的配置,后者负责http请求管道的配置。
  • project.json  项目的基础配置文件

删除示例页面Project_Readme.html,此时我们直接F5运行程序,界面输出HelloWorld,这是因为Stratup.cs中

复制代码

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(); if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

        app.Run(async (context) => { await context.Response.WriteAsync("Hello World!");
        });
    }

复制代码

3.0 添加MVC依赖项引用

添加MVC引用有两种方法。

1 通过project.json

打开project.json文件,在dependencies部门增加Microsoft.AspNetCore.Mvc的引用,当你输入时,Visual Studio会有自动的提示,非常贴心方便。

复制代码

“dependencies”: {
“Microsoft.NETCore.App”: {
“version”: “1.0.0”,
“type”: “platform”
},
“Microsoft.AspNetCore.Diagnostics”: “1.0.0”,

"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.0"

},

复制代码

2 通过NuGet

通过NuGet管理器搜索Microsoft.AspNetCore.Mvc添加引用,或直接在程序包管理器控制台输入命令_Install_-Package Microsoft.AspNetCore._Mvc_即可完成Mvc引用的添加。

3.1 添加控制器

右键添加项目文件夹,命名为Controllers,右键Controllers文件夹,添加一个名字为HomeController的控制器。

1

3.2 添加视图

右键项目添加Views文件夹,右键Views文件夹添加一个Home文件夹,与HomeController控制器相对应,在Home文件夹下新建一个名称为Index的视图文件。

1

将Index.cshtml的内容修改为

<h1>Hello,Asp.Net Core MVC</h1>

3.3 添加MVC服务及Http请求管道处理

1 添加MVC服务

修改Startup.cs的ConfigureServices方法如下

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}

2 添加Http请求管道处理

修改Startup.cs的Configure方法如下

复制代码

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(); if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

        app.UseMvcWithDefaultRoute(); //app.Run(async (context) => //{ // await context.Response.WriteAsync("Hello World!"); //});
    }

复制代码

F5运行程序,可以看到已经可以按照mvc的默认路由机制解析了。

1

Asp.Net Core应用程序默认提供IIS服务和Kestrel服务两种寄宿方式,意味着Asp.Net Core应用程序可以脱离IIS运行,这也是跨平台的基础。在Program.cs文件中可以看到这种服务的添加。

复制代码

public static void Main(string[] args)
{ var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup()
.Build();

        host.Run();
    }

复制代码

Visual Studio的默认启动选项为IIS Express,即采用IIS服务方式,我们可以通过以下两种方式使用Kestrel服务运行我们的程序

1 在VS的启动选项中选择我们的项目名称同名的选项,然后F5运行。

1

2 在项目文件夹根目录,按住Shift键,点击鼠标右键,单击“在此处打开命令窗口”菜单,输入dotnet run命令。

1

会提示应用程序服务已经启动,并且地址为http://localhost:5000,我们打开浏览器输入这个地址,可以看到页面输出了我们预期的结果,与使用IIS服务是同样的效果。

可以在Program.cs通过使用UseUrls方法修改应用程序的地址。

复制代码

public static void Main(string[] args)
{ var host = new WebHostBuilder()
.UseKestrel()
.UseUrls(“http://localhost:8000“)
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup()
.Build();

        host.Run();
    }

复制代码

Asp.Net Core 项目实战之权限管理系统(3) 通过EntityFramework Core使用PostgreSQL - Fonour - 博客园

Excerpt

0 Asp.Net Core 项目实战之权限管理系统(0) 无中生有 1 Asp.Net Core 项目实战之权限管理系统(1) 使用AdminLTE搭建前端 2 Asp.Net Core 项目实战之权限管理系统(2) 功能及实体设计 3 Asp.Net Core 项目实战之权限管理系统(3) 通过


0 Asp.Net Core 项目实战之权限管理系统(0) 无中生有

1 Asp.Net Core 项目实战之权限管理系统(1) 使用AdminLTE搭建前端

2 Asp.Net Core 项目实战之权限管理系统(2) 功能及实体设计

3 Asp.Net Core 项目实战之权限管理系统(3) 通过EntityFramework Core使用PostgreSQL

4 Asp.Net Core 项目实战之权限管理系统(4) 依赖注入、仓储、服务的多项目分层实现

5 Asp.Net Core 项目实战之权限管理系统(5) 用户登录

6 Asp.Net Core 项目实战之权限管理系统(6) 功能管理

7 Asp.Net Core 项目实战之权限管理系统(7) 组织机构、角色、用户权限

8 Asp.Net Core 项目实战之权限管理系统(8) 功能菜单的动态加载

github源码地址

0 PostgreSQL安装及配置

0.0 PostgreSQL简介

既然Asp.Net Core最大的特性就是跨平台,就搭配使用一个可以跨平台的数据库。PostgreSQL是一个功能强大的开源数据库系统。经过长达15年以上的积极开发和不断改进,PostgreSQL已在可靠性、稳定性、数据一致性等获得了业内极高的声誉。目前PostgreSQL可以运行在所有主流操作系统上,包括Linux、Unix(AIX、BSD、HP-UX、SGI IRIX、Mac OS X、Solaris和Tru64)和Windows。PostgreSQL是完全的事务安全性数据库,完整地支持外键、联合、视图、触发器和存储过程(并支持多种语言开发存储过程)。它支持了大多数的SQL:2008标准的数据类型,包括整型、数值值、布尔型、字节型、字符型、日期型、时间间隔型和时间型,它也支持存储二进制的大对像,包括图片、声音和视频。PostgreSQL对很多高级开发语言有原生的编程接口,如C/C++、Java、.Net、Perl、Python、Ruby、Tcl 和ODBC以及其他语言等,也包含各种文档。

0.1 PostgreSQL安装及配置

自行去PostgreSQL官网下载符合你自己系统的版本,开始安装,从我自己的安装体验来看,没什么需要特别注意的地方,只需要按照提示一步步安装即可,在最后的时候根据需要选择以下语言,设置超级用户角色postgres的登录密码即可。

创建一个系统使用的角色

打开安装好的PostgreSQL数据库,输入密码进入管理界面。右键“登录角色”,创建一个名称为“fonour”的角色,在“角色权限”页签中把所有能能勾选的功能都勾选上。

1

右键PostgreSQL服务器,选择断开服务器。接着右键,单击“属性”菜单,在弹出窗口的用户名出输入刚才新建的“fonour”角色,输入密码并勾选记住密码。确定,连接即可。

1 使用EntityFrameworkCore的CodeFirst方式创建数据库

1.0 在Fonour.EntityFrameworkCore项目中创建DbContext

由于EF Core跟PostgreSQL都需要现学现用,EF Core的使用跟EF6.0还是有很多不同的,在使用的过程中遇到了不少的问题,尤其是针对PostgreSQL使用Guid类型的主键,后面会把这些坑做一个简单的记录。

0 添加相关依赖项

需要添加的相关依赖及说明如下:

  • Npgsql.EntityFrameworkCore.PostgreSQL

      PostgreSQL数据提供的支持EF Core的基础类库,是通过EF Core使用PostgreSQL数据库的根本。

  • Npgsql.EntityFrameworkCore.PostgreSQL.Design

      使用Guid(对应Postgre数据的类型为uuid)类型的主键必须,int/long类型的主键不添加也没问题。

  • Microsoft.EntityFrameworkCore.Tools

      EF Core工具,CodeFirst数据库迁移相关操作必须。

  • Fonour.Domain

      我们自己创建的一个类库项目,其中包含了组织机构、功能、角色、用户等实体的定义。

添加相关引用依赖的方式有多种,可以通过NuGet程序包管理器控制台的Install-Packege命令

1
2
3
PM&gt; Install-<span>Package Npgsql.EntityFrameworkCore.PostgreSQL
PM</span>&gt; Install-<span>Package Npgsql.EntityFrameworkCore.PostgreSQL.Design
PM</span>&gt; Install-Package Microsoft.EntityFrameworkCore.Tools

或者直接在NuGet程序包管理器中搜索相关类库,进行安装

1

最直接的方法是直接修改project.json配置文件,project.json配置文件最终修改后内容如下。

复制代码

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
<span>{
</span>"version": "1.0.0-*"<span>,

</span>"dependencies"<span>: {
</span>"Fonour.Domain": "1.0.0-0"<span>,
</span>"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"<span>,
</span>"NETStandard.Library": "1.6.0"<span>,
</span>"Npgsql.EntityFrameworkCore.PostgreSQL": "1.0.1"<span>,
</span>"Npgsql.EntityFrameworkCore.PostgreSQL.Design": "1.0.1"<span>
},

</span>"frameworks"<span>: {
</span>"netcoreapp1.0"<span>: {
</span>"imports"<span>: [
</span>"dotnet5.6"<span>,
</span>"portable-net45+win8"<span>
]
}
},

</span>"tools"<span>: {
</span>"Microsoft.EntityFrameworkCore.Tools"<span>: {
</span>"version": "1.0.0-preview2-final"<span>,
</span>"imports"<span>: [
</span>"portable-net45+win8+dnxcore50"<span>,
</span>"portable-net45+win8"<span>
]
}
}
}</span>

复制代码

注意,frameworks部分,如果默认是netstandard1.6框架,必须进行修改,否则会提示不支持相关依赖项。

1 创建DbContext

根据EF Core对多对多关联关系的要求,增加了UserRole、RoleMenu两个关联关系实体,同时对原有实体进行了调整。

复制代码

1
2
3
4
5
6
7
8
9
<span>public</span> <span>class</span><span> UserRole
{
</span><span>public</span> Guid UserId { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> User User { <span>get</span>; <span>set</span><span>; }

</span><span>public</span> Guid RoleId { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> Role Role { <span>get</span>; <span>set</span><span>; }

}</span>

复制代码

复制代码

1
2
3
4
5
6
7
8
<span>public</span> <span>class</span><span> RoleMenu
{
</span><span>public</span> Guid RoleId { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> Role Role { <span>get</span>; <span>set</span><span>; }

</span><span>public</span> Guid MenuId { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> Menu Menu { <span>get</span>; <span>set</span><span>; }
}</span>

复制代码

在Fonour.EntityFrameworkCore项目下新建一个数据上下文操作类,命名为“FonourDBContext”,其中增加权限管理系统相关实体的DbSet的定义。最终代码如下:

复制代码

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
<span>public</span> <span>class</span><span> FonourDbContext : DbContext
{
</span><span>public</span> FonourDbContext(DbContextOptions&lt;FonourDbContext&gt; options) : <span>base</span><span>(options)
{

}
</span><span>public</span> DbSet&lt;Department&gt; Departments { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> DbSet&lt;Menu&gt; Menus { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> DbSet&lt;Role&gt; Roles { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> DbSet&lt;User&gt; Users { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> DbSet&lt;UserRole&gt; UserRoles { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> DbSet&lt;RoleMenu&gt; RoleMenus { <span>get</span>; <span>set</span><span>; }

</span><span>protected</span> <span>override</span> <span>void</span><span> OnModelCreating(ModelBuilder builder)
{
</span><span>//</span><span>UserRole关联配置</span>
builder.Entity&lt;UserRole&gt;<span>()
.HasKey(ur </span>=&gt; <span>new</span><span> { ur.UserId, ur.RoleId });

</span><span>//</span><span>RoleMenu关联配置</span>
builder.Entity&lt;RoleMenu&gt;<span>()
.HasKey(rm </span>=&gt; <span>new</span><span> { rm.RoleId, rm.MenuId });
builder.Entity</span>&lt;RoleMenu&gt;<span>()
.HasOne(rm </span>=&gt;<span> rm.Role)
.WithMany(r </span>=&gt;<span> r.RoleMenus)
.HasForeignKey(rm </span>=&gt; rm.RoleId).HasForeignKey(rm =&gt;<span> rm.MenuId);

</span><span>//</span><span>启用Guid主键类型扩展</span>
builder.HasPostgresExtension(<span>"</span><span>uuid-ossp</span><span>"</span><span>);

</span><span>base</span><span>.OnModelCreating(builder);
}
}</span>

复制代码

1.1 在Fonour.MVC项目中进行数据库连接相关配置

0 添加相关依赖项

在Asp.Net Core中,使用json格式的配置文件进行系统相关参数的配置,将相关配置文件通过ConfigurationBuilder进行统一管理,得到IConfigurationRoot的配置实例,获取相关配置文件配置节点的信息。想要使用配置文件相关服务,需要添加一下依赖。

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.FileExtensions
  • Microsoft.Extensions.Configuration.Json

还需要添加对Fonour.EntityFrameworkCore项目的引用。

最终Fonour.MVC项目的project.json文件配置如下:

复制代码

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
<span>{
</span>"dependencies"<span>: {
</span>"Microsoft.NETCore.App": "1.0.1"<span>,
</span>"Microsoft.AspNetCore.Diagnostics": "1.0.0"<span>,
</span>"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0"<span>,
</span>"Microsoft.AspNetCore.Server.Kestrel": "1.0.1"<span>,
</span>"Microsoft.Extensions.Logging.Console": "1.0.0"<span>,
</span>"Microsoft.AspNetCore.Mvc": "1.0.1"<span>,
</span>"Microsoft.AspNetCore.StaticFiles": "1.0.0"<span>,
</span>"Microsoft.Extensions.Configuration": "1.0.0"<span>,
</span>"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0"<span>,
</span>"Microsoft.Extensions.Configuration.Json": "1.0.0"<span>,
</span>"Fonour.EntityFrameworkCore": "1.0.0-*"<span>
},

</span>"tools"<span>: {
</span>"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"<span>
},

</span>"frameworks"<span>: {
</span>"netcoreapp1.0"<span>: {
</span>"imports"<span>: [
</span>"dotnet5.6"<span>,
</span>"portable-net45+win8"<span>
]
}
},

</span>"buildOptions"<span>: {
</span>"emitEntryPoint": <span>true</span><span>,
</span>"preserveCompilationContext": <span>true</span><span>
},

</span>"runtimeOptions"<span>: {
</span>"configProperties"<span>: {
</span>"System.GC.Server": <span>true</span><span>
}
},

</span>"publishOptions"<span>: {
</span>"include"<span>: [
</span>"wwwroot"<span>,
</span>"web.config"<span>
]
},

</span>"scripts"<span>: {
</span>"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%"<span> ]
},
</span>"runtimes"<span>: {
</span>"win10-x64"<span>: {}
}
}</span>

复制代码

1 增加appsettings.json配置文件

右键Fonour.MVC项目,新增一个Asp.Net配置文件类型,名称为appsettings.json的配置文件。

1

appsettings.json文件目前主要内容为定义数据库连接字符串,内容如下:

1
2
3
4
5
<span>{
</span>"ConnectionStrings"<span>: {
</span>"Default": "User ID=fonour;Password=123456;Host=localhost;Port=5432;Database=Fonour;Pooling=true;"<span>
}
}</span>

2 启用数据库连接

首先在Startup.cs中定义一个IConfigurationRoot的属性,然后在系统启动类Startup.cs的构造函数中,对配置文件进行管理。

复制代码

1
2
3
4
5
6
7
8
9
10
<span>public</span> IConfigurationRoot Configuration { <span>get</span><span>; }
</span><span>public</span><span> Startup(IHostingEnvironment env)
{
</span><span>var</span> builder = <span>new</span><span> ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile(</span><span>"</span><span>appsettings.json</span><span>"</span>, optional: <span>true</span>, reloadOnChange: <span>true</span><span>)
.AddJsonFile($</span><span>"</span><span>appsettings.{env.EnvironmentName}.json</span><span>"</span>, optional: <span>true</span><span>);
builder.AddEnvironmentVariables();
Configuration </span>=<span> builder.Build();
}</span>

复制代码

在ConfigureServices方法中获取数据库连接字符串,并添加数据库连接服务。

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
<span>public</span> <span>void</span><span> ConfigureServices(IServiceCollection services)
{
</span><span>//</span><span>获取数据库连接字符串</span>
<span>var</span> sqlConnectionString = Configuration.GetConnectionString(<span>"</span><span>Default</span><span>"</span><span>);

</span><span>//</span><span>添加数据上下文</span>
services.AddDbContext&lt;FonourDbContext&gt;(options =&gt;<span>
options.UseNpgsql(sqlConnectionString)
);

services.AddMvc();
}</span>

复制代码

1.2 使用CodeFirst数据库迁移命令创建数据库

在EntityFrameworkCore中数据库迁移有两种方式。

0 使用命令行工具

在应用程序根目录按住shift键同时单击鼠标右键,选择“在此处打开命令窗口”,输入数据库迁移的命令

1
2
3
<span>dotnet ef migrations add Init

dotnet ef database update</span>

1 使用程序包管理器控制台

在程序包管理器控制台中默认项目选择Fonour.EntityFrameworkCore,输入以下命令,自动创建数据库迁移文件。

注意,一定要将Fonour.MVC设置为启动项目。

执行完成后,项目中增加了数据库迁移文件。

1

输入以下命令,进行数据库更新操作。

提示更新完成后,查看我们的数据库,会发现数据库及数据库表已经创建完成。

1

2 数据初始化

为保证系统正常运行,我们需要对系统进行数据初始化,要初始化的数据包括一下内容:

  • 用户表插入一条用户名为admin的超级管理员用户信息
  • 功能菜单表增加组织机构管理、角色管理、用户管理、功能管理四条基本数据。
  • 组织机构表插入一条组织机构信息(主要是因为用户表存在一个DepartmentId的外键)

在Fonour.EntityFrameworkCore项目中增加一个新的数据初始化类,命名为SeedData.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
<span>public</span> <span>static</span> <span>class</span><span> SeedData
{
</span><span>public</span> <span>static</span> <span>void</span><span> Initialize(IServicePr
{
</span><span>using</span> (<span>var</span> context = <span>new</span><span> FonourDbCon
{
</span><span>if</span><span> (context.Users.Any())
{
</span><span>return</span>; <span>//</span><span> 已经初始化过数据</span>
<span> }
Guid departmentId </span>=<span> Guid.NewGuid
</span><span>//</span><span>增加一个部门</span>
<span> context.Departments.Add(
</span><span>new</span><span> Department
{
Id </span>=<span> departmentId,
Name </span>= <span>"</span><span>Fonour集团总部</span><span>"</span><span>,
ParentId </span>=<span> Guid.Empty
}
);
</span><span>//</span><span>增加一个超级管理员用户</span>
<span> context.Users.Add(
</span><span>new</span><span> User
{
UserName </span>= <span>"</span><span>admin</span><span>"</span><span>,
Password </span>= <span>"</span><span>123456</span><span>"</span>, <span>//
</span> Name = <span>"</span><span>超级管理员</span><span>"</span><span>,
DepartmentId </span>=<span> departme
}
);
</span><span>//</span><span>增加四个基本功能菜单</span>
<span> context.Menus.AddRange(
</span><span>new</span><span> Menu
{
Name </span>= <span>"</span><span>组织机构管理</span><span>"</span><span>,
Code </span>= <span>"</span><span>Department</span><span>"</span><span>,
SerialNumber </span>= <span>0</span><span>,
ParentId </span>=<span> Guid.Empty,
Icon </span>= <span>"</span><span>fa fa-link</span><span>"</span><span>
},
</span><span>new</span><span> Menu
{
Name </span>= <span>"</span><span>角色管理</span><span>"</span><span>,
Code </span>= <span>"</span><span>Role</span><span>"</span><span>,
SerialNumber </span>= <span>1</span><span>,
ParentId </span>=<span> Guid.Empty,
Icon </span>= <span>"</span><span>fa fa-link</span><span>"</span><span>
},
</span><span>new</span><span> Menu
{
Name </span>= <span>"</span><span>用户管理</span><span>"</span><span>,
Code </span>= <span>"</span><span>User</span><span>"</span><span>,
SerialNumber </span>= <span>2</span><span>,
ParentId </span>=<span> Guid.Empty,
Icon </span>= <span>"</span><span>fa fa-link</span><span>"</span><span>
},
</span><span>new</span><span> Menu
{
Name </span>= <span>"</span><span>功能管理</span><span>"</span><span>,
Code </span>= <span>"</span><span>Department</span><span>"</span><span>,
SerialNumber </span>= <span>3</span><span>,
ParentId </span>=<span> Guid.Empty,
Icon </span>= <span>"</span><span>fa fa-link</span><span>"</span><span>
}
);
context.SaveChanges();
}
}
}</span>

复制代码

在Fonour.MVC项目的Startup.cs中Configure方法最后增加数据初始化的操作。

复制代码

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
<span>public</span> <span>void</span><span> Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();

</span><span>if</span><span> (env.IsDevelopment())
{
</span><span>//</span><span>开发环境异常处理</span>
<span> app.UseDeveloperExceptionPage();
}
</span><span>else</span><span>
{
</span><span>//</span><span>生产环境异常处理</span>
app.UseExceptionHandler(<span>"</span><span>/Shared/Error</span><span>"</span><span>);
}
</span><span>//</span><span>使用静态文件</span>
<span> app.UseStaticFiles();
</span><span>//</span><span>使用Mvc,设置默认路由为系统登录</span>
app.UseMvc(routes =&gt;<span>
{
routes.MapRoute(
name: </span><span>"</span><span>default</span><span>"</span><span>,
template: </span><span>"</span><span>{controller=Login}/{action=Index}/{id?}</span><span>"</span><span>);
});

SeedData.Initialize(app.ApplicationServices); </span><span>//</span><span>初始化数据</span>
}

复制代码

运行程序。查看数据库,发现初始化数据已经生成成功。

1

2

3

3 踩过的一些坑

  • 使用Guid类型的主键一定要添加”Npgsql.EntityFrameworkCore.PostgreSQL.Design”: “1.0.1”的引用,注意1.0.0版本是不支持的。
  • 要想使用Guid类型的主键一定要在DbContext的 OnModelCreating 重写方法中启用uuid的扩展 builder.HasPostgresExtension(“uuid-ossp”);
  • 关于多对关系的实体设计

      在以前的EntityFramework中,多对多关系的实体,只需要创建两个实体,两个实体分别包含一个对方集合的导航属性即可,是不需要创建关联实体类的。如,User实体中包含一个ICollection Roles的导航属性,同时Role实体中包含一个ICollection Users的导航属性,EF会根据实体间的关系自动创建一个User_Role中间表,若想修改中间表名,进行相关配置即可。

      EF Core中必须创建关联关系的实体才行。

  • EF Core暂不支持LayzingLoad

4 总结

本节主要是研究怎样通过EntityFramework Core使用PostgreSQL,通过CodeFirst数据库迁移的方式根据设计好的实体进行数据库的创建,最后我们根据系统需要,进行了一些相关数据的初始化。

接下来要实现用户登录功能。

  一个完整的ASP.NET的请求中会存在身份验证(Authentication)阶段以及授权(Authorization)阶段,英文单词Authentication和Authorization非常相似,所以很多时候会混淆这两个概念。身份验证(Authentication)的目的是知道“你”是谁,而授权(Authorization)则是当“你”访问一个资源时是否符合访问条件,符合就将访问权限授权给你进行访问,否则拒绝访问。

  本文将从以下几点介绍ASP.NET MVC如何使用Identity完成资源访问的限制:
  ● 资源访问的限制方式
  ● ASP.NET中的访问限制
  ● ASP.NET MVC中基的访问限制
  ● ASP.NET MVC中的用户信息
  ● ASP.NET Identity用户身份信息填充
  ● ASP.NET MVC访问限制的实现
  ● ASP.NET MVC基于用户声明的访问限制及自定义限制

资源访问的限制方式

  什么是资源?在Web中通过URI(Uniform Resource Identifier,统一资源标识符)来对HTML文档、图片、图像等内容定位,反过来说在Web中HTML文档、图片、图像等内容就是资源,而资源访问的限制就是对在用户通过URI访问Web资源时,判断该用户是否有权限访问该资源,如果有则继续访问,否则拒绝访问。
  资源访问有以下几种限制方式:
  ● 匿名访问限制:所有人都可以访问资源。
  ● 根据用户名访问限制:指定特定的用户,让其能够或者不能访问资源。
  ● 根据用户角色访问限制:指定特定角色,让拥有该角色的用户能够访问资源。
  ● 根据用户声明(Claim)访问限制:指定特定的声明(Claim),让身份信息中含有该声明的用户能够访问资源。
  ● 使用其它用户信息进行访问限制:根据用户身份的其它信息来判断用户是否能够访问资源。
  从上面几点可以看出资源访问的限制或者说授权,实际上就是通过用户信息来判断用户是否有访问资源的权限,而常用的信息是用户名、用户角色以及用户声明。
  注:对于授权来说,它处于身份验证的后续阶段,所以可以认为在授权阶段时已经存在用户信息,所以可以直接使用用户信息来完成访问限制。

ASP.NET中的访问限制

  ASP.NET中通过HTTPModule的方式实现了FileAuthorizationModule以及UrlAuthorizationModule来对用户访问文件以及其它资源进行权限控制,其中UrlAuthorizationModule可以通过在web.config中添加如下配置来通过用户名或者用户角色限制访问:

  

  在ASP.NET中可以通过Forms验证+UrlAuthorizationModule来实现用户身份验证和访问授权,更多信息可参考文档:https://docs.microsoft.com/en-us/aspnet/web-forms/overview/older-versions-security/membership/user-based-authorization-cs

ASP.NET MVC中的访问限制

  Forms验证+UrlAuthorizationModule的方式是用于基于ASP.NET Web Form的应用程序,而ASP.NET MVC虽然也可以使用Forms验证,但是ASP.NET MVC的授权方式是不一样的,它是通过过滤器的方式实现,下面代码为之前文章中用于限制后台管理页面需要登录的代码:

  

  通过在Controller上使用了一个名为Authorize的特性来实现的,这个特性的定义如下:

  

  它用于当用户访问Controller或Action方法时可以通过用户信息对其访问进行限制。

  在Authorize特性的定义中可以看到名为Roles以及Users的属性,其作用就是设置可以访问该资源的用户或者角色:

  

  使用方法如下所示:

  

ASP.NET MVC中的用户信息

  通过前面的介绍可以知道用户的授权是根据用户信息来的,无论是基于角色的、用户的、声明的甚至是自定义的,都需要依赖用户信息进行权限判断,那么ASP.NET MVC中到底包含什么用户信息?
  1. HttpContextBase与IPrincipal:
  首先可以知道的是在ASP.NET中有一个最核心的HTTP上下文对象HttpContextBase,它保存了整个Http请求到响应过程的所有相关数据,其定义如下:

  

  其中就包含了一个名为User的IPrincipal类型:

  

  该类型中的Identity属性就包含了用户的信息:

  

  从接口中可以看到它仅仅包含了用户名、身份验证反射以及是否验证通过三个属性。

  注:IPrincipal的实现有多种而本例中使用的是ClaimPrincipal。

  

  2. ClaimsIdentity与AuthenticationTicket:
  通过前面的文章分析得知Identity基于Cookie的身份验证方式实际上是对一个AuthenticationTicket对象序列化加密、反序列化解密的过程,而这个AuthenticationTicket就携带了所有用户的信息,在AuthenticationTicket的定义中可以看到两个重要的对象,其中AuthenticationProperties保存了身份验证的会话信息,如过期时间、是否允许刷新等。而另一个ClaimsIdentity属性就是以声明(Claim)的方式实现的用户信息。

  

  ClaimsIdentity的部分定义如下:

  

  其中除了实现IIdentity接口的属性外,还有一个重要的属性是Claims,它用于以声明的方式来保存用户信息,那么Identity是如何完成用户数据填充的?

ASP.NET Identity用户身份信息填充

  用户的获取填充主要是在用户登录(注册用户后会自动登录)的时候完成的,因为在后续的请求中Identity仅需通过解析加密后的用户信息字符串即可获得用户信息(注:会存在重新生成刷新该信息的情况,如身份信息的滑动过期等)。

  通过前面文章的分析知道了在Identity中用户的登录是通过SignInManager对象完成的以下是用户登录的代码及注册代码:

  

  以下是注册代码,实际上是创建完成用户后执行了登录操作:

  

  注:通过对源码分析,SignInManager.PasswordSignInAsync方法实际上最后也是调用SignInAsync方法完成的登录。

  那么SignInAsync到底做了什么?

  

  从代码中可以看到,该方法调用了一个名为CreateUserIdentityAsync的方法,根据其方法名、参数以及返回值类型来看就已经可以确定该方法就是通过用户对象生成上面提到的用于以声明的方式保存用户信息的方法。从它的实现中可以看出它实际上是通过UserManager生成的:

  

  而UserManager又是通过ClaimsIdentityFactory完成的:

  

  ClaimsIdentityFactory.CreateAsync方法的实现:

  

  注:实际上身份信息的刷新也是通过UserManager完成的:

  

  方法的实现仍然是UserManager的CreateIdentityAsync方法:

  

  从实现中可以看出如果UserManager支持角色、声明等功能,它会从数据库中加载对应的信息以声明(Claim)的形式保存在ClaimsIdentity对象中。
  在数据库中添加以下数据(为了演示功能直接在数据库中添加角色、声明信息并与用户数据进行关联,如果要开发此功能可基于UserManager完成):
  角色信息:

  

  用户声明(Claim):

  

  角色与用户信息关联:

  

  注:由于此处Identity EF与MySQL,对象与表映射存在问题,所以多了一些ID列,暂时不管这个问题,关于Identity与MySQL用法可以参考这篇文档:https://docs.microsoft.com/en-us/aspnet/identity/overview/getting-started/aspnet-identity-using-mysql-storage-with-an-entityframework-mysql-provider
  运行程序并登录后,在用户信息(ClaimsIdentity)中可找到添加的角色和声明信息:

  

ASP.NET MVC访问限制的实现

  上面介绍了用户信息的填充,那么访问的限制实际上就是对用户信息比较而已,下面是Authorize特性的核心方法:

  

  其中核心的三个判断为:
  ● user.Identity.IsAuthenticated:必须通过身份验证。
  ● (this._usersSplit.Length <= 0 || this._usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)):特性没有指定用户或者当前用户存在于指定的用户列表中。
  ● (this._rolesSplit.Length <= 0 || this._rolesSplit.Any(new Func<string, bool>(user.IsInRole)):特性没有指定角色或者当前用户拥有指定的角色。
  以上三个条件必须全部符合才能够访问。

ASP.NET MVC基于用户声明的访问限制及自定义访问限制

  ASP.NET MVC中虽然用户信息是基于声明的方式保存的,但是却没有实际的实现,所以需要自己动手实现一个(注:也可以参考ASP.NET Core中的实现https://docs.microsoft.com/en-us/aspnet/core/security/authorization/claims)。
  实现一个自定义的授权特性(注:之前分析过HttpContext中的User是一个IPrincipal类型,实际上MVC使用的是与ClaimsIdentity对应的ClaimsPrincipal)代码如下,该过滤器只是在原有的授权方式基础上添加了声明的检查:

  

  使用方式如下:

  

  登录后访问上面action得到下面结果,验证通过:

  

  注:需要注意的是以上介绍的授权方式,无论是通过角色、用户名还是声明,它都需要以硬编码的形式写在代码中,换句话说就是在开发时必须确定该功能或者Controller/Action访问需求。但是一些时候也会出现访问需求不确定的情况,访问权限的配置会在运行时通过配置文件或者数据库来动态配置,这样的话自定义的授权过滤器就需要依赖一些业务组件来实现自定义的授权流程。

小结

  本章介绍了授权与身份验证的关系以及在ASP.NET中的实现,并详细介绍了ASP.NET MVC中的Identity是如何使用身份验证数据来完成授权的。常见的授权方式一般是基于用户名、角色以及声明,但是它们使用的场景边界是不那么明确的,就是说用什么都行实际情况需要根据需求来看,一般权限控制较简单的使用基于角色的授权即可。但无论基于什么来对用户授权,这些信息都属于用户信息,所以在拓展用户的授权时首先要考虑的是用户的特征信息,其次是用户身份验证时如何获取填充这些信息,最后才是考虑如何使用这些信息来进行授权。

参考:  

  https://msdn.microsoft.com/en-us/library/wce3kxhd.aspx
  https://stackoverflow.com/questions/21645323/what-is-the-claims-in-asp-net-identity
  https://www.codeproject.com/Articles/98950/ASP-NET-authentication-and-authorization

本文链接:http://www.cnblogs.com/selimsong/p/7828326.html 

ASP.NET没有魔法——目录

  上一篇文章介绍了OAuth2.0以及如何使用.Net来实现基于OAuth的身份验证,本文是对上一篇文章的补充,主要是介绍OAuth与Jwt以及OpenID Connect之间的关系与区别。

  本文主要内容有:
  ● Jwt简介
  ● .Net的Jwt实现
  ● OAuth与Jwt
  ● .Net中使用Jwt Bearer Token实现OAuth身份验证
  ● OAuth与OpenID Connect

  注:本章内容源码下载:https://files.cnblogs.com/files/selimsong/OAuth2Demo_jwt.zip

Jwt简介

  Jwt(Json Web Token)它是一种基于Json用于安全的信息传输标准,Jwt具有以下几个特点:
  ● 紧凑:Jwt由于是为Web准备的,所以就需要让数据尽可能小,能够在Url、Post参数或者Http Header中携带Jwt,同时由于数据小,所以也增加了数据传输的速度。
  ● 自包含:在Jwt的playload部分包含了所有应该包含的信息,特别是在Jwt用于身份验证时playload中包含了用户必要的身份信息(注:不应该包含敏感信息),这样在进行身份验证时就无需去数据库中查询用户信息。
  ● 可信:Jwt是带有数字签名的,可以知道Jwt在传输过程中是否被篡改,保证数据是完整的,可用的签名算法有RS256(RSA+SHA-256)、HS256(HMAC+SHA-256)等。

  Jwt有两个用途,其一是用于数据交互,因为Jwt是被签名的,可以保证数据的完整性。另外就是用来携带用户信息进行身份验证。

  Jwt包含三个部分:
  ● Header:包含了签名算法以及令牌类型(默认为JWT)。如:

  

  注:alg以及typ均是缩写,其目的就是为了减小jwt的大小。

  ● Playload:包含Jwt所携带的信息内容,Playload中包含了3种类型的Claim(声明)定义,分别是标准的,如iss(issuer,Jwt的发行者)、sub(subject,Jwt所代表的用户)、aud(audience,Jwt的接收者)、exp(expiration time,Jwt的过期时间),还有一些是公共约定的如: http://www.iana.org/assignments/jwt/jwt.xhtml,另外就是私有自定义的,这些用来存放具体的信息。
  Playload的结构如下:

  

  ● Signature:包含了Header以及Playload的base64Url编码后的签名结果,其计算过程如下:

  

  最终三个部分均使用Base64Url的方式进行编码后使用符号“.”进行分隔,以下是一个完整Jwt的例子:

  

  注:Jwt中的数据是透明的,既任何人拿到数据都能Base64Url反编码的形式看到内容,签名仅仅是保证内容不被纂改,所以不能在Jwt中包含敏感数据。以上例子均来自https://jwt.io/introduction/

.Net的Jwt实现

  Jwt是一个标准,在https://jwt.io/上可以看到很多不同语言对Jwt的实现,而.Net的其中一个实现是System.IdentityModel.Tokens.Jwt组件,该组件是由微软实现的,它有两个重要的类型分别是:
  注:从名称(IdentityModel)都可以看出,微软的这个实现主要是用于身份验证的,如果使用Jwt的目的不是身份验证可以选择其它的组件或自定义实现。
  ● JwtSecurityToken:这个类型是Jwt的一个封装,它除了包含Jwt的三个要素(Header、Playload、Signature)外,还拓展了一些如Subject、Iusser、Audiences、有效期、签名算法、签名密钥等重要属性。
  下图是JwtSecurityToken的部分定义:

  

  ● JwtSecurityTokenHandler:该对象用来对Jwt进行操作,如Jwt的创建、验证( 包含发布者、接收者、签名等验证)、Jwt的序列化与反序列化(字符串形式与对象形式之间的转换)
  下图是JwtSecurityTokenHandler的部分定义:

  

OAuth与Jwt

  OAuth与Jwt前者是一个授权协议后者是一个信息安全传输标准,看起来它们之间并没有什么关系,但其实OAuth的Access Token有一种实现方式就是Jwt。
为什么要使用Jwt来作为OAuth的Access Token?首先来看一下上一篇文章中生成的Access Token:

  

  它是一个加密后的字符串,该字符串包含了用户的相关信息,但是该字符串只能够被使用Microsoft.Owin.Security.OAuth组件的应用程序解密(不包括参照源码的实现),并且还要保证加解密的密钥是相同的。但是OAuth很多时候是用于一些分布式的场景中,甚至还会使用不同语言来编写不同的应用、服务。这样的话上面这种Token的实现方式就无法满足需求。
  所以需要使用Jwt Bearer Token来解决不同应用中的Token识别问题。

.Net中使用Jwt Bearer Token实现OAuth身份验证

  在上一篇文章中提到了Microsoft.Owin.Security.OAuth组件中Access Token的生成实际上是对一个AuthenticationTicket对象序列化并加密后的字符串,而Access Token的验证则是对加密后的字符串解密并反序列化获得AuthenticationTicket对象的过程。
  而对于Access Token来说无论是Microsoft.Owin.Security.OAuth组件的实现方式还是Jwt,甚至是自定义格式,它的核心都在于如何将用户信息包含到一个字符串令牌中,并且能够通过这个字符串令牌还原出正确的用户信息。对于这一个过程在.Net的Owin身份验证解决方案中将其抽象为一个ISecureDataFormat接口,其中身份验证的泛型TData类型为AuthenticationTicket。下图是ISecureDataFormat接口的定义,它的两个方法就是用于进行字符串加密令牌与用户信息对象之间的转换,可参考《ASP.NET没有魔法——ASP.NET Identity的加密与解密

  

  上一篇文章中也给出了Microsoft.Owin.Security.OAuth组件中,默认对Access Token加解密对象是TicketDataFormat,该对象实际上就是一个实现了ISecureDataFormat接口的类型,用于通过数据保护器来完成数据对象的序列化与加解密的工作,可参考《ASP.NET没有魔法——ASP.NET Identity的加密与解密》:

  

  可以这样理解要在.Net中实现基于Jwt Bearer Token的OAuth身份验证,仅需要在Microsoft.Owin.Security.OAuth组件的基础上自定义一个ISecureDataFormat类型即可。

Jwt主要属性的说明

  实现之前再次对Jwt的一些重要属性进行说明:
  ● Issuer:发布者,Jwt里面包含并且会进行验证的信息,Token的发布者,该发布者实际上就是身份验证服务器本身。
  ● Audience:观众,发布者生成一个Token是根据观众来生成的,因为整个验证体系是以发布者为中心的分布式的包含多种应用的,为了保证数据安全一个Token只应该针对其中一个应用有效,所以在验证Jwt时还要对Audience进行验证。
  ● Subject:主题,在身份验证中一般用于保存用户信息,如用户名。

  它们三的关系如下图:

  

  User代表的就是Subject,在OAuth中有Client的概念,OAuth的Client就相当于Audience。之前已经实现了Client的管理,现在为每一个Client添加一个用来数字签名的密钥,该密钥是一个32位byte数组的Base64编码字符串。另外这里是使用HMAC算法来完成对Token的摘要计算。

  

实现一个基于Jwt的ISecureDataFormat

  下面就开始介绍如何来实现这个ISecureDataFormat:
  1. 通过Nuget安装Microsoft.Owin.Security.Jwt组件:
注:微软实现了一个用于解析Jwt Bearer Token的组件,但是该组件只实现了Unprotect方法,使用这个组件开发可以减少一些工作量。

  2. 了解Microsoft.Owin.Security.Jwt中JwtFormat类型:
  Microsoft.Owin.Security.Jwt中实现了一个JwtFormat的对象,该对象正好实现了需要的ISecureDataFormat接口:

  

  但是从源码中得知该对象没有实现Protect方法:

  

  而它的UnProtect方法的实现主要工作如下:

  

  ● 对发布者以及Token的签名、过期时间等进行验证(注:验证操作是由System.IdentityModel.Tokens.Jwt组件中的JwtSecurityTokenHandler类型提供的)。
  ● 验证成功后获取Token中包含的用户信息。

  3. 实现Jwt的Protect方法:

  

  完整代码:

1 public class MyJwtFormat : ISecureDataFormat
2 {
3 //用于从AuthenticationTicket中获取Audience信息
4 private const string AudiencePropertyKey = “aud”;
5
6 private readonly string _issuer = string.Empty;
7 //Jwt的发布者和用于数字签名的密钥
8 public MyJwtFormat(string issuer) 9 { 10 _issuer = issuer; 11 } 12
13 public string Protect(AuthenticationTicket data) 14 { 15 if (data == null) 16 { 17 throw new ArgumentNullException(“data”); 18 } 19 //获取Audience名称及其信息
20 string audienceId = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ?
21 data.Properties.Dictionary[AudiencePropertyKey] : null; 22 if (string.IsNullOrWhiteSpace(audienceId)) throw new InvalidOperationException(“AuthenticationTicket.Properties does not include audience”); 23 var audience = ClientRepository.Clients.Where(c => c.Id == audienceId).FirstOrDefault(); 24 if (audience == null) throw new InvalidOperationException(“Audience invalid.”); 25 //根据密钥创建用于数字签名的SigningCredentials,该对象在JwtSecurityToken中使用
26 var keyByteArray = TextEncodings.Base64Url.Decode(audience.Secret); 27 var signingKey = new InMemorySymmetricSecurityKey(keyByteArray); 28 var signingCredentials = new SigningCredentials(signingKey, 29 SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.Sha256Digest); 30 //获取发布时间和过期时间
31 var issued = data.Properties.IssuedUtc; 32 var expires = data.Properties.ExpiresUtc; 33 //创建JwtToken对象
34 var token = new JwtSecurityToken(_issuer, 35 audienceId, 36 data.Identity.Claims, 37 issued.Value.UtcDateTime, 38 expires.Value.UtcDateTime, 39 signingCredentials); 40 //使用JwtSecurityTokenHandler将Token对象序列化成字符串
41 var handler = new JwtSecurityTokenHandler(); 42 var jwt = handler.WriteToken(token); 43 return jwt; 44 } 45
46 public AuthenticationTicket Unprotect(string protectedText) 47 { 48 throw new NotImplementedException(); 49 } 50 }

View Code 

  上面代码做了以下几件事:
  ● 从AuthenticationTicket中获取Audience信息(注:AuthenticationTicket是.Net中用来保存用户信息的对象,它除了用户信息,如用户名以及用户Claims之外还携带了身份验证的有效期等附加信息,见下图。AuthenticationTicket的创建方式有两种,其一是登录时,在判断登录信息无误后,从数据库中获取相应的用户信息以及从配置(或者默认)获取身份验证信息,如有效期等。另外就是通过反序列化身份Token获取。这里的Protect方法实际上就是序列化Token的方法,所以它得到的AuthenticationTicket是通过第一总方式创建的)

  

  ● 创建用于数字签名的SignatureCredentials对象,该对象代表了用于数字签名的算法及其密钥,创建该对象的原因仅仅是JwtSecurityToken对象需要它来完成Token创建。
  ● 通过JwtSecurityToken对象创建Token,该对象的创建需要发布者(issuer)、观众(audience)、用户Claims信息、发布时间、有效期以及数字签名需要的算法及密钥等。
  ● 通过JwtSecurityTokenHandler完成对Token的序列化。

  3. 在AuthenticationTicket中加入Audience信息。
  上面在创建Token时提到了需要Audience信息,而Token是通过AuthenticationTicket创建的,所以需要在创建AuthenticationTicket时加入Audience信息,另外上面也提到AuthenticationTicket的两种创建方法,这里使用的方法就是在“登录”时创建的,而OAuth的“登录”是通过不同类型的“授权”方式实现的,所以要加入Audience信息,只需要在相应方式的授权代码中添加即可(以基于用户名、密码的模式为例,其它方法复制代码即可):

  

  4. 为Audience(Client)添加用于解析Token的JwtBearerAuthentication中间件:

  

  Audience或者说Client包含了受限制的资源,当要访问这些资源时就需要解析Token完成身份验证。而Audience之间或者是Client之间是相对独立的,所以它应该限制可访问的Audience以及拥有自己的加密密钥,甚至还需要验证发布者以确定token的安全性。(注:本例将身份验证服务器和Client都包含在同一个应用中,实际应用可将其分开,这样就是一个简单的单点登录系统)。

  5. 运行程序

  

  使用该Token能够正常访问受限资源:

  

  下面是将Token Base64解码后的结果,可以看到Jwt包含的信息:

  

  如果使用test2这个Client获取的Token,将无法访问test1保护的资源:

  

  身份验证失败,跳转登录页面:

  

OAuth与OpenID Connect

  OAuth与OpenID Connect是经常一起出现的两个名词,前者在本系列文章中已经进行过介绍,OAuth是一个授权协议,但是有点矛盾的就是身份验证和授权实际上是两个概念,前面文章也提到过的,身份验证的目的是知道“你”是谁,而授权则是判断“你”是否有权限访问资源。但是从上一篇文章开始介绍的OAuth相关的内容都是用来做身份验证。授权协议用来做身份验证,所以说是矛盾的。
  OpenID Connect就是为了弥补OAuth协议的缺陷,而在OAuth协议基础上进行补充拓展的一个身份验证协议。它包含了如发现服务、动态注册、Session管理、注销机制等新的高级特性。
  使用OAuth来做身份验证,只是因为OAuth相对简单,适合小型项目,这个与OAuth是授权协议还是身份验证协议无关,它关注的是能否满足需求,包括app.UseOAuthBearerAuthentication方法名称都是Authentication而不是Authorization,通过添加OAuth Bearer身份验证中间件来实现身份验证。OpenID Connect更适合于大型项目,在这里就不再深入介绍。

  关于OAuth与OpenID Connect的内容可参考 的博客。感谢 blackheart给我提的意见。^_^

小结

  本章介绍了Jwt以及Jwt在.Net中的实现,并介绍了在.Net中如何使用Jwt Token实现基于OAuth的身份验证。使用Jwt Token最主要的是为了解决不同应用的Token识别问题。
  最后简单的说明了OAuth与OpenID Connect的区别,它们取舍的关键点在于需求,对于小型应用来说OAuth就能够满足,而由于OpenID Connect非常复杂,如果有需求时也可以先考虑使用如IdentityServer这些开源组件。

  与身份验证相关的内容暂时到此,关于.Net安全相关内容可以参考下面的博客,非常全面包含了身份验证以及.Net中的加解密等内容:https://dotnetcodr.com/security-and-cryptography/

参考:

  https://dzone.com/articles/whats-better-oauth-access-tokens-or-json-web-token
  https://stackoverflow.com/questions/32964774/oauth-or-jwt-which-one-to-use-and-why
  http://openid.net/specs/draft-jones-oauth-jwt-bearer-03.html
  https://tools.ietf.org/html/rfc7523
  https://auth0.com/learn/json-web-tokens/
  https://stackoverflow.com/questions/39239051/rs256-vs-hs256-whats-the-difference
  https://stackoverflow.com/questions/18677837/decoding-and-verifying-jwt-token-using-system-identitymodel-tokens-jwt
  http://www.c-sharpcorner.com/UploadFile/4b0136/openid-connect-availability-in-owin-security-components/
  https://security.stackexchange.com/questions/94995/oauth-2-vs-openid-connect-to-secure-api
  https://www.cnblogs.com/linianhui/archive/2017/05/30/openid-connect-core.html

本文链接:http://www.cnblogs.com/selimsong/p/8184904.html 

ASP.NET没有魔法——目录

(1)有助于及时推出功能, 而不会破坏现有系统。

(2)它还可以帮助为选定的客户提供额外的功能。

API 版本控制可以采用不同的方式进行控制,方法如下:

(1)在 URL 中追加版本或作为查询字符串参数,

(2)通过自定义标头和通过接受标头

在这篇文章中, 让我们来看看如何支持多个版本的 ASP.NET  Core  Web API。

一、创建asp.net core webapi 项目,引用NuGet包:Install-Package Microsoft.AspNetCore.Mvc.Versioning -Version 2.0.0

 

 

项目和安装包都好了,接着我们需要在Startup.cs中的ConfigureServices 方法中添加下面的代码:

 

如您所见, 配置了3不同的选项。

  •  ReportAPIVersions: 这是可选的。但是, 当设置为 true 时, API 将返回响应标头中支持的版本信息。
  •  AssumeDefaultVersionWhenUnspecified: 此选项将用于不提供版本的请求。默认情况下, 假定的 API 版本为1.0。
  • DefaultApiVersion: 此选项用于指定在请求中未指定版本时要使用的默认 API 版本。这将默认版本为1.0。

这是所有的配置和设置。现在, 我们将看到访问 API 版本的不同方式。

二、通过QueryString来实现版本控制

打开我们的控制器,在上面添加ApiVersion特性,如下代码所示:

上面的代码作为1.0版本。您还可以在不同的命名空间中创建另一个具有相同名称的控制器类, 并将 API 版本设置为2.0版本。如下图所示:

 

就这样。现在转到浏览器并访问控制器。您应该看到 API 版本1.0 控制器的输出, 因为它被设置为默认值。现在在 URL 中追加 api-version=2, 您应该看到 api 版本2.0 控制器的输出。

查询字符串参数很有用, 但在长 URL 和其他查询字符串参数的情况下可能会很痛苦。相反, 更好的方法是在 URL 路径中添加版本。比如:

  • api/v1/values
  • api/v2/values

还是上面的项目,只不过需要在v1和v2控制器中加入,下面的代码。如下图所示:

同样, 您需要将路由参数更新到所有适用的位置。使用此更改, 在访问API 接口时总是需要有版本号。您可以通过 api/v1/values 访问到版本 1.0, 通过api/v2/values 访问版本 2.0, 更改 URL 中的版本号。简单, 看起来更干净了。

测试结果如下:

在上述两种方法中, 需要修改 URL 以支持版本控制。但是, 如果您希望 api 的URL 保持干净, 则 api 版本信息也可以通过附加 HTTP 报头来传递。要进行此工作, 您需要配置 ApiVersionReader 选项。代码如下:

 

突出显示的行告诉我们header  “api-version” 现在是 api 版本号的预期位置。确保路由属性没有版本详细信息。所以测试它,结果如下:

当您将2.0 作为值提供给 “api 版本” 时, 它将调用版本2.0 控制器并返回输出。

简单, 易于设置。但是, 现在查询字符串参数的方法进行版本控制将不起作用。一旦设置了header, 就不能指定查询字符串参数。如果您希望支持这两种情况, 而不是 HeaderApiVersionReader, 请使用 QueryStringOrHeaderApiVersionReader。代码如下:

因此, 现在支持查询字符串参数和header。默认查询字符串参数名称是 api-version, 因此您可以将构造函数留空, 但如果需要其他名称, 则需要提供。您还可以对查询字符串参数和标头使用有不同的名称。请记住, 我们还将 ReportApiVersions 设置为 true, 该值返回响应标头中的版本信息。见下图。

现在, 让我们来看看另外几个选项。

MapToApiVersion参数的用法:

MapToApiVersion 属性允许将单个 API 操作映射到任何版本。换言之, 一个支持多个版本的单控制器。控制器可能只有版本3支持的 API 操作方法。在这种情况下, 您可以使用 MapToApiVersion。看看下面的代码。

上面代码的意思是:public string Get()该方法只有在版本1.0中支持,public string Getv3()方法只有在版本3.0中支持。

有图有真像,很灵活,我很喜欢。

Deprecated参数的用法:

当支持多个 API 版本时, 某些版本最终会随着时间的推移而被弃用。要标记一个或多个 api 版本已被废弃, 只需用Deprecated修饰您的控制器。这并不意味着不支持 API 版本。你仍然可以调用该版本。它只是一种让 调用API 用户意识到以下版本在将来会被弃用。

上面把Deprecated设置为TRUE表示,版本1.0将来会被弃用。访问我们的API接口,可以在响应头中可以看到,下面信息,如下图所示:

ApiVersionNeutral特性的使用:

ApiVersionNeutral 特性定义此 API 不在支持版本控制。无论 支持api 版本控制或不支持 api 版本控制的旧式 api,这对于行为完全相同的 api 非常有用。因此, 您可以添加 ApiVersionNeutral 属性以从版本控制中退出。

获取版本信息(Version Information)

如果你想知道那个版本的客户端在被访问,你可以通过下面的代码实现该功能:

综上所述, 具有多个版本的 API 可以帮助以一种有效的方式推出增强的功能, 同时也便于跟踪更改。在这篇文章中, 我们看到了如何在 ASP.NET  core WEB API 中添加对多个版本的支持。nuget 包支持通过查询字符串参数进行版本控制, 在 URL 中添加路径段和通过标头。它还具有版本单一 API 操作和从版本中选择退出的功能。

能不能不借助第三方的包来实现一个API的版本控制,方法是有的,不卖关子了,大家接着往下看。

新建一个core API项目:

 

在VersionControl文件夹下面,新建一个实现了IApplicationModelConvention接口的类NameSpaceVersionRoutingConvention   代码如下:

复制代码

1 public class NameSpaceVersionRoutingConvention:IApplicationModelConvention 2 {
3 private readonly string apiPrefix; 4 private const string urlTemplate = “{0}/{1}/{2}”;
5 public NameSpaceVersionRoutingConvention(string apiPrefix = “api”)
6 {
7 this.apiPrefix = apiPrefix; 8 }
9
10 public void Apply(ApplicationModel application) 11 { 12 foreach (var controller in application.Controllers) 13 { 14
15 var hasRouteAttribute = controller.Selectors 16 .Any(x => x.AttributeRouteModel != null); 17 if (!hasRouteAttribute) 18 { 19 continue; 20 } 21 var nameSpaces = controller.ControllerType.Namespace.Split(‘.’); 22 //获取namespace中版本号部分
23 var version = nameSpaces.FirstOrDefault(x => Regex.IsMatch(x, @”^v(\d+)$”)); 24 if (string.IsNullOrEmpty(version)) 25 { 26 continue; 27 } 28 string template = string.Format(urlTemplate, apiPrefix, version, 29 controller.ControllerName); 30 controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel() 31 { 32 Template = template 33 }; 34 } 35 } 36 }

复制代码

调试代码发现这种方式只在程序第一次运行的时候会执行,之后不会再执行多次,因此效率很高。

通过上面两种版本控制的实现和对比,我更偏向通过第三方的包来实现版本控制,这种方法功能更强大。这纯属于个人爱好了,大家可以根据不同的场景来决定使用哪种方式来实现,好了讲到这里,谢谢,希望对你有帮助。

觉得可以的话,希望点下推荐哈~你们的推荐是我的动力。

多谢@光阴四溅 及时指出我的错误,很感谢!

代码下载地址:WebApiVersionControl.rar

作者:郭峥

出处:http://www.cnblogs.com/runningsmallguo/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

2017年最后几天,你们都高高兴兴的跨年,博主还在加班制作.net安装包。因为年前要出来第一版的安装包,所以博主是加班加点啊。本来想用VS自带的制作工具,不过用过的人都知道,真是非常好(tong)用(ku),各种包需要单独下载不说,界面也不美观,所以决定弃用之。同事推荐用Advanced Installer,不过同事也是很久不用了,记不起来具体用法。所以博主是边摸索边前进,所以不免跳进坑里,为了避免后来人跳进同样的坑里,作文以记之。

好了,不说废话,正文开始!

俗话说的好,“磨刀不误砍柴工”!

首先强调一点,我们要先弄清楚我们每一步要做什么,这样接下来在制作安装包时才能条理清晰,有条不紊,不容易犯错。

比如我们要判断客户机器上是否安装.net framework3.5及以上版本,是否安装IIS,是否安装SQL Server2005及以上版本数据库等等。

我这个项目的流程图如下:

Tips1:开始之前最好先画流程图

有了流程图,只需按部就班!关于Advanced Installer的基础用法,网上有很多的资料,官网也有帮助文档,这里就不赘述了。由于要用到自定义安装包的过程,需要新建对话框,博主用了企业模板,Advanced Installer的版本是14.5.2,也是最新的版本。

1、选择模板

首先新建模板里面选择Enterprise,并选择语言(默认为中文),然后创建项目:

Tips2:如果安装包中需要自定义对话框,则需要选择Enterprise版。

2、设置基本信息

Name即为该安装包的名称,公司就是你所在的公司啦,还有版本信息等等,这个很简单。

3、设置默认安装路径

点击Package Definition->Install Parameters,填写默认安装路径。

Application folder默认的内容是这样:“[ProgramFilesFolder][Manufacturer]\[ProductName]”, 这个内容表示安装时默认的安装路径:

[ProgramFilesFolder]表示文件夹,这里为D:\Program Files(x86)。

[Manufacturer]表示公司名称,这里为Landsoft。

[ProductName]表示产品名称,这里为LandaV9。

 

4、设置安装条件

点击Requirements->Launch Conditions->Software,勾选相应的条件。这里根据流程图所示,我应该勾选.Net Framework、IIS以及SQL Server。注意这里的条件指的是最低条件,比如我这里SQL Server选的是2005,即客户机器上应该装有2005及以上版本的SQL Server,如果没有装,则会提示没有安装SQL2005。

这里还有系统条件即要求客户机器是win7还是winxp等等。如果还有更多的条件,Custom里面可以自定义。

 

5、添加文件或文件夹

点击Resources->Files and Folders,在Application Folders上右键,选择Add Folder或者Add Files,这里我添加了5个文件夹,用于后续操作。

6、自定义对话框(Dialog)

新建对话框之前可以在Themes中选择主题和样式。

点击User Interface->Dialogs,在FolderDlg上右键,选择Add Dialog是添加Advanced Installer中自定义的对话框,选择New Dialog是新增一个空的对话框,

然后我们可自行在上面定义内容:

新建对话框后,可以拖出安装过程中dialog的外观,这个对于我们.Net开发人员来说,应该是小Case吧,看一下效果:

可以对文本框设置默认值:

Tips3:文本框中的属性名,不要修改。如果修改会有大问题,至于什么问题,后面再说!

由于我们需要设置连接字符串,所以需要客户设置数据库的相关信息:

选择Add Dialog后,在弹出框中选择SQLServerConnentionDlg

效果如下:

最后再新建一个对话框,用于部署项目到IIS所需要的条件:

同样可以设置网站名称和端口号:根据流程图,这里的默认值需要分别设置为Mango和8001,用户可以自行修改。

到这里新建对话框告一段落。

7、设置必填验证

就拿上面这个对话框为例,虽然我们设置的有默认值,但是客户在安装过程中不小心给删掉了,又没有填写,恰巧又点了下一步,这样的话,安装过程中会有问题。所以我们要做必填验证,即文本框中没有值,则“下一步”按钮不可用,有值才可用。

所以选择“下一步”,关注下方的“Control Conditions”:

 

点击New按钮在新弹出的界面中在Condition中填写“NOT EDIT_1_DROP OR NOT EDIT_1_DROP_1”,在Action中选择Disable。

EDIT_1_DROP和EDIT_1_DROP_1就是网站名称和端口号,前面加NOT的意思就是,如果这两个文本框中任意一个没有填写,则按钮不可用(Disable)。

同理再添加一个“EDIT_1_DROP AND EDIT_1_DROP_1”,在Action中选择Enable。即都填内容了,则按钮可用。这样就对控件做了必填验证。是不是很简单呢!

8、设置桌面快捷方式

根据流程图所示,安装结束后,需要在桌面生成快捷防方式。所以接下来,我们就来做这件事:

首先来到Files and Folders选项卡,在Application Shortcut Folder上面右键 选择“New Shortcut”,弹出如下界面:

在这个框里主要设置快捷方式的Name和描述以及图标(这个图标,可以选择.exe文件)。点击OK,就会在Application Shortcut Folder中显示这样一条信息。

但是先别急,如果仅仅是这样的话,快捷方式是不起作用的。因为我们还没有设置快捷方式对应的可执行文件。

双击这条信息,在弹出的对话框中设置Shortcut Target。

看到这,肯定有人有疑问?既然这样为什么不在新增快捷方式的时候一起设置呢?

因为在新增的时候,Shortcut target这个文本框中的内容不能修改。这个很坑爹,博主也试了好久才试出来的。

最后,将这个信息拖到Desktop里面即可,安装完毕后,就会在桌面显示这个快捷方式。

在上面对话框的最后一步中,我们记住网站名称的和端口号的文本框的属性名,然后来到IIS选项卡中:

首先新增一个应用程序池,名称即为网站名称文本框的属性名,这里为EDIT_1_PROP,在Advanced Installer里面,使用中括号[]将控件的属性名括起来表示变量的概念。

在Basic Pool Settings中设置程序池的启动模式为总是启动,勾选立即启动程序池复选框,framework版本选择为4.0,托管模式选择为集成。

在Identity中设置应用程序池表示为ApplicationPoolIdentity。

应用程序池设置好之后,再来设置站点:

新建一个站点名称同样为EDIT_1_PROP,在Basic Site Setting中设置Name和文件路径。

在Bindings/SSL中设置端口号,IP地址选择全部未分配。

在Application Pool中选择刚刚新建的应用程序池。

到这里IIS这一块就设置好了。

安装过程中,Advanced Installer就会自动的将网站部署好。这个真的是很牛很强大!不得不佩服人家做的就是好!

1、直接在Advanced Installer中设置

在添加文件的时候,如果有xml类型的文件,会有这样的提示:

这时候我们勾选需要修改配置的文件,点击ok,这样就可以直接在Advanced Installer中修改。

找到需要修改的节点,直接有文本框的属性替换掉就可以:

这样,在安装过程中就会将config文件中的相应的节点替换为客户输入的值。

还记得Tips3吗?我们说过如果修改文本框的属性名之后,那么这样设置之后,config中的值不会设置为客户输入的值,会始终是文本框中设置的默认值。

切记切记!!!重要的事情三个叹号!

2、使用自定义的dll文件

关于这点请见第六小节。

点击Custome Behavior->Custom Actions,选择.Net Installer Class action。这时会让你选择文件中的dll文件。

当然了,在此之前,请打开你的Visual Studio,新建一个类库项目,这里命名为InstallLandaV9ServiceT,然后新建一个安装程序类,重写Install方法。

到这里,我们先暂停一下,问一下自己:在程序中怎么接收从Advanced Installer传过来的参数呢?

假如此时我们已经将这个dll加入到Advanced Installer中,在Installer Class Paramters中可以添加参数。

比如将文件路径传到后台,可以这样添加:Name为FilePath,Value为[APPDIR]\。

这里有地方需要注意,传递文件路径时,Value为[APPDIR]\,这个”\“请不要漏掉了。

Tips4:传递文件路径时,Value为[APPDIR]\,这个”\“请不要漏掉了。

其他参数的Value,需要设置为对应文本框的属性名称,格式为:[属性名]。

下面来看一看代码:

复制代码

public override void Install(IDictionary stateSaver)
{
Parameters configParms = new Parameters();
configParms.FilePath = Context.Parameters[“FilePath”];
configParms.ServerPath = Context.Parameters[“ServerPath”];
configParms.ClientPath = Context.Parameters[“ClientPath”];
configParms.DataSource = Context.Parameters[“DataSource”];
configParms.UserId = Context.Parameters[“UserId”];
configParms.Password = Context.Parameters[“Password”]; new Operators().UpdateLandaV9Config(configParms); base.Install(stateSaver);
}

复制代码

这里主要接收从Advanced Installer中传过来的参数,接着在Operators类中更新配置文件。

还记得Tips3吗?如果在那里修改的属性名,那么这里接收的参数也是文本框的默认值。

这里面主要干了三件事,获取连接字符串,更新连接字符串,安装服务

复制代码

public void UpdateLandaV9Config(Parameters configParms)
{ string filePath = Path.Combine(configParms.FilePath, “LandaV9Service”, “LandaEntryInfoService.exe.config”); string connectionString = this.GetConnectionString(configParms); this.UpdateConnections(filePath, connectionString);this.InstallService(Path.Combine(configParms.FilePath, “LandaV9Service”)); }

复制代码

还记得Tips4吗?如果在传递文件路径的时候只是写[APPDIR],而不是[APPDIR]\,那么程序运行到这里的时候,会抛“FileNotFound”异常!

下来来看一下具体的代码:

复制代码

///


/// 获取连接字符串 ///

///
///
public string GetConnectionString(Parameters op)
{ return string.Format(“Data Source={0};Initial Catalog=LandaEntryManager;Persist Security Info=True;User ID={1};Password={2}”, op.DataSource, op.UserId, op.Password);
} ///
/// 更新连接字符串 ///

///
///
public void UpdateConnections(string filePath, string conn)
{
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(filePath);
XmlElement xmlElement = (XmlElement)xmlDocument.SelectSingleNode(“/configuration/connectionStrings/add[@name=’LandaEntryManager’]“); if (xmlElement != null)
xmlElement.SetAttribute(“connectionString”, conn);
xmlDocument.Save(filePath);
}

复制代码

再来看安装服务:

复制代码

///


/// 安装服务 ///

///
public void InstallService(string filePath)
{
Environment.CurrentDirectory = filePath;
Process process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = “Install.bat”;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Verb = “runas”; //主要设置为以管理员启动
process.Start();
}

复制代码

有安装就有启动,可是如果紧跟其后写启动服务,会提示找不到服务,安装程序就会出错。

博主想了一个办法,重写OnAfterInstall方法,在这个方法里面写启动服务:

复制代码

///


/// 重启服务 ///

///
protected override void OnAfterInstall(IDictionary savedState)
{
ServiceController serviceController = new ServiceController(“LandaInformationServiceV9”);
serviceController.Start(); base.OnAfterInstall(savedState);
}

复制代码

这样就没问题了!

在这个程序集里面可以自定义你想做的其他事情,比如,还原数据库等等。

本次教程到此结束,这也是博主在这几天假期摸索出来的,如果有不当之处,尽请指正!

附上Advanced Installer的官网地址:https://www.advancedinstaller.com/

希望本文对你有所帮助。如果你觉得本文能够帮助你,就点个赞吧。你的支持是博主继续坚持的不懈动力。

蓝牙BLE官方Demo下载地址:   http://download.csdn.net/detail/lqw770737185/8116019
参考博客地址:    http://www.eoeandroid.com/thread-563868-1-1.html?\_dsign=843d16d6

设备:MX5手机一台,农药残留检测仪一台(BLE终端设备)

目的:实现手机控制仪器,如发送打印指令,仪器能进行打印操作等

关于如何打开蓝牙,配置相关权限,搜索BLE设备等步骤网上有很多资料,这里就不多做作解释了,本文主要讲通信方面的内容。需要注意的是搜索BLE设备的结果是异步返回的,在BluetoothAdapter.LeScanCallback这个回调中返回,并且搜索过程是一个非常耗电的过程,所以我们应该做好相应处理,例如可以让它搜索10s就停止搜索等。

在我们理解Android设备与BLE终端设备通信过程之前,我们需要来先了解几个类:

BluetoothGatt:BluetoothGatt 是我们用的最多,也是我们最重要的一个类,为了尽可能通俗的理解,这里我们可以把它看成Android手机与BLE终端设备建立通信的一个管道,只有有了这个管道,我们才有了通信的前提。

BluetoothGattService:蓝牙设备的服务,在这里我们把BluetoothGattService比喻成班级。而Bluetoothdevice我们把它比喻成学校,一个学校里面可以有很多班级,也就是说我们每台BLE终端设备拥有多个服务,班级(各个服务)之间通过UUID(唯一标识符)区别。

BluetoothGattCharacteristic: 蓝牙设备所拥有的特征,它是手机与BLE终端设备交换数据的关键,我们做的所有事情,目的就是为了得到它。在这里我们把它比喻成学生,一个班级里面有很多个学生,也就是说我们每个服务下拥有多个特征,学生(各个特征)之间通过UUID(唯一标识符)区别。

总结:当我们想要用手机与BLE设备进行通信时,实际上也就相当于我们要去找一个学生交流,首先我们需要搭建一个管道,也就是我们需要先获取得到一个BluetoothGatt,其次我们需要知道这个学生在哪一个班级,学号是什么,这也就是我们所说的serviceUUID,和charUUID。这里我们还需要注意一下,找到这个学生后并不是直接和他交流,他就好像一个中介一样,在手机和BLE终端设备之间帮助这两者传递着信息,我们手机所发数据要先经过他,在由他传递到BLE设备上,而BLE设备上的返回信息,也是先传递到他那边,然后手机再从他那边进行读取。

Android手机与BLE终端设备通信结果都是以回调的形式返回,如下是几个常见的返回回调(可见于官方Demo的BluetoothLeservice类):

复制代码

private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { //连接状态改变的回调
@Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { // 连接成功后启动服务发现
Log.e(“AAAAAAAA”, “启动服务发现:” + mBluetoothGatt.discoverServices());
}
}; //发现服务的回调
public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, “成功发现服务”);
}else{
Log.e(TAG, “服务发现失败,错误码为:” + status);
}
}; //写操作的回调
public void onCharacteristicWrite(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, “写入成功” +characteristic.getValue());
}
}; //读操作的回调
public void onCharacteristicRead(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, “读取成功” +characteristic.getValue());
}
} //数据返回的回调(此处接收BLE设备返回数据)
public void onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic) {
};
};
}

复制代码

一、连接蓝牙BLE终端设备

在我们打开蓝牙,扫描发现想要连接的设备后,接下来要做的,当然就是去连接它了,连接方法很简单,我们一句代码就可以实现了(如下)。可以发现,当我们开始连接BLE终端设备的时候,连接方法就自动就帮我们返回了一个BluetoothGatt对象了,前面我们说到BluetoothGatt是我们最重要的一个类,它相当于一个管道,是我们建立通信的前提:(这里面有三个参数,第一个参数是上下文对象,第二个参数是是否自动连接,这里设置为false,第三个参数就是上面的回调方法)

mBluetoothGatt = device.connectGatt(this, false, mGattCallback);

连接成功与否都会通过下面这个回调来告诉我们:

复制代码

// 连接状态改变的回调
@Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { //代表连接成功,此处我们可以发送一个广播回去告诉activity已成功连接
if (newState == BluetoothProfile.STATE_CONNECTED) { //连接成功后启动服务发现
Log.e(“AAAAAAAA”, “启动服务发现:” + mBluetoothGatt.discoverServices());
}
}

复制代码

二、启动服务发现

 连接成功后,我们就要去寻找我们所需要的服务,这里需要先启动服务发现,使用一句代码即可:

mBluetoothGatt.discoverServices() ;

启动服务发现,它的结果也是通过回调函数返回:

复制代码

    // 发现服务的回调
    public void onServicesDiscovered(BluetoothGatt gatt, int status) { //成功发现服务后可以调用相应方法得到该BLE设备的所有服务,并且打印每一个服务的UUID和每个服务下各个特征的UUID
        if (status == BluetoothGatt.GATT\_SUCCESS) {
                List<BluetoothGattService> supportedGattServices =mBluetoothGatt.getServices(); for(int i=0;i<supportedGattServices.size();i++){
                   Log.e("AAAAA","1:BluetoothGattService UUID=:"+supportedGattServices.get(i).getUuid());
                   List<BluetoothGattCharacteristic> listGattCharacteristic=supportedGattServices.get(i).getCharacteristics(); for(int j=0;j<listGattCharacteristic.size();j++){
                            Log.e("a","2:   BluetoothGattCharacteristic UUID=:"+listGattCharacteristic.get(j).getUuid());
                                }
                             }
        } else {
            Log.e("AAAAA", "onservicesdiscovered收到: " + status);
        }
    }

复制代码

我们也可以通过调用下面方法得知每个特征所具有的属性:可读或者可写或者具备通知功能或者都具备等

复制代码

// 循环遍历服务以及每个服务下面的各个特征,判断读写,通知属性
for (BluetoothGattService gattService :supportedGattServices) {
List gattCharacteristics =supportedGattServices.getCharacteristics(); for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { int charaProp = gattCharacteristic.getProperties(); if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) { // Log.e(“nihao”,”gattCharacteristic的UUID为:”+gattCharacteristic.getUuid()); // Log.e(“nihao”,”gattCharacteristic的属性为: 可读”);
} if ((charaProp | BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) { // Log.e(“nihao”,”gattCharacteristic的UUID为:”+gattCharacteristic.getUuid()); // Log.e(“nihao”,”gattCharacteristic的属性为: 可写”);
} if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { // Log.e(“nihao”,”gattCharacteristic的UUID为:”+gattCharacteristic.getUuid()+gattCharacteristic); // Log.e(“nihao”,”gattCharacteristic的属性为: 具备通知属性”);
}

        }
    }

复制代码

三、获取Characteristic

       之前我们说过,我们的最终目的就是获取Characteristic来进行通信,正常情况下,我们可以从硬件工程师那边得到serviceUUID和characteristicUUID,也就是我们所比喻的班级号和学号,以此来获得我们的characteristic,但如果我们无法得知这两个所需的UUID时,我们也可以通过上一步的方法来获取(打印所有特征UUID,取出自己想要的特征)。这次试验我就是通过此方法获得,但是通过打印日志发现,虽然我这边的BLE终端它每个服务下的所有特征都具有可读可写可通知属性,但是只有characteristicUUID=”0000ffe1-0000-1000-8000-00805f9b34fb”这个特征UUID能进行通信,它对应的serviceUUID=”0000ffe0-0000-1000-8000-00805f9b34fb”,暂且就先用这个,反正一个能用就行。假设我们在知道serviceUUID和characteristicUUID的前提下,我们就可以通过下面代码获取相应特征值:

BluetoothGattService service = mBluetoothGatt.getService(UUID.fromString(“0000ffe0-0000-1000-8000-00805f9b34fb”));
BluetoothGattCharacteristic characteristic= service.getCharacteristic(UUID.fromString(“0000ffe1-0000-1000-8000-00805f9b34fb”));

四、开始通信

       我们在得到一个相应的特征以后,接下来就可以开始读写操作进行通信了。

a.读操作,读操作比较简单,只需将相应的特征值传入即可得到该特征值下的数据,如下代码:       

mBluetoothGatt.readCharacteristic(characteristic);

读取的结果通过onCharacteristicRead回调返回:(通过characteristic.getValue()就可以得到读取到的值了)

复制代码

// 读操作的回调
    public void onCharacteristicRead(BluetoothGatt gatt,
            BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT\_SUCCESS) {
            Log.e(TAG, "读取成功" +characteristic.getValue());
        }
    }

复制代码

       b.写操作,写操作是我们的重点,我们可以通过向characteristic写入指令(发送指令)以此来达到控制BLE终端设备的目的:

                       //将指令放置进特征中
                        characteristic.setValue(new byte\[\] {0x7e, 0x14, 0x00, 0x00,0x00,(byte) 0xaa}); //设置回复形式

characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); //开始写数据
mBluetoothGatt.writeCharacteristic(chharacteristic);

注:与仪器通信,我们这里发送的是16进制的数据,发送的时候需要先将其装载到byte[]数组中,例如我发送 7e 14 00 00 00 aa这个指令,我需要把它转化为ew byte[] {0x7e, 0x14, 0x00, 0x00,0x00,(byte) 0xaa }这样去发送,因为BLE传输过程每次最大只能传输20个字节,所以如果发送的指令大于20字节的话要分包发送,例如现在要发送28个字节的,可以先write(前20个字节),开启线程sleep(几十毫秒)后在write(后面8个字节)

五、BLE终端设备返回数据
       当我们向BLE终端设备写入指令时,如果写入成功并且指令也正确,我们就会获得相应的响应指令,在下面这个回调中我们可以得到BLE设备返回回来的响应指令(通过characteristic.getValue()取出返回数据):

// 数据返回的回调(此处接收机器返回数据并作处理)
    public void onCharacteristicChanged(BluetoothGatt gatt,
            BluetoothGattCharacteristic characteristic) {
        Log.e("AAAAAAAA",characteristic.getValue());
    };

接收到返回数据的前提是我们设置了该特征具有Notification功能,所以完整的写操作代码应该是这样的(注意设置特征Notification的代码要放在最前):

复制代码

       mBluetoothGatt.setCharacteristicNotification(characteristic,true) //将指令放置进来
       characteristic.setValue(new byte\[\] {0x7e, 0x14, 0x00, 0x00,0x00,(byte) 0xaa}); //设置回复形式

characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); //开始写数据
mBluetoothGatt.writeCharacteristic(chharacteristic);

复制代码

以上就是自己对Android手机与BLE设备通信一些初步认识,如果有哪里说的不正确,还请指正。

一、修改版本和指定生成APK文件名【可选】

将项目切换到Project视图,打开app目录下的build.gradle文件

1.1 修定软件版本

如1.2图所示。

versionCode是app的大版本号,为数值类型,默认为1我这里改为2。

versionName是app的具体版本号,为际符串类型,默认为1.0我这里改为2.3。

1.2 指定生成的APK文件名

一样是在build.gradle文件中修改,默认生成的release版apk名为app-release.apk。

在android内部defaultConfig同层下添加以不内容(outputFileName改成自己想要的apk名)

复制代码

android.applicationVariants.all {
variant -> variant.outputs.all { // 此处指定生成的apk文件名
outputFileName = “SecTest.apk” }
}

复制代码

二、进行安全加固【可选】

如果想提高APP的反编译能力,可以对APP进行安全加固,参看APP安全防护基本方法(混淆/签名验证/反调试)

三、导出签名发布版****apk

直接点“Build APK(s)”生成的是使用默认的debug.keystore签名的Debug版apk(生成在app\build\outputs\apk\debug目录下),真正发布软件时我们需要生成自己密钥签名的release版apk。

菜单栏-—Build—-Generate Signed APK

在Key store path中输入自己要用来进行签名的密钥文件的存放位置,同时输入密钥文件的读写用户名密码(如果还没有密钥文件点击图中的“Create New”进行创建即可)

APK Destination Folder—–APK文件生成的目录

Build Type—-生成release版还是debug版

Signature Version—-对哪些部分进行签名

生成完成后在指定生成目录下即会有一个release文件夹在其下即可找到生成的apk(app-release.apk是我之前生成的版本)