引言
为规范研发团队的详细设计过程,提高设计质量与设计输出能力,特编制此文档。
设计过程
为便于理解系统设计的整个过程,本文选取最为常见的用户登录认证的需求为示例,通过该需求的设计过程中的每一步,设计人员可对整个设计过程的方法和工具有深入全面的理解。
需求示例:
-
系统应采用口令、验证码组合的鉴别技术对用户进行身份鉴别,身份标识具有唯一性,密码采用不可逆算法加密加盐存储。
-
系统应具有登录失败处理功能,应配置限制非法登录次数等措施,登录失败次数超过一定的次数,系统将自动锁定用户,锁定用户一定时间后方可继续登录。
需求
分析
识别业务用例
发现参与者
涉众:也称为干系人,是与要建设的系统有利益相关的一切人和事,涉众的利益要求会影响系统的建设。只要和这个系统有利益关系的都是这个项目的涉众。
参与者:是涉众的代表,对系统的要求直接影响系统的建设,他们的要求就是系统需求的来源。参与者通过对系统提出要求来获得他所代表的涉众的利益。
可通过以下两个问题来识别参与者:
-
谁对系统有着明确的目标和要求并且主动发出动作?
-
系统为谁服务?
识别业务用例
用例(Use Case)是把现实世界的需求捕获下来的方法。用例定义了一组用例实例,其中每个实例都是系统所执行的一系列操作。
一个完整的用例定义由参与者、前置条件、场景、后置条件构成。
用例的特征:
-
用例是相对独立的。
-
一个用例就是一个需求单元、分析单元、设计单元、开发单元、测试单元。
-
用例必然是以动宾短语形式出现的。
-
这件事必须由一个参与者发起。
-
用例执行结果对参与者来说是可观测的和有意义的。
示例:
在该需求中,用户所有的活动都集中在登录认证的活动中,所以登录认证作为本需求的业务用例。我们就可以画出如下用例图:
对于用例图中无法表示的内容,可以通过用例规约进行详细说明,以上用例的用例规约如下:
用例名称:
简要说明: 用户访问系统登录页,通过口令及验证码登录的过 事件流: 基本事件流:
扩展事件流:
非功能需求: 前置条件 无 后置条件 a)如果用户输入正确的用户名、密码、验证码,则登录成功。 b)如果用户输入的用户名、密码、验证码之一不正确,则登录失败。 c)如果用户连续X次登录失败,则系统锁定用户。锁定一定时间后才可继续登录。 |
说明:
以上示例中仅包含一个业务主角和一个业务用例,在我们实际的需求中,通常都会有多个业务主角和多个业务用例,设计人员在需求分析时,可按照如上方法,找出所有的业务主角,针对每一个业务主角识别出该业务主角所有的业务用例。
识别关键业务用例
什么是关键需求:
关键的功能需求,即涉及的模块最多、最典型的功能,或必须实现的重要的功能。
关键的质量属性需求,是那些经过权衡取舍、最终决定重点支持的质量属性需求。
关键的商业需求。
示例:
由于本次需求仅登录认证一个业务用例,是必须实现功能,所以登录用例作为本次需求的关键用例。
说明:
对于多业务主角、多业务用例的场景,设计人员需要认真评估哪些业务主角的哪些用例是关键用例,输出关键用例的列表。
关键业务用例分析
关键用例的分析方法有很多,主要工具有时序图、协作图、鲁棒图等。
时序图用于描述按时间顺序排列的对象之间的交互模式。
图 登录认证时序图
使用鲁棒图对关键用例进行分析
图 认证用例实现初稿
以上是认证登录用例的实现初稿,初步看上去,好像没什么问题,但是仔细分析一下,却发现还有一些细节没有体现出来,比如验证码返回前端的不是数字,而应该是图片,未考虑登录失败处理,未考虑登录凭证等。
于是,我们对上述鲁棒图进行了调整,调整后的如下:
调整后的实现
上图还存在一个问题,就是失败处理在这里没有体现具体的细节,如需要判断失败次数是否超过阈值,如果是,则锁定用户,否则返回相应的错误信息。用户被锁定后,在什么节点进行解锁判断,也需要考虑。但由于鲁棒图不太擅长表现此类复杂场景,可换做流程图(泳道图)表现更合适。
通过完成后的鲁棒图,可识别出一系列页面、动作和实体,这些将成为下一步工作的重要输入。
领域建模
所谓领域(Domain),即是对现实世界问题的一种统称,是一个组织的业务开展方式,体现一个组织所做的事情以及其中所包含的一切业务范围和所进行的活动,我们在开发软件时面对的就是组织的领域。例如,一个电商网站的领域包含了产品名录、订单、库存和物流的概念,而医疗信息化公司关注挂号、就诊、用药、健康报告等领域。
示例:
通过上面用例分析步骤中,我们识别出了一系列实体:用户、登录日志、会话、验证码,这些就可以看做领域实体,实体关系图如下:
领域模型
实体关系图中需要表现出实体间的关系,用户与会话是一对多的关系,用户与登录日志也是一对多的关系,验证码中无用户信息。但是验证码需要存储在会话中以确保下次请求时,系统能够找回验证码。
如果用例中涉及到了状态的变化,就需要通过状态图来表现状态之间的转换规则,本用例涉及到账户的未登录、登录、锁定、登录失败等状态的转换,所以需要通过状态图表示出来,状态图如下:
状态图表示领域模型
系统设计
技术关注点
这里列出技术上需要进行预研、技术上比较有难度或重要性比较高的的关注点,并通过预研最终确定技术选型。
示例
-
认证
-
密码采用不可逆算法加密加盐存储。
-
随机验证码转换成为图片,且图片需具备一定的混淆能力。
解决方案
-
密码通存储采用不可逆的加密算法(
MD5
)
+ S alt
,使用方法如
MD5(username+password+Salt)
,
其中
Salt
为随机字符串,并妥善保存。
-
采用
java.awt
包实现验证码图片。
-
会话管理
-
系统需有专门的模块对会话进行管理,包括会话创建、存储、销毁、会话超时等控制。
技术分析
当前业界常用的安全框架有
Spring Security
、
jwt
、
Shiro
等,各自优缺点如下:
Spring Security
Spring Security
是一个能够为基于
Spring
的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在
Spring
应用上下文中配置的
Bean
,充分利用了
Spring IoC
(控制反转),
DI
(
依赖注入)和
AOP
(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。它是一个轻量级的安全框架,它确保基于
Spring
的应用程序提供身份验证和授权支持。它与
Spring MVC
有很好地集成,并配备了流行的安全算法实现捆绑在一起。安全主要包括两个操作“认证”与“验证”(有时候也会叫做权限控制)。“认证”是为用户建立一个其声明的角色的过程,这个角色可以一个用户、一个设备或者一个系统。“验证”指的是一个用户在你的应用中能够执行某个操作。在到达授权判断之前,角色已经在身份认证过程中建立了。
JWT
JWT
又名
Json Web Token
,基于数字签名,定义了一个紧凑、字包含的方式,用于
json
在各方之间安全传输信息。
使用场景
一般用于授权认证和数据交换:
Authorization (
授权
) :
这是使用
JWT
的最常见场景。一旦用户登录,后续每个请求都将包含
JWT
,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的
JWT
的一个特性,因为它的开销很小,并且可以轻松地跨域使用
Information Exchange (
信息交换
) :
对于安全的在各方之间传输信息而言,
JSON Web Tokens
无疑是一种很好的方式。因为
JWTs
可以被签名,例如,用公钥
/
私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
Shiro
Apache Shiro
是
Java
的一个安全框架,在轻量级的程序应用更广泛,简化了
Spring security
的功能,提供了处理身份认证,授权,企业会话管理和加密的功能。
应用领域
-
验证用户
-
对用户执行访问控制,如:
-
判断用户是否拥有角色
admin
。
-
判断用户是否拥有访问的权限
-
在任何环境下使用
Session API
。例如
CS
程序。
-
可以使用多个用户数据源。例如一个是
oracle
用户库,另外一个是
mysql
用户库。
-
单点登录(
SSO
)功能。
-
“
Remember Me
”服务
,类似购物车的功能,
shiro
官方建议开启
核心领域:
-
身份验证
Authentication
(重要模块)
-
授权
Authorization
(重要模块)
-
会话管理
Session Management
(重要模块)
-
加密
Cryptography
(重要模块)
-
web support
:
Web
支持,可以非常容易的集成到
Web
环境
(
次要
)
-
Caching
:缓存,比如用户登录后,其用户信息、拥有的角色
/
权限不必每次去查,这样可以提高效率;
(
次要
)
-
Concurrency
:
shiro
支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
(
次要
)
-
Testing
:提供测试支持;
(
次要
)
-
Run As
:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
(
次要
)
-
Remember Me
:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
(
次要
)
|
|
|
|
|
|
|
|
|
|
|
|
解决方案
-
经过综合考虑,采用
Shiro
安全框架对会话进行管理比较适合当前的技术框架。
组件设计
组件划分
回顾我们在用例分析、领域建模的输出(页面、操作、实体),这些将会成为组件划分的依据之一,组件划分的另一依据是对技术关注点细化的结果。
示例
我们在关键用例分析中识别以下内容:
页面:登录页、首页。
操作:生成验证码、生成验证码图片、保存验证码、返回验证码、校验验证码、读取验证码、校验用户密码、失败处理、获取用户信息、生成会话、准备首页数据。
实体:验证码、登录日志、用户、会话。
我们在技术关注点中识别出了MD5加密、会话管理等。
分析过程:
1.
页面部分如何划分,取决于后台模块如何划分。所以我们先分析后端的模块。
2.
几个验证码相关的操作(生成验证码、生成验证码图片、保存验证码、读取验证码)可合并为一个模块,其中生成验证码图片可以放在验证码模块中,也可以拆分出通用的模块,取决于系统中是否有此类的复用要求。这里我们将生成验证码图片独立出来成为通用模块,所以我们识别出了两个模块:验证码处理、数字转图片。
3.
校验验证码、校验用户密码、失败处理、生成会话等几个操作都是为登录服务的,离开登录而独立存在就没有意义,所以这几个可以合并成登录模块。
4.
准备首页数据这个操作和业务有关,包含一系列的操作,也不是本用例的主要关注点,所以在未能看清的情况下,可设置首页模块。
5.MD5
加密作为通用模块,会话管理采用Shiro,可作为单独的模块。
根据以上的分析,登录页面可纳入登录模块,首页页面可纳入到首页模块。
经过以上分析我们划分了一下模块:
-
登录控制:包括校验验证码、校验用户密码、失败处理、生成会话、登录页面。
-
验证码处理:包括生成验证码、验证码转图片、保存验证码、读取验证码。
-
会话管理:创建会话、获取会话、销毁会话。
-
首页:准备首页数据、首页页面。
-
数字转图片
-
加密处理
接下来我们还需要对这些模块进行分类,使其归属到不同的子系统或上层组件中。
登录控制、验证码处理、会话管理在逻辑上都是和登录相关的,所以这几个可以合并到一个上层组件中。
首页控制涉及到业务,不在本次设计重点,单独放置在一个上层组件中。
数字转图片、加密处理等属于通用功能,所以可以放在上层的通用组件中,组件图如下:
图 组件图
组件协作
示例
图 组件协作图
组件设计
图 登录控制组件
类设计
|
LoginController |
|
com.design.sample.security.web.controller |
|
|
login |
处理用户登录请求 |
checkCaptcha |
校验验证码 |
checkAccountPassword |
校验用户名密码 |
createSession |
创建Session |
handleFail |
失败处理 |
checkLockedAccount |
校验账户是否处于锁定状态 |
lockAccount |
锁定账户 |
unlockAccount |
解锁账户 |
|
public User login(String account, String password, String captchaCode) |
||
|
|
|
|
account |
账号 |
入参 |
|
password |
密码 |
入参 |
|
captchaCode |
验证码 |
入参 |
|
|
|||
|
|
private Boolean checkCaptcha(String captchaCode) |
||
|
|
|
|
captchaCode |
验证码 |
入参 |
|
|
|||
|
|
private Boolean checkAccountPassword(String account, String password) |
||
|
|
|
|
account |
账号 |
入参 |
|
password |
密码 |
入参 |
|
|
|||
将password按照加密规则加密,将加密后的字串与用户信息中的password字段进行比较,如果匹配返回true,否则返回false。 |
|
private Boolean createSession(String account, String password) |
||
|
|
|
|
account |
账号 |
入参 |
|
password |
密码 |
入参 |
|
|
|||
将password按照加密规则加密,将加密后的字串与用户信息中的password字段进行比较,如果匹配返回true,否则返回false。 |
|
private String handleFail(User user) |
||
|
|
|
|
account |
账号 |
入参 |
|
password |
密码 |
入参 |
|
|
|||
如果超过阈值,则将用户设置为锁定状态,记录锁定时间,返回错误消息“登录失败超过X次,用户已锁定,请Y分钟后重试”。 |
|
private Boolean checkLockedAccount(User user) |
||
|
|
|
|
user |
用户信息 |
入参 |
|
|
|||
|
|
private void lockAccount(User user) |
||
|
|
|
|
user |
用户信息 |
入参 |
|
|
|||
|
|
private Boolean unlockAccount(User user) |
||
|
|
|
|
user |
用户信息 |
入参 |
|
|
|||
|
图 验证码处理组件
|
CaptchaGenerater |
|
com.design.sample.security.captcha |
|
|
createCaptcha |
创建验证码 |
gnenrateCaptchaCode |
生成验证码code |
codeToImage |
将验证码code转换成图片 |
图 会话管理
|
SessionManager |
|
com.design.sample.security.session |
|
|
createSession |
创建session |
getSession |
获得本地session |
removeSession |
销毁session |
图 数字转图片
|
CaptchaImage |
|
com.design.sample.common.tools |
|
|
codeToImage |
将code转换为图片 |
|
public static BufferImage codeToImage(String code) |
||
|
|
|
|
code |
待转换的code |
入参 |
|
|
|||
略 |
图 加密处理
|
EncryptUtil |
|
com.design.sample.common.tools |
|
|
encryptMd5 |
md5编码 |
|
public static String encryptMd5(String content) |
||
|
|
|
|
content |
加密内容 |
入参 |
|
|
|||
略 |
实体设计
图 实体关系图
实体类
|
User |
|
com.design.sample.security.entity |
|
|
String id |
id |
String name |
名称 |
String password |
密码 |
String account |
账号 |
Integer sex |
性别 |
String mobile |
手机号码 |
Date birthday |
出生日期 |
User createUser |
创建用户 |
Timestamp createTime |
创建时间 |
Timestamp modifyTime |
修改时间 |
Integer status |
状态 |
|
LoginLog |
|
com.design.sample.security.entity |
|
|
String id |
id |
String account |
账号 |
Timestamp loginTime |
登录时间 |
Integer loginStatus |
登录状态 |
数据库设计
|
|
|||
|
|
|
|
|
id |
id |
character varying |
|
|
name |
名称 |
character varying |
|
|
password |
密码 |
character varying |
|
|
account |
账号 |
character varying |
|
|
sex |
性别 |
character(1) |
|
|
mobile |
手机号码 |
character(11) |
|
|
birthday |
出生日期 |
Date |
|
|
create_user |
创建用户 |
character varying |
|
|
create_time |
创建时间 |
Timestamp |
|
|
modify_time |
修改时间 |
Timestamp |
|
|
status |
状态 |
Integer |
|
0 正常 1 锁定 2 注销 |
主键字段 |
id |
索引列表 |
|
索引名称 |
USER_UIDX_ACCOUNT |
索引类型 |
唯一索引 |
索引字段 |
account |
索引名称 |
USER_IDX_NAME |
索引类型 |
普通索引 |
索引字段 |
name |
|
|
|||
|
|
|
|
|
id |
id |
character varying |
|
|
account |
账号 |
character varying |
|
|
login_time |
登录时间 |
Timestamp |
|
|
login_status |
登录状态 |
Integer |
|
0 成功 1 失败 |
|
|
|
|
|
|
|
|
|
|
主键字段 |
id |
索引列表 |
|
索引名称 |
LOGIN_LOG_IDX_ACCOUNT |
索引类型 |
普通索引 |
索引字段 |
account, login_time |