�概述
任何一个微服务需要基本的安全保证, 也是遵循AAA原则: 鉴证,授权和计帐
- Authentication 要求是合法用户
- Authorization 要求有合法权限
- Accounting 要求有记录可追踪
让我们从需求分析到代码实现,尽量啰嗦地来说说怎么做一个看似简单的登录注册模块, 假设该服务叫做 Checklist 我的清单
需求分析
用例 Use case
除了使用绘图工具和画用例图, 还有有�几种方法通过脚本来�生成用例图
一是使用在线网站 yuml.me
UML 生成脚本如下
[User]-(Sign In)
[User]-(Sign Out)
[User]-(Sign Up)
[User]-(Forget Password)
[User]-(Change Password)
(Sign In)>(Remember Me)
(Sign Up)>(Send Verification Email)
(Forget Password)>(Send Reset Password Email)
(Change Password)<(Send Reset Password Email)
[Admin]^[User]
[Admin]-(Add User)
[Admin]-(Delete User)
[Admin]-(Lock User)
[Admin]-(Change Password Policy)
�二是使用是通过 plantuml 来生成
到 http://plantuml.com/ 上下载 plantuml.jar , 然后用如下命令生成用例图
java -jar plantuml.jar usecase.txt
示例UML 生成脚本如下
@startuml
User -> (Sign In)
User --> (Sign Out)
User --> (Sign Up)
User --> (activate)
User --> (forget/reset password)
:Admin: ---> (lock user)
:Admin: ---> (add user)
:Admin: ---> (delete user)
@enduml
三是使用graphviz
先安装graphviz, 再运行如下命令
dot usecase1.gv -Tpng -o usecase1.png
示例UML生成脚本如下
digraph G {
rankdir=LR;
subgraph clusterUser {label="User"; labelloc="b"; peripheries=0; user};
user [shapefile="stick.png", peripheries=0];
signin [label="Sign In", shape=ellipse];
signout [label="Sign Out", shape=ellipse];
signup [label="Sign Up", shape=ellipse];
user->signin [arrowhead=none];
user->signout [arrowhead=none];
user->signup [arrowhead=none];
}
用户故事 User Story
User Story 讲究 INVEST 原则
- "I" ndependent (of all others) 独立的
- "N" egotiable (not a specific contract for features) 可协商的
- "V" aluable (or vertical) 有价值的
- "E" stimable (to a good approximation) 可估量的
- "S" mall (so as to fit within an iteration) 足够小的
- "T" estable (in principle, even if there isn't a test for it yet) 可测试的
Sign Up 注册
- 作为一个未注册用户, 我想输入我的电子邮件地址和密码,注册到 Checklist
1.1 我必须输入合法和邮件地址,符合密码策略的密码以及一致的验证码进行注册
默认的密码策略是最低8个字符, �必须包含大小写字母和至少一个数字
| # | Story | Priority | Estimation | Deadline| Comments |
|---|---|---|---|---|
| 1.1.1 | �生成验证码 |---|---|---|--- |
| 1.1.2 | 显示注册表单|---|---|---|--- |
| 1.1.3 | 邮件地址格式验证|---|---|---|--- |
| 1.1.4 | 比较两次输入的密码是否相同|---|---|---|--- |
| 1.1.5 | 验证密码是否符合密码策略|---|---|---|--- |
| 1.1.6 | 验证输入的验证码|---|---|---|--- |
| 1.1.7 | 检查是否已有相同的邮件地址存在|---|---|---|--- |
| 1.1.8 | 输入验证无误后存入数据库,状态为pending|---|---|---|--- |
| 1.1.9 | 生成此用户的激活链接|---|---|---|--- |
| 1.1.10 | 向注册邮箱发送一封确认邮件|---|---|---|--- |
1.2 我的注册邮箱会收到一封验证邮件, 提示我点击注册连接, �从而激活我的注册帐户
1.3 当我完成激活后会自动跳到 Checklist 的首页, 提示我进行登录
实现
这次我们用Java实现,选择的框架是Spring Boot, 先从最笨最直接的方法入手, 之后再看看相关的框架 Spring Security 和 Apache Shiro 是怎么做的
Model
View
字段 | �控件 |
---|---|
username | text |
password | password |
confirmPassword | password |
rememberMe | checkbox |
forgetPassword | link |
创建项目
- 在 http://start.spring.io 上选择所需模块, 创建 Checklist 项目并打包下载
或者直接用 Spring Cli 直接生成
spring init --build=maven --java-version=1.8 --dependencies=web --packaging=jar --groupId=com.github.walterfan --artifactId=checklist
在实践中始终牢记 三个基本点
- 无模型不编程-MDD 模型驱动开发
- 无测试不开发-TDD 测试驱动开发
- 无度量不交付-MDD 度量驱动开发
领域模型很简单
Register
User
Role
测试用例也简单
- 注册
- 激活
- 登录
度量就只记录
- 注册次数
- 激活次数
- 性能数据
代码结构
废话不多说,上代码 checklist source codes on github
数据库我们选用两个
- h2 作为测试数据库
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
- mysql 作为产品数据库
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
表现层
表现层选用 Freemarker 作为后端模板, 前端选用 AngularJS + BootStrap
freemarker 是比较流行的后端页面生成的模板引擎, 这所以不用 JSP 和 JSF, 就是为了不想在后端模板层面引入太多逻辑和不必要的复杂性, freemarker 就只干模板引擎该干的事
在 src/main/resources/templates 做如下模板
- about.ftl
- admin.ftl
- footer.ftl
- header.ftl
- index.ftl
- layout.ftl
- login.ftl
主要的 Freemarker 模板 layout.ftl 如下
<#macro myLayout>
<!DOCTYPE html>
<html >
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="Kanban">
<meta name="author" content="Walter">
<link rel="icon" href="./images/favicon.ico">
<title>Check List</title>
<!-- Bootstrap core CSS -->
<link href="./css/bootstrap.min.css" rel="stylesheet">
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<link href="./css/ie10-viewport-bug-workaround.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="./css/app.css" rel="stylesheet">
<link href="./css/jumbotron-narrow.css" rel="stylesheet">
<script src="./js/vendor/jquery-1.11.2.min.js"></script>
<script src="./js/vendor/angular.js"></script>
<script src="./js/vendor/angular-sanitize.js"></script>
<script src="./js/vendor/angular-resource.js"></script>
<script src="./js/vendor/ui-bootstrap.js"></script>
<script src="./js/vendor/ui-bootstrap-tpls.js"></script>
<script src="./js/vendor/ngDialog.min.js"></script>
<script src="./js/app.js"></script>
</head>
<body>
<div class="container" >
<#include "header.ftl"/>
<div class="panel panel-default" >
<#nested/>
</div>
<#include "footer.ftl"/>
</div> <!-- /container -->
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="./js/vendor/ie10-viewport-bug-workaround.js"></script>
</body>
</html>
</#macro>
首页
源码 index.ftl 如下
<#import "layout.ftl" as layout>
<@layout.myLayout>
<div class="jumbotron">
<h2>Checklist</h2>
<p class="lead">
Checklist for your work and life
</p>
<p><a class="btn btn-lg btn-success" href="/checkist/add" role="button">Add a Check list</a></p>
</div>
<script>
$('li:eq(0)').addClass('active');
</script>
</@layout.myLayout>
登录页面
源码 login.ftl 如下
<#import "layout.ftl" as layout>
<@layout.myLayout>
<!-- refer to http://bootsnipp.com/snippets/featured/login-and-register-tabbed-form -->
<div class="page-header text-center">
<div class="row nav nav-tabs nav-justified">
<div class="col-xs-6">
<a href="#" class="active" id="login-form-link">Login</a>
</div>
<div class="col-xs-6">
<a href="#" id="register-form-link">Register</a>
</div>
</div>
</div>
<div class="panel-body" ng-app="myApp" ng-controller="myController">
<div class="row">
<div class="col-lg-12">
<form id="login-form" class="form-horizontal" ng-submit="submitLoginForm()">
<div class="form-group">
<input type="text" name="username" id="username" tabindex="1" class="form-control" placeholder="Username" value="" ng-model="user.username">
</div>
<div class="form-group">
<input type="password" name="password" id="password" tabindex="2" class="form-control" placeholder="Password" ng-model="user.password">
</div>
<div class="form-group text-center">
<input type="checkbox" tabindex="3" class="" name="remember" id="remember">
<label for="remember"> Remember Me</label>
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<input type="submit" name="login-submit" id="login-submit" tabindex="4" class="form-control btn btn-login" value="Log In">
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-lg-12">
<div class="text-center">
<a href="http://phpoll.com/recover" tabindex="5" class="forgot-password">Forgot Password?</a>
</div>
</div>
</div>
</div>
</form>
<form id="register-form" class="form-horizontal" style="display: none;" ng-submit="submitRegisterForm()">
<div class="form-group">
<input type="text" name="username" id="username" tabindex="1" class="form-control" placeholder="Username" value="" ng-model="user.username">
</div>
<div class="form-group">
<input type="email" name="email" id="email" tabindex="1" class="form-control" placeholder="Email Address" value="" ng-model="user.email">
</div>
<div class="form-group">
<input type="password" name="password" id="password" tabindex="2" class="form-control" placeholder="Password" ng-model="user.password">
</div>
<div class="form-group">
<input type="password" name="confirm-password" id="confirm-password" tabindex="2" class="form-control" placeholder="Confirm Password" ng-model="user.passwordConfirmation">
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<input type="submit" name="register-submit" id="register-submit" tabindex="4" class="form-control btn btn-register" value="Register Now">
</div>
</div>
</div>
</form>
</div>
</div>
</div> <!-- panel-body end -->
<script>
$('li:eq(1)').addClass('active');
</script>
</@layout.myLayout>
注: 我不太擅长前端页面的界面设计, 这里参考了 http://bootsnipp.com/snippets/jvgVX 的示例, 一个很有用的基于 bootstrap 的样式主题设计网站
当用户点击注册页面, 填写所需字段, 并提交表单时, 用 Angular JS 向后台提交, 代码如下
'use strict';
$(function() {
$('#login-form-link').click(function(e) {
$("#login-form").delay(100).fadeIn(100);
$("#register-form").fadeOut(100);
$('#register-form-link').removeClass('active');
$(this).addClass('active');
e.preventDefault();
});
$('#register-form-link').click(function(e) {
$("#register-form").delay(100).fadeIn(100);
$("#login-form").fadeOut(100);
$('#login-form-link').removeClass('active');
$(this).addClass('active');
e.preventDefault();
});
});
// Defining angularjs application.
var myApp = angular.module('myApp', []);
// Controller function and passing $http service and $scope var.
myApp.controller('myController', function($scope, $http) {
// create a blank object to handle form data.
$scope.user = {};
// calling our submit function.
$scope.submitRegisterForm = function() {
var postData = {
username:$scope.user.username,
email: $scope.user.email,
password: $scope.user.password,
passwordConfirmation: $scope.user.passwordConfirmation
};
$http({
method : 'POST',
url : '/checklist/api/v1/users/register',
data : postData,
headers : {'Content-Type': 'application/json'}
})
.success(function(data) {
if (data.errors) {
// Showing errors.
$scope.errors = data.errors;
} else {
$scope.message = data.message;
}
});
};
$scope.submitLoginrForm = function() {
var postData = {
email: $scope.user.email,
password: $scope.user.password,
};
$http({
method : 'POST',
url : '/checklist/api/v1/users/login',
data : postData,
headers : {'Content-Type': 'application/json'}
})
.success(function(data) {
if (data.errors) {
// Showing errors.
$scope.errors = data.errors;
} else {
$scope.message = data.message;
}
});
};
});
好了表现层包括前端的代码大致搞定了, 现在开始写后端的 Java web service 代码, 参见 微服务从零开始之登录与注册二