Blazor入手教程(二)css和class绑定 - 小小爵 - 博客园

mikel阅读(1151)

来源: Blazor入手教程(二)css和class绑定 – 小小爵 – 博客园

Css和class绑定

 

Blazor中的css和class绑定还是比较便利的。方式也和vue 十分类似,感觉唯一区别就是Blazor中拼接时不用像vue那样用+加号拼接字符串

 

 

@page "/cssbinding"
<style scoped>
    .active{
        width:80px;
        height:80px;
    }
    .active2 {
        background-color:#ff6a00;
    }
</style>
<h3>绑定单个css属性</h3>
<div style="font-size:@(fontsize)px">文字大小</div>
<h3 style="margin-top:100px">绑定多个css属性</h3>
<div style="@style.ToString()"></div>
<h3 style="margin-top:100px">绑定多个class</h3>
<div class="@getClass()"></div>
@code {
    public class Style
    {
        public int height { get; set; } = 100;
        public int width { get; set; } = 100;
        public string color { get; set; } = "#ccc";
        public override string ToString()
        {
            return $"width:{width}px;height:{height}px;background-color:{color}";
        }
    }
    public int fontsize =30;
    public Style style = new Style();
    public string[] classArray = new string[] { "active", "active2" };
    public string getClass()
    {
        return string.Join(" ", classArray);
    }
}

 

 

Blazor入手教程(一)前言 - 小小爵 - 博客园

mikel阅读(1067)

来源: Blazor入手教程(一)前言 – 小小爵 – 博客园

Blazor入手教程(一)前言

 

结论

最近在学习blazor。得出了这么一个结论:

Blazor是一门很值得学习的技术,未来.net下将会有相当多的 web应用使用blazor开发。十分看好这一技术,原因有这么两点:

1,开发效率高。选择了Blazor就等于选择了全栈开发。以往的经验告诉我,全栈开发的效率更高,全栈省去了相当一部分的沟通成本,而且使用blazor时可以重用很多以往的C#代码,这能提高不少效率。

2,社区发展势头很好。已经有很多很不错的组件库AntDesign-blazor,Bootsrap Blazor等等。且微软对blazor也是相当支持的,从它把blazor的文档放到最前面以及文档的详细程度就可以看出。

 

两种模式

Blazor 提供了两种模式,服务端模式(server-inside) 和客户端模式(client-inside)

服务端基于SignalR ,以websocket上连接来保持状态和UI的一致。

客户端模式是基于Webassemely,可以理解为在浏览器执行你的编写的dll文件。

个人感觉Server-inside的应用场景较少,Client-inside才是会被广泛应用的模式,所以本篇主要介绍客户端模式的blazor。

 

和Vue 的对比学习

因为之前有相当一段时间的全栈开发经历(vue),所以在学习的过程发现和blazor和vue有许多相通的地方,比如组件,api,生命周期,数据驱动的思想 等等。如果你之前有学习过vue,那么上手应该是很快的(两天上手毫不夸张)。如果没有,那么在学习过程中一定要先习惯这种数据驱动视图的开发思想。

 

Nginx 的简单使用 (IIS,Asp.Net) - 小小爵 - 博客园

mikel阅读(943)

来源: Nginx 的简单使用 (IIS,Asp.Net) – 小小爵 – 博客园

