?概述
任何一個(gè)微服務(wù)需要基本的安全保證, 也是遵循AAA原則: 鑒證照皆,授權(quán)和計(jì)帳
- Authentication 要求是合法用戶
- Authorization 要求有合法權(quán)限
- Accounting 要求有記錄可追蹤
讓我們從需求分析到代碼實(shí)現(xiàn)急鳄,盡量啰嗦地來說說怎么做一個(gè)看似簡(jiǎn)單的登錄注冊(cè)模塊, 假設(shè)該服務(wù)叫做 Checklist 我的清單
需求分析
用例 Use case
除了使用繪圖工具和畫用例圖堰酿, 還有有?幾種方法通過腳本來?生成用例圖
一是使用在線網(wǎng)站 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, 再運(yùn)行如下命令
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) 獨(dú)立的
- "N" egotiable (not a specific contract for features) 可協(xié)商的
- "V" aluable (or vertical) 有價(jià)值的
- "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) 可測(cè)試的
Sign Up 注冊(cè)
- 作為一個(gè)未注冊(cè)用戶, 我想輸入我的電子郵件地址和密碼触创,注冊(cè)到 Checklist
1.1 我必須輸入合法和郵件地址坎藐,符合密碼策略的密碼以及一致的驗(yàn)證碼進(jìn)行注冊(cè)
默認(rèn)的密碼策略是最低8個(gè)字符, ?必須包含大小寫字母和至少一個(gè)數(shù)字
| # | Story | Priority | Estimation | Deadline| Comments |
|---|---|---|---|---|
| 1.1.1 | ?生成驗(yàn)證碼 |---|---|---|--- |
| 1.1.2 | 顯示注冊(cè)表單|---|---|---|--- |
| 1.1.3 | 郵件地址格式驗(yàn)證|---|---|---|--- |
| 1.1.4 | 比較兩次輸入的密碼是否相同|---|---|---|--- |
| 1.1.5 | 驗(yàn)證密碼是否符合密碼策略|---|---|---|--- |
| 1.1.6 | 驗(yàn)證輸入的驗(yàn)證碼|---|---|---|--- |
| 1.1.7 | 檢查是否已有相同的郵件地址存在|---|---|---|--- |
| 1.1.8 | 輸入驗(yàn)證無誤后存入數(shù)據(jù)庫(kù),狀態(tài)為pending|---|---|---|--- |
| 1.1.9 | 生成此用戶的激活鏈接|---|---|---|--- |
| 1.1.10 | 向注冊(cè)郵箱發(fā)送一封確認(rèn)郵件|---|---|---|--- |
1.2 我的注冊(cè)郵箱會(huì)收到一封驗(yàn)證郵件岩馍, 提示我點(diǎn)擊注冊(cè)連接碉咆, ?從而激活我的注冊(cè)帳戶
1.3 當(dāng)我完成激活后會(huì)自動(dòng)跳到 Checklist 的首頁, 提示我進(jìn)行登錄
實(shí)現(xiàn)
這次我們用Java實(shí)現(xiàn)蛀恩,選擇的框架是Spring Boot, 先從最笨最直接的方法入手疫铜, 之后再看看相關(guān)的框架 Spring Security 和 Apache Shiro 是怎么做的
Model
View
字段 | ?控件 |
---|---|
username | text |
password | password |
confirmPassword | password |
rememberMe | checkbox |
forgetPassword | link |
創(chuàng)建項(xiàng)目
- 在 http://start.spring.io 上選擇所需模塊, 創(chuàng)建 Checklist 項(xiàng)目并打包下載
或者直接用 Spring Cli 直接生成
spring init --build=maven --java-version=1.8 --dependencies=web --packaging=jar --groupId=com.github.walterfan --artifactId=checklist
在實(shí)踐中始終牢記 三個(gè)基本點(diǎn)
- 無模型不編程-MDD 模型驅(qū)動(dòng)開發(fā)
- 無測(cè)試不開發(fā)-TDD 測(cè)試驅(qū)動(dòng)開發(fā)
- 無度量不交付-MDD 度量驅(qū)動(dòng)開發(fā)
領(lǐng)域模型很簡(jiǎn)單
Register
User
Role
測(cè)試用例也簡(jiǎn)單
- 注冊(cè)
- 激活
- 登錄
度量就只記錄
- 注冊(cè)次數(shù)
- 激活次數(shù)
- 性能數(shù)據(jù)
代碼結(jié)構(gòu)
廢話不多說双谆,上代碼 checklist source codes on github
數(shù)據(jù)庫(kù)我們選用兩個(gè)
- h2 作為測(cè)試數(shù)據(jù)庫(kù)
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
- mysql 作為產(chǎn)品數(shù)據(jù)庫(kù)
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
表現(xiàn)層
表現(xiàn)層選用 Freemarker 作為后端模板壳咕, 前端選用 AngularJS + BootStrap
freemarker 是比較流行的后端頁面生成的模板引擎, 這所以不用 JSP 和 JSF, 就是為了不想在后端模板層面引入太多邏輯和不必要的復(fù)雜性, 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 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>
注: 我不太擅長(zhǎng)前端頁面的界面設(shè)計(jì), 這里參考了 http://bootsnipp.com/snippets/jvgVX 的示例, 一個(gè)很有用的基于 bootstrap 的樣式主題設(shè)計(jì)網(wǎng)站
當(dāng)用戶點(diǎn)擊注冊(cè)頁面, 填寫所需字段, 并提交表單時(shí), 用 Angular JS 向后臺(tái)提交, 代碼如下
'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;
}
});
};
});
好了表現(xiàn)層包括前端的代碼大致搞定了, 現(xiàn)在開始寫后端的 Java web service 代碼, 參見 微服務(wù)從零開始之登錄與注冊(cè)二