Nginx 的一些常见功能(windows,AspNet ,IIS

 

 

下载

官方网站https://nginx.org/en/download.html

下载,解压缩是这个样子

 

 

 

启动:

启动方式有两种

方式一:双击nginx.exe

 

方式二:进入cmd 到该目录下,运行 start nginx

 

 

启动闪退,查看错误日志,原因是80端口已经被占用了,所以我们找到nginx.conf文件,换了一个监听的端口8085

 

 

 

 

 

 

 

错误日志文件

 

 

 

 

 

 

 

 

,再次启动,启动成功,启动成功。发现多了一个nginx.pid文件,在浏览器里面访问localhost:8035

 

这个样子就是成功了

 

这里是日志文件

记录了每次请求

 

 

 

接下来,我们做第一个nginx常用的功能:

使用ngxix负载均衡

把请求分散到多个主机

 

 

 

这里我们新建了一个aspnetmvc项目,功能很简单,

 

 

发布之后,复制两个一样的文件夹,只改动web。Config里面的serverid。分别部署到8001端口,8002端口

 

 

8001的配置文件

 

8002的配置文件

 

 

 

 

 

Serverid 1

 

Serverid 2

 

 

 

 

 

我们的目标是:访问8035端口时,将请求通过nginx转到8001和8002

 

 

配置前:

8085

 

8001

 

8002

 

开始配置

 

 

1,在server节点上面加入 upstream节点,起个名字 my_web_server,

 

在大括号里面加入要映射的地址,格式是 server + 地址 +权重,权重越高,访问越多

 

2,修改server节点,里面的server_name 和上面保持一致

 

3,修改 location 节点 名字也要和上面保持一致

 

 

 

齐活。

 

保存配置文件,在cmd中重启nginx配置文件

 

 

重启命令 nginx –s reload

 

 

然后我们再去访问8085 ,配置成功

 

 

 

 

可以看到一次1,一次二,目前是两个不加任何策略配置

接下来我们加上ip_hash 的策略

 

 

接着访问多次8085

,发现他只会映射到8001上面去,这个就是ip_hash策略的作用:根据不通ip的哈希值,指定到固定端口上面去,ip_hash是很常用的一种策略,可以解决一点点的session不共享问题

 

 

 

其他的几种策略这里不再赘述,

可以去看看这里 https://blog.csdn.net/balalamm/article/details/79916483

 

 

 

使用nginx代理二级目录

 

 

我们经常会遇到这样的 结构

 

比如说这是某个应用的接口地址http://api.example.com/,他有v1和v2两个版本

放在不同的服务器或者端口上,我们想使用下面的url来请求

http://api.example.com/v1/  和  http://api.example.com/v2/

 

这时候我们就可以是用nginx来处理

 

我们的目标是

 

请求   http://localhost:8085/v1/    转到8001端口

 

请求   http://localhost:8085/v2/      转到8002端口

 

修改配置文件,这里暂时用不到upstream 节点了

 

修改成这个样子

 

加一个location 就行,是不是很简单,注意8001/后面的斜杠 /一定不能少

 

效果图  v1/

 

 

V2/

 

 

Nginx的功能是十分强大,以上只是基础,其他的功能比如:使用nginx防盗链,请求日志记录,拦截请求等等,使用nginx实现要比在应用程序中实现简单快捷很多,总而言之就是nginx很有学习的必要。

 

附录:nginx常量对照表(原文 https://blog.csdn.net/echizao1839/article/details/80872378

 

$http_user_agent, $http_cookie, 等等。下面是nginx支持的所有内置变量:

$arg_name:请求中的的参数名,即“?”后面的arg_name=arg_value形式的arg_name

$args:请求中的参数值

$binary_remote_addr:客户端地址的二进制形式, 固定长度为4个字节

$body_bytes_sent:传输给客户端的字节数,响应头不计算在内;这个变量和Apache的mod_log_config模块中的“%B”参数保持兼容

$bytes_sent:传输给客户端的字节数 (1.3.8, 1.2.5)

$connection:TCP连接的序列号 (1.3.8, 1.2.5)

$connection_requests:TCP连接当前的请求数量 (1.3.8, 1.2.5)

$content_length:“Content-Length” 请求头字段

$content_type:“Content-Type” 请求头字段

$cookie_name:cookie名称

$document_root:当前请求的文档根目录或别名

$document_uri:同 $uri

$host:优先级如下:HTTP请求行的主机名>”HOST”请求头字段>符合请求的服务器名

$hostname:主机名

$http_name:匹配任意请求头字段; 变量名中的后半部分“name”可以替换成任意请求头字段,如在配置文件中需要获取http请求头:“Accept-Language”,那么将“-”替换为下划线,大写字母替换为小写,形如:$http_accept_language即可。

$https:如果开启了SSL安全模式,值为“on”,否则为空字符串。

$is_args:如果请求中有参数,值为“?”,否则为空字符串。

$limit_rate:用于设置响应的速度限制,详见 limit_rate

$msec:当前的Unix时间戳 (1.3.9, 1.2.6)

$nginx_version:nginx版本

$pid:工作进程的PID

$pipe:如果请求来自管道通信,值为“p”,否则为“.” (1.3.12, 1.2.7)

$proxy_protocol_addr:获取代理访问服务器的客户端地址,如果是直接访问,该值为空字符串。(1.5.12)

$query_string:同 $args

$realpath_root:当前请求的文档根目录或别名的真实路径,会将所有符号连接转换为真实路径。

$remote_addr:客户端地址

$remote_port:客户端端口

$remote_user:用于HTTP基础认证服务的用户名

$request:代表客户端的请求地址

$request_body:客户端的请求主体,此变量可在location中使用,将请求主体通过proxy_pass, fastcgi_pass, uwsgi_pass, 和 scgi_pass传递给下一级的代理服务器。

$request_body_file:将客户端请求主体保存在临时文件中。文件处理结束后,此文件需删除。如果需要之一开启此功能,需要设置client_body_in_file_only。如果将次文件传递给后端的代理服务器,需要禁用request body,即设置proxy_pass_request_body off,fastcgi_pass_request_body off, uwsgi_pass_request_body off, or scgi_pass_request_body off 

$request_completion:如果请求成功,值为”OK”,如果请求未完成或者请求不是一个范围请求的最后一部分,则为空。

$request_filename:当前连接请求的文件路径,由root或alias指令与URI请求生成。

$request_length:请求的长度 (包括请求的地址, http请求头和请求主体) (1.3.12, 1.2.7)

$request_method:HTTP请求方法,通常为“GET”或“POST

$request_time:处理客户端请求使用的时间 (1.3.9, 1.2.6); 从读取客户端的第一个字节开始计时。

$request_uri:这个变量等于包含一些客户端请求参数的原始URI,它无法修改,请查看$uri更改或重写URI,不包含主机名,例如:”/cnphp/test.php?arg=freemouse”。

$scheme:请求使用的Web协议, “http” 或 “https

$sent_http_name:可以设置任意http响应头字段; 变量名中的后半部分“name”可以替换成任意响应头字段,如需要设置响应头Content-length,那么将“-”替换为下划线,大写字母替换为小写,形如:$sent_http_content_length 4096即可。

$server_addr:服务器端地址,需要注意的是:为了避免访问linux系统内核,应将ip地址提前设置在配置文件中。

$server_name:服务器名,www.cnphp.info

$server_port:服务器端口

$server_protocol:服务器的HTTP版本, 通常为 “HTTP/1.0” 或 “HTTP/1.1

$status:HTTP响应代码 (1.3.2, 1.2.2)

$tcpinfo_rtt, $tcpinfo_rttvar, $tcpinfo_snd_cwnd, $tcpinfo_rcv_space:客户端TCP连接的具体信息

$time_iso8601:服务器时间的ISO 8610格式 (1.3.12, 1.2.7)

$time_local:服务器时间(LOG Format 格式) (1.3.12, 1.2.7)

$uri:请求中的当前URI(不带请求参数,参数位于$args),可以不同于浏览器传递的$request_uri的值,它可以通过内部重定向,或者使用index指令进行修改,$uri不包含主机名,如”/foo/bar.html”。

Aspnet Mvc 前后端分离项目手记(四)vue项目的搭建(一)(iview) - 小小爵 - 博客园

mikel阅读(677)

来源: Aspnet Mvc 前后端分离项目手记(四)vue项目的搭建(一)(iview) – 小小爵 – 博客园

一项目创建

1,搭建vue-cli脚手架(依赖npm)

没有安装npm的同学,请先使用npm install -g vue-cli ,然后再进行这一步

安装的过程中有几项

? Project name p1        //项目名
? Project description 1   //描述
? Author jimsfriend          //作者
? Vue build standalone
? Install vue-router? Yes   //是否使用路由(请选择Yes)
? Use ESLint to lint your code? No  //这里是是否使用严格模式,一定要选否,不要问我为什么,严格模式很痛苦!
? Set up unit tests No            //这里随便
? Setup e2e tests with Nightwatch? No       //这里随便

然后就是下载依赖包,得等会儿

 

二 项目结构

1,build:webpack配置文件,可以不用动

2,config,配置文件

dev.env.js看名字叫生产环境.js,没啥重要的用途,可以不用关注

这个也是

比较重要的是config/index.js文件

 

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
var path = require('path')
module.exports = {
  build: { // production 环境
    env: require('./prod.env'), // 使用 config/prod.env.js 中定义的编译环境
    index: path.resolve(__dirname, '../dist/index.html'), // 编译输入的 index.html 文件
    assetsRoot: path.resolve(__dirname, '../dist'), // 编译输出的静态资源路径
    assetsSubDirectory: 'static'// 编译输出的二级目录
    assetsPublicPath: '/'// 编译发布的根目录,可配置为资源服务器域名或 CDN 域名
    productionSourceMap: true// 是否开启 cssSourceMap
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false// 是否开启 gzip
    productionGzipExtensions: ['js''css'// 需要使用 gzip 压缩的文件扩展名
  },
  dev: { // dev 环境
    env: require('./dev.env'), // 使用 config/dev.env.js 中定义的编译环境
    port: 8080, // 运行测试页面的端口
    assetsSubDirectory: 'static'// 编译输出的二级目录
    assetsPublicPath: '/'// 编译发布的根目录,可配置为资源服务器域名或 CDN 域名
    proxyTable: {}, // 需要 proxyTable 代理的接口(可跨域)
    // CSS Sourcemaps off by default because relative paths are "buggy"
    // with this option, according to the CSS-Loader README
    // (https://github.com/webpack/css-loader#sourcemaps)
    // In our experience, they generally work as expected,
    // just be aware of this issue when enabling this option.
    cssSourceMap: false // 是否开启 cssSourceMap
  }
}

 

 这段代码是抄了园友,更加详细的原文地址    https://www.cnblogs.com/whkl-m/p/6627864.html

 3,dist: 编译后的打包文件默认是没有的,

 

项目编译打包:

cmd 到project1目录,运行:npm run build ,然后对多出来一个dist文件,这个dist文件就是编译后的项目,发布的时候只需把dist放在 web宿主 (如 IIS)上即可

 4,mode_modules,node模块,就是一些依赖包,

5 ,src,主要编写代码文件,后面会单独介绍

6,static,也是静态文件存放文件

7,剩下的都是写不怎么重要的,省略了。

 

二 项目运行,进入project1 目录  在cmd中运行 npm run dev  回车即可

 

然后就运行起来,访问  http://localhost:8080/ 

 

 

三 安装iview,

 

也是project1目录下,运行 npm install iview –save

装好之后基本上依赖项和环境就Ok,然后就开干了

Iview的文档地址    http://v1.iviewui.com/docs/guide/install,一定要注意版本,不同的版本api不同

 

 

先了解一下src的项目结构

 

assets,放一些静态文件,比如图片图标啥的

components , 存放组件 ,理解为抽取出来的一些公共的自定义组件

js,这个是我自己新建的文件夹放一些公共的js文件比如 用axios粉装的Http请求 , 常用的工具类等等

router ,里面有一个index.js 文件,是路由文件 。就像ASPNET MVC 中的路由表,只不过他是显式的定义出来,而mvc中默认使用ctroller前缀和action名作为路由。

views文件夹,放主要的页面,代码量最多的地方

App.vue,入口的组件

main.js入口文件

清楚之后,我们来写一个登录页

先搞一个_layout文件放布局页,是不是和mvc里面的_layout很像?没错他们都是一个意思

 

这里的<router-view></router-view> 和mvc中的RendBody()是一个意思

 

然后搞 account文件夹,来放登陆注册这种页面 ,搞一个login.vue文件,就是登录页了

 

搞完之后就去访问 /account/login,你会发现啥都没有,因为还没有再router/index.js里面定义

,定义一个 account_login ,名字随便起 from后面是文件夹的路径

,下面的path是在浏览器中访问的地址

再次访问,已经有了

,写不动了,今天先写这么多,下一节写路由的使用

 

PHP中比较两个时间的大小与日期的差值_jimlong的专栏-CSDN博客_php时间比大小

mikel阅读(688)

来源: PHP中比较两个时间的大小与日期的差值_jimlong的专栏-CSDN博客_php时间比大小

在这里我们全用到时间戳

mktime(hour,minute,second,month,day,year,[is_dst])
其参数可以从右向左省略,任何省略的参数都会被设置成本地日期和时间的当前值。
参数 描述
hour 可选。规定小时。
minute 可选。规定分钟。
second 可选。规定秒。
month 可选。规定用数字表示的月。
day 可选。规定天。
year 可选。规定年。在某些系统上,合法值介于 1901 – 2038 之间。不过在 php教程 5 中已经不存在这个限制了。
is_dst 可选。如果时间在日光节约时间(dst)期间,则设置为1,否则设置为0,若未知,则设置为-1。自 5.1.0 起,is_dst 参数被废弃。因此应该使用新的时区处理特性

在日常生活中我们要经常比较时间的早晚,对于我们来说判断时间的大小很简单。但是时间的比较不只是单纯的数字大小的比较,因此相对来说还是比较复杂。那么在php中通过什么方式来比较两个时间的大小呢?

要比较两个时间的大小,我们需要将时间转化为时间戳格式,然后再进行比较这是最常用的方法。常用到的函数是:strtotime()
语法格式:strtotime(time,now)
如果time是绝对时间,则now参数不起作用
如果time是相对时间,则相对应的参数则对应函数就是now来提供,如果没有提供now参数,那么相对应的时间就是当前的本地时间。

实例:比较两个绝对时间的大小
代码:
<?php
$zero1=date(“y-m-d h:i:s”);
$zero2=”2010-11-29 21:07:00′;
echo “zero1的时间为:”.$zero1.”<br>”;
echo “zero2的时间为:”.$zero2.”<br>”;
if(strtotime($zero1)<strtotime($zero2)){undefined
echo “zero1早于zero2′;
}else{undefined
echo “zero2早于zero1′;
}
?>

输出结果:
zero1的时间为:2010-11-30 21:12:55
zero2的时间为:2010-11-29 21:07:00
zero2早于zero1

注:可以根据实例发散思维

计算两个日期的差值
奥运会倒计时,亚运会倒计时,生日倒计时这些倒计时都可以通过计算两个日期的差值来实现,同样需要用到strottime()函数。
实现倒计时需要将两个时间的差值整数化,需要用到函数ceil()
ceil()函数的作用是求不小于给定实数的最小整数

实例:倒计时小程序
实例代码:
<?php
$zero1=strtotime (date(“y-m-d h:i:s”)); //当前时间
$zero2=strtotime (“2011-2-03 24:00:00′);  //过年时间
$guonian=ceil(($zero2-$zero1)/86400); //60s*60min*24h
echo “离过年还有<strong>$guonian</strong>天!”;
?>

输出结果:
离过年还有66天!

strtotime()函数解析

定义和用法
strtotime() 函数将任何英文文本的日期时间描述解析为 unix 时间戳。

语法
strtotime(time,now)参数 描述
time 规定要解析的时间字符串。
now 用来计算返回值的时间戳。如果省略该参数,则使用当前时间。

说明
该函数预期接受一个包含美国英语日期格式的字符串并尝试将其解析为 unix 时间戳(自 january 1 1970 00:00:00 gmt 起的秒数),其值相对于 now 参数给出的时间,如果没有提供此参数,则用系统当前时间

JSON Web Token 入门教程

mikel阅读(580)

作者: 阮一峰

日期: 2018年7月23日

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。

一、跨域认证的问题

互联网服务离不开用户认证。一般流程是下面这样。

1、用户向服务器发送用户名和密码。

2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

3、服务器向用户返回一个 session_id,写入用户的 Cookie。

4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

二、JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。


{
  "姓名": "张三",
  "角色": "管理员",
  "到期时间": "2018年7月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

三、JWT 的数据结构

实际的 JWT 大概就像下面这样。

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下。

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

写成一行,就是下面的样子。


Header.Payload.Signature

下面依次介绍这三个部分。

3.1 Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。


{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

3.2 Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。


{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

3.3 Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。


HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

3.4 Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

四、JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。


Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

五、JWT 的几个特点

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

六、参考链接

(完)

Aspnet Mvc 前后端分离项目手记(二)关于token认证 - 小小爵 - 博客园

mikel阅读(713)

来源: Aspnet Mvc 前后端分离项目手记(二)关于token认证 – 小小爵 – 博客园

在前后端分离的项目中,首先我们要解决的问题就是身份认证

以往的时候,我们使用cookie+session,或者只用cookie来保持会话。

 

一,先来复习一下cookie和session

首先我们来复习一下在aspnet中cookie和session的关系,做一个简单试验

这是一个普通的view没有任何处理

 

 

可以看到,没有任何东西(cookie),然后当我们写入一个session之后

\

 

 

 

会发现多了一个名为ASP.NET_SessionId的cookie。我们都知道在aspnet中,session是保存在服务器端的内存中的,而http协议是无状态的,那么他是怎么确定不同请求的session

没错,session是借助cookie来实现的:cookie中保存着 session的key,当我们清除掉浏览器缓存时,会发现session也找不到了,就是这个原因。

使用session来保持会话有几个很严重的缺点:1 session容易丢失;2无法支持分布式;3,cookie 对跨域的支持不好

 

所以就用到了我们今天说的token

二,token 

1,token的产生

一般是用户登录成功,服务器端产生一个token并返给前端,前端将token保存在cookie或者localStorage里面,然后每次请求时都带上这个token,一般都带在请求头里面

 2,token的内容

一般的token里面必须有的是:1,会话用户的标识:比如userid。2,token的过期时间,如果想更完整一点,可以加上token的颁发者,签名等等

3,token的生成算法,一般是由服务器端将token的主要内容,过期时间等等做非对称加密,然后进行签名算法(防止客户端更改),具体看后面jwt

 

4,token校验

当服务器端收到请求时,首先会校验token,校验有两种不同的方式

 一, token产生后保存在服务器端(redis或者其他比较速度快的缓存中) 。优点:可控性强,可以用这个来做单点登录,比如另一个地方登录,就remove掉之前的token。缺点:实现麻烦一点,而且要占服务器压力

二, token产生后服务器端不保存,只负责校验。 优点:大大降低了服务器的压力,实现起来,也要相对简单一点。缺点:token一旦颁发,服务器端就不可控了,只能等它过期。

    具体用哪种看具体的需求。如果不是做可控性要求很强,个人建议第二种。

 

5 jwt 

jwt 全名Json Web Tokens,算是一种token的规范吧

园子里面有很不不错的介绍 ,比如这篇:阮一峰 jwt介绍   http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

 

组成有三部分

  • Header(头部,一般包含了token的签名方式)
  • Payload(负载,也就是具体的有效部分)
  • Signature(签名,将前两部分进行签名算法,防止客户端篡改)

实现方式,将header部分和payload部分分别进行base64算法,然后用点号“.”隔开拼接,然后进行签名算法,然后在将三部分拼接(点号隔开)就得到了jwt

注意 ,jwt默认是采用base64编码的,也就是说 客户端也能解码得出具体内容的,所以除非特殊情况,重要敏感字段一定不能放在token中

 

以下是具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
namespace Rk.JWT
{
 
    public class Jwt
    {
        //参考自 阮一峰 jwt介绍  http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
 
        public static string SALT = "OXpcRP8jmCfMKumY";
     
         
        /// <summary>
        ///
        /// </summary>
        /// <param name="ExraPayload">额外的信息</param>
        /// <returns></returns>
        public static string Create(Dictionary<string,object> ExraPayload)
        {
            var Header = new Dictionary<stringstring>();
            Header.Add("tp""MD5");
            var Payload = new Dictionary<stringobject>();
            //JWT 规定了7个官方字段,供选用。
            Payload.Add("iss""signBy"); //颁发人
            Payload.Add("jti", Guid.NewGuid().ToString()); //jwt的id
            Payload.Add("exp",System.DateTime.Now.AddMinutes(20));//过期时间
            Payload.Add("nbf", System.DateTime.Now);//生效时间
            Payload.Add("iat", System.DateTime.Now);//签发时间
            Payload.Add("sub""subject");//主题
            Payload.Add("aud""audience");//受众
            foreach (var item in ExraPayload)
            {
                if (Payload.ContainsKey(item.Key))
                {
                    throw new Exception($"{item.Key}键值已被占用 不能使用 ");
                }
                else
                {
                    Payload.Add(item.Key, item.Value);
                }
            }
            string base64Header = Base64Url(Newtonsoft.Json.JsonConvert.SerializeObject(Header));
            string base64Payload = Base64Url(Newtonsoft.Json.JsonConvert.SerializeObject(Payload));
            string tmp = base64Header + "." + base64Payload;
 
            string sign = Md5(tmp+ SALT);//加盐,重要
            return base64Header+"."+ base64Payload+"."+ sign;
        }
        //校验是否合法,是否过期
        public static bool Check(string token)
        {
            string base64Header = token.Split('.')[0];
            string base64Payload = token.Split('.')[1];
            string sign = token.Split('.')[2];
            string tmp = base64Header + "." + base64Payload;
            var signCheck = Md5(base64Header + "." + base64Payload + SALT);
            if(signCheck!= sign)
            {
                return false;
            }
            var dic = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<stringobject>>(Base64UrlDecode(base64Payload));
            if(  Convert.ToDateTime(dic["exp"])<System.DateTime.Now)
            {
                //过期了
                return false;
            }
            return true;
        }
        //校验是否合法,是否过期
        public static Dictionary<string,object> GetPayLoad(string token)
        {
    
            string base64Payload = token.Split('.')[1];
         
            var dic = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<stringobject>>(Base64UrlDecode(base64Payload));
           
            return dic;
        }
        public static string Base64Url(string input)
        {
            //JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。
            //Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。
            string output = "";
                byte[] bytes = Encoding.UTF8.GetBytes(input);
            try
            {
                output = Convert.ToBase64String(bytes).Replace('+''-').Replace('/''_').TrimEnd('=') ;
             
            }
            catch (Exception e)
            {
                throw e;
            }
            return output;
        }
        public static string Base64UrlDecode(string input)
        {
            string output = "";
         
            input = input.Replace('-''+').Replace('_''/');
            switch (input.Length % 4)
            {
                case 2:
                    input += "==";
                    break;
                case 3:
                    input += "=";
                    break;
            }
            byte[] bytes = Convert.FromBase64String(input);
            try
            {
                output = Encoding.UTF8.GetString(bytes);
            }
            catch
            {
                output = input;
            }
            return output;
        }
        public static string Md5(string input,int bit=16)
        {
            
            MD5CryptoServiceProvider md5Hasher = new MD5CryptoServiceProvider();
            byte[] hashedDataBytes;
            hashedDataBytes = md5Hasher.ComputeHash(Encoding.GetEncoding("gb2312").GetBytes(input));
            StringBuilder tmp = new StringBuilder();
            foreach (byte in hashedDataBytes)
            {
                tmp.Append(i.ToString("x2"));
            }
            if (bit == 16)
                return tmp.ToString().Substring(8, 16);
            else
            if (bit == 32) return tmp.ToString();//默认情况
            else return string.Empty;
           
        }
    }
    
}

 

使用方式

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class HomeController : BaseController
   {
       public ActionResult Login(string username, string pwd)
       {
           /// 1, todo 验证用户名密码正确
           //2,//在token中加入用户id,创建token
           var dic = new Dictionary<stringobject>();
           dic.Add("userid""20125521225858");
           string token = JWT.Jwt.Create(dic);
           //验证token是否正确是否过期
           var isChecked = JWT.Jwt.Check(token);
           return Content("");
       }
   }

 

下一篇我们将会聊一聊 rest 风格url在前后端分离项目中的使用

MVC 统一验证Token demo - _小马哥 - 博客园

mikel阅读(544)

来源: MVC 统一验证Token demo – _小马哥 – 博客园

复制代码
/// <summary>
        /// 获取token
        /// </summary>
        /// <param name="staffId"></param>
        /// <returns></returns>
        public JsonResult GetToken(string staffId)
        {
            ResultMsg resultMsg = null;

            //判断参数是否合法
            if (string.IsNullOrEmpty(staffId))
            {
                resultMsg = new ResultMsg();
                resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;
                resultMsg.Info = "staffId不合法";
                resultMsg.Data = new Token();
                return Json(resultMsg, JsonRequestBehavior.AllowGet);
            }

            //插入缓存
            Token token = (Token)HttpRuntime.Cache.Get(staffId);
            if (HttpRuntime.Cache.Get(staffId.ToString()) == null)
            {
                token = new Token();
                token.StaffId = staffId;
                token.SignToken = Guid.NewGuid();
                token.ExpireTime = DateTime.Now.AddDays(1);
                HttpRuntime.Cache.Insert(token.StaffId.ToString(), token, null, token.ExpireTime, TimeSpan.Zero);
            }

            //返回token信息
            resultMsg = new ResultMsg();
            resultMsg.StatusCode = (int)StatusCodeEnum.Success;
            resultMsg.Info = "";
            resultMsg.Data = token;
            return  Json(resultMsg, JsonRequestBehavior.AllowGet);

        }
复制代码
复制代码
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Mvc;
using WebApplication_Token.Models;

namespace WebApplication_Token.Controllers
{
    public class VerificationTokenController : Controller
    {
        protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            ResultMsg resultMsg = null;
            var request = Request;
            var method = request.HttpMethod;
            string staffid = string.Empty, timestamp = string.Empty, nonce = string.Empty, signature = string.Empty;

            if (!string.IsNullOrEmpty(request.Headers["staffid"]))
            {
                staffid = HttpUtility.UrlDecode(request.Headers.GetValues("staffid").FirstOrDefault());
            }
            if (!string.IsNullOrEmpty(request.Headers["timestamp"]))
            {
                timestamp = HttpUtility.UrlDecode(request.Headers.GetValues("timestamp").FirstOrDefault());
            }
            if (!string.IsNullOrEmpty(request.Headers["nonce"]))
            {
                nonce = HttpUtility.UrlDecode(request.Headers.GetValues("nonce").FirstOrDefault());
            }
            if (!string.IsNullOrEmpty(request.Headers["signature"]))
            {
                signature = HttpUtility.UrlDecode(request.Headers.GetValues("signature").FirstOrDefault());
            }

            //GetToken方法不需要进行签名验证
            if (filterContext.ActionDescriptor.ActionName == "GetToken")
            {
                base.OnActionExecuting(filterContext);
                return;
            }

            //判断请求头是否包含以下参数
            if (string.IsNullOrEmpty(staffid) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(signature))
            {
                resultMsg = new ResultMsg();
                resultMsg.StatusCode = (int)HttpStatusCode.PartialContent;
                resultMsg.Info = "请求头缺少参数";
                resultMsg.Data = new Token();
                filterContext.Result = Json(resultMsg, JsonRequestBehavior.AllowGet);//返回json数据
                base.OnActionExecuting(filterContext);
                return;
            }

            //判断token是否有效
            Token token = (Token)HttpRuntime.Cache.Get(staffid);
            
            string signtoken = string.Empty;
            if (token == null)
            {
                resultMsg = new ResultMsg();
                resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;
                resultMsg.Info = "token为null";
                resultMsg.Data = new Token();
                filterContext.Result = Json(resultMsg, JsonRequestBehavior.AllowGet);//返回json数据
                base.OnActionExecuting(filterContext);
                return;
            }
            else
            {
                signtoken = token.SignToken.ToString();
            }

            bool timespanvalidate = token.ExpireTime > Convert.ToDateTime(timestamp);
            if (!timespanvalidate)
            {
                resultMsg = new ResultMsg();
                resultMsg.StatusCode = (int)HttpStatusCode.PartialContent;
                resultMsg.Info = "token已过期";
                resultMsg.Data = new Token();
                filterContext.Result = Json(resultMsg, JsonRequestBehavior.AllowGet);//返回json数据
                base.OnActionExecuting(filterContext);
                return;
            }

            //根据请求类型拼接参数
            NameValueCollection coll = Request.Form;
            string[] requestItem = coll.AllKeys;
            Dictionary<string, string> sArray = new Dictionary<string, string>();
            int j = 0;
            for (j = 0; j < requestItem.Length; j++)
            {
                sArray.Add(requestItem[j], Request.Form[requestItem[j]]);
            }
            var queryStr = GetQueryString(sArray);
            var _signature = GetSingnature(timestamp, queryStr.Item1, staffid, signtoken, queryStr.Item2);

            if(signature!= _signature)
            {
                resultMsg = new ResultMsg();
                resultMsg.StatusCode = (int)HttpStatusCode.PartialContent;
                resultMsg.Info = "token不合法";
                resultMsg.Data = new Token();
                filterContext.Result = Json(resultMsg, JsonRequestBehavior.AllowGet);//返回json数据
                base.OnActionExecuting(filterContext);
                return;
            }

        }

        /// <summary>
        /// 获取签名字符串
        /// </summary>
        /// <param name="parames"></param>
        /// <returns></returns>
        public Tuple<string, string> GetQueryString(Dictionary<string, string> parames)
        {
            // 第一步:把字典按Key的字母顺序排序
            IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parames);
            IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();

            // 第二步:把所有参数名和参数值串在一起
            StringBuilder query = new StringBuilder("");//签名字符串
            StringBuilder queryStr = new StringBuilder("");//url参数
            if (parames == null || parames.Count == 0)
            {
                return new Tuple<string, string>("", "");
            }

            while (dem.MoveNext())
            {
                string key = dem.Current.Key;
                string value = dem.Current.Value;
                if (!string.IsNullOrEmpty(key))
                {
                    query.Append(key).Append(value);
                    queryStr.Append("&").Append(key).Append("=").Append(value);
                }
            }

            return new Tuple<string, string>(query.ToString(), queryStr.ToString().Substring(1, queryStr.Length - 1));
        }

        /// <summary>
        /// 根据参数计算签名
        /// </summary>
        /// <param name="timeStamp">发起请求时的时间戳(单位:毫秒)</param>
        /// <param name="nonce">随机数</param>
        /// <param name="staffId">当前请求用户StaffId</param>
        /// <param name="signToken">signToken</param>
        /// <param name="data">参数url</param>
        /// <returns></returns>
        public string GetSingnature(string timeStamp, string nonce, string staffId,string signToken, string data)
        {
            var hash = System.Security.Cryptography.MD5.Create();
            //拼接签名
            var signStr = timeStamp + nonce + staffId + signToken + data;
            //将字符串中字符按升序排序
            var sortStr = string.Concat(signStr.OrderBy(c => c));
            var bytes = Encoding.UTF8.GetBytes(sortStr);
            //使用MD5加密
            var md5Val = hash.ComputeHash(bytes);
            //把二进制转大写十六进制
            StringBuilder result = new StringBuilder();
            foreach (var c in md5Val)
            {
                result.Append(c.ToString("X2"));
            }
            return result.ToString().ToUpper();

        }
    }
}
复制代码

 

从MVC到DDD的架构演进 - 木小丰 - 博客园

mikel阅读(598)

来源: 从MVC到DDD的架构演进 – 木小丰 – 博客园

DDD这几年越来越火,资料也很多,大部分的资料都偏向于理论介绍,有给出的代码与传统MVC的三层架构差异较大,再加上大量的新概念很容易让初学者望而却步。本文从MVC架构角度来讲解如何演进到DDD架构。

从DDD的角度看MVC架构的问题

代码角度:

  • 瘦实体模型:只起到数据类的作用,业务逻辑散落到service,可维护性越来越差;
  • 面向数据库表编程,而非模型编程;
  • 实体类之间的关系是复杂的网状结构,成为大泥球,牵一发而动全身,导致不敢轻易改代码;
  • service类承接的所有的业务逻辑,越来越臃肿,很容易出现几千行的service类;
  • 对外接口直接暴露实体模型,导致不必要开放内部逻辑对外暴露,就算有DTO类一般也是实体类的直接copy;
  • 外部依赖层直接从service层调用,字段转换、异常处理大量充斥在service方法中;

项目管理角度:

  • 交付效率:越来越低;
  • 稳定性差:不好测试,代码改动的影响范围不好预估;
  • 理解成本高:新成员介入成本高,长期会导致模块只有一个人最熟悉,离职成本很大;

第一层:初出茅庐

以上的问题越来越严重,很多人开始把眼光转向DDD,于是埋头啃了几本大部头的书,对以下概念有了基本的了解:

  • 统一语言
  • 限界上下文
  • 领域、子域、支撑域
  • 聚合、实体、值对象
  • 分层:用户接口层、应用层、领域层、基础层

于是把MVC架构进行了改造,演进成DDD的分层架构。

DDD分层架构:

image

image

 

MVC架构到DDD分层架构的映射:

image

image

 

至此,算了基本入门了DDD架构,扩展性也得到了一定的提升。不过随着业务的发展,不断冒出新的问题:

  • 一段业务逻辑代码,到底应该放到应用层还是领域层?
  • 领域服务当成原来的MVC中的service层,随着业务不断发展,类也在不断膨胀,好像还是老样子啊?
  • 聚合包含多个实体类,这个接口用不到这么多实体,为了性能还是直接写个SQL返回必要的操作吧,不过这样貌似又回到了MVC模式
  • 既然实体类可以包含业务逻辑、领域服务也可以放业务逻辑,那到底放哪里?
  • 资料上说领域层不能有外部依赖,要做到100%单测覆盖,可是我的领域服务中需要用到外部接口、中央缓存等等,那这不就有了外部依赖了吗?

第二层:草船借箭(战术设计)

带着问题不断学习他人经验,并不断的尝试,逐渐get到以下技能:

1、领域层

领域(domain)是个模块,包含以下组成部分,传统的service按功能可能拆分到任何一个地方,各司其职。

  • 1个聚合
  • 1到多个实体
  • 若干值对象
  • 多个DomainService
  • 1个Factory:新建聚合
  • 1个Repository:聚合仓储服务
聚合根(AggregateRoot)

聚合本身也是一个实体,聚合可以包含其他实体,其他实体不能脱离聚合而单独提供服务,比如一篇文章下的评论,评论必须从属于文章,没有文章也就没有评论。仓库层(repository)也必须是以聚合为核心提供服务的;

实体:可以理解为一张数据库表,必须有主键;

值对象:没有主键,依附于实体而存在,比如用户实体下住址对象,一般在数据库中已json字符串的形式存在;最常见的值对象是枚举;

仓库服务(repository)

资源库是聚合的仓储机制,外部世界通过资源库,而且只能通过资源库来完成对聚合的访问。资源库以聚合的整体管理对象。因此,一个聚合只能有一个资源库对象,那就是以聚合根命名的资源库。除此之外的其他对象,都不应该提供资源库对象。仓储服务的实现一般有Spring Data JPA、Mybatis两种方式。

如果是用Spring Data JPA实现,直接使用JPA注解@OneToOne、@OneToMany,配合fetch配置,即可一个方法查询出所有的关联实体。

如果是用Mybatis实现,那么repository需要加入多个mapper的引用,再手动做拼装。

这里有一个经典的Hibernate笛卡尔积问题,答案是在聚合根中,一般不会加在大量的关联实体对象。如果确实需要查询关联对象而关联对象又比较多怎么办呢?在DDD中有一个CQRS(Command-Query Responsibility Segregation)模式,是一种读写分离模式,在此场景中需要将查询操作放到查询命令中分页查询。

当然CQRS也是一个很复杂模式,不应照搬他人方案,而是根据自己的业务场景选择适合自己的方案,以下列举了CQRS的几种应用模式:

image

image

 

工厂服务(factory)

作用是创建聚合,只传入必要的参数,工厂服务内部隐藏复杂的创建逻辑。简单的聚合可以直接通过new、静态方法等创建,不是必须由factory创建。

领域服务

单个实体对象能处理的逻辑放到实体里,多个实体或有交互的场景放到领域服务里。

领域服务可不可以调用仓储层或外部接口? 可以,但不能直接和领域服务代码放一起,领域服务模块存放API,实现放基础层(infrastructure)。

领域服务对象不建议直接以聚合名+DomainService命名,而要以操作命令关联,比如用户保存服务命名为:UserSaveService, 审核服务:UserAuditSerivce。

2、应用层

应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

比如下订单服务的方法:

  1. public void submitOrder(Long orderId) {
  2. Order order = OrderFetchService.fetchById(orderId); //获取订单对象
  3. OrderCheckSerivce.check(order); //验证订单是否有效
  4. OrderSubmitSerivce.submit(order); //提交订单
  5. ShoppingCartClearService.clear(order); //移除购物车中已购商品
  6. NotifySerivce.emailNotify(order.getUser()); //发送邮件通知买家
  7. }

对于复杂的业务来说,应用层也有几种模式:

  • 编排服务:最典型比如Drools;
  • Command、Query命令模式;
  • 业务按Rhase、Step逐层拆分模式;

image

image

 

3、Maven模块划分

基础层是比较简单一层,不过这里还有个比较疑惑的问题:按照DDD的四层架构图去划分Maven模块,基础层是最上的一层,但是基础层也要包含基础组件供其他层使用,这时基础层应该是放到最下层,直接按照这样构建Maven模块会造成循环依赖。

image

image

 

相比来说,另一个架构图更准确一些,不过依然没有直观体现Maven模块如何划分。

image

image

 

我的最佳实践是将基础层拆分两部分,一部分是基础的组件+仓储API,一部分是实现,maven模块划分图如下所示:

image

image

 

第三层:运筹帷幄(战略设计)

经过以上的两层的磨炼,恭喜你把DDD战术都学习完了,应付日常的代码开发也够了,不过作为架构师来说,探索的道路还不能止步于此,接下来会DDD战略部分。战略部分关注点有3个:

  • 统一语言
  • 领域
  • 限界上下文
1、统一语言

统一语言的重要性可以根据Jeff Patton 在《用户故事地图》中给出的一副漫画来直观的描述:

image

image

 

统一语言是提炼领域知识的输出结果,也是进行后续需求迭代及重构的基础,统一语言的建立有以下几个要点:

  • 统一语言必须以文档的形式提供出来,并且在整个项目组的各团队达成共识;
  • 统一语言必须每个中文名有对应的英文名,并且在整个技术栈保持一致;
  • 统一语言必须是完整的,包含以下要素:
    1. 领域模型的概念与逻辑;
    2. 界限上下文(Bounded Context);
    3. 系统隐喻;
    4. 职责的分层;
    5. 模式(patterns)与惯用法。
2、领域划分

以事件风暴的形式(Event Storming),列出所有的用户故事(Use Story),用户故事可通过6W模型来构建,即描写场景的 Who、What、Why、Where、When 与 hoW 六个要素。然后圈选功能相近的部分,就形成了领域,领域又根据职能不同划分为:核心域、支撑域、通用域,

具体的过程有很多参考资料,这里不再细讲,最终的输出是领域划分图,以下是一个保险业务示例:

image

image

 

3、限界上下文

限界上下文包含两部分:上下文(Context)是业务目标,限界(Bounded)则是保护和隔离上下文的边界。

比如上图中的实现部分即是限界上下文的边界,虚线部分代表了领域的边界。限界上下文没有统一的划分标准,需要的读者根据自己的业务场景来甄别如何划分。

一个上下文中包含了相同的领域知识,角色在上下文中完成动作目标;

边界体现在以下几方面:

  • 领域逻辑层:确定了领域模型的业务边界,维护了模型的完整性与一致性,从而降低系统的业务复杂度;
  • 团队合作层:限界上下文一般也是用户换分团队的依据;
  • 技术实现层:限界上下文可当成是微服务的划分边界;

DDD的不足

DDD架构作为一套先进的方法论,在很多场景能发挥很大价值,但是DDD也不是银弹。高级的架构师把DDD架构当成一种工具,结合其他架构经验一起为业务服务。

DDD的不足有几个方面:

  1. 性能:DDD是基于聚合来组织代码,对于高性能场景下,加载聚合中大量的无用字段会严重影响性能,比如报表场景中,直接写SQL会更简单直接;
  2. 事务:DDD中的事务被限定在限界上下文中,跨多个限界上下文的场景需要开发者额外考虑分布式事务问题;
  3. 难度系数高,推广成本大:DDD项目需要领域专家专家,且需要特别熟悉业务、建模、OOP,对于管理者来说评估一个人是否真的能胜任也是一件困难的事情;

总结

本文从MVC架构开始讲述了如何从演进到DDD架构,限于篇幅很多DDD的知识点没有讲到,希望大家在实践过程中能灵活运用,尽享DDD给业务带来的价值。本文如有不足之处敬请反馈。

本文链接:从MVC到DDD的架构演进

作者简介:木小丰,美团Java技术专家,专注分享软件研发实践、架构思考。欢迎关注公共号:Java研发

mysql日期加一个天数获得新的日期 - php、凯 - 博客园

mikel阅读(1207)

来源: mysql日期加一个天数获得新的日期 – php、凯 – 博客园

2012-06-13 20:16 mySQL日期加一个天数获得新的日期

阅读更多

在当前的日期上加三天,天数随便改:

SELECT date_add(CURRENT_DATE(), interval 3 day);

在指定的日期上加三天:
SELECT date_add(‘2014-04-17’, interval 3 day);

在指定的具体时间上加一个时间:

如在”2012年1月1日两点”加上一个小时零十分零十秒的MySQL语句为:

select date_add(‘2014-04-17 2:00:00’, interval ‘1:10:10’ hour_second);

select date_add(日期, interval 1 day);
select date_add(日期, interval 1 hour);
select date_add(日期, interval 1 minute);
select date_add(日期, interval 1 second);
select date_add(日期, interval 1 microsecond);
select date_add(日期, interval 1 week);
select date_add(日期, interval 1 month);
select date_add(日期, interval 1 quarter);
select date_add(日期, interval 1 year);