微服務(wù)從零開始之登錄與注冊(cè)一

?概述

任何一個(gè)微服務(wù)需要基本的安全保證, 也是遵循AAA原則: 鑒證,授權(quán)和計(jì)帳

  • Authentication 要求是合法用戶
  • Authorization 要求有合法權(quán)限
  • Accounting 要求有記錄可追蹤

讓我們從需求分析到代碼實(shí)現(xiàn),盡量啰嗦地來說說怎么做一個(gè)看似簡單的登錄注冊(cè)模塊, 假設(shè)該服務(wù)叫做 Checklist 我的清單

需求分析

用例 Use case

除了使用繪圖工具和畫用例圖, 還有有?幾種方法通過腳本來?生成用例圖

一是使用在線網(wǎng)站 yuml.me

https://yuml.me/608ca377

use case

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) 可測試的

Sign Up 注冊(cè)

  1. 作為一個(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ù)庫,狀態(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

model

View

字段 ?控件
username text
email email
password password
confirmPassword password
rememberMe checkbox
forgetPassword link

創(chuàng)建項(xiàng)目

  • http://start.spring.io 上選擇所需模塊, 創(chuàng)建 Checklist 項(xiàng)目并打包下載
spring boot starter

或者直接用 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ā)
  • 無測試不開發(fā)-TDD 測試驅(qū)動(dòng)開發(fā)
  • 無度量不交付-MDD 度量驅(qū)動(dòng)開發(fā)

領(lǐng)域模型很簡單
Register
User
Role

測試用例也簡單

  1. 注冊(cè)
  2. 激活
  3. 登錄

度量就只記錄

  1. 注冊(cè)次數(shù)
  2. 激活次數(shù)
  3. 性能數(shù)據(jù)

代碼結(jié)構(gòu)

廢話不多說,上代碼 checklist source codes on github

code structure

數(shù)據(jù)庫我們選用兩個(gè)

  1. h2 作為測試數(shù)據(jù)庫
               <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
  1. mysql 作為產(chǎn)品數(shù)據(jù)庫
                 <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>

首頁

home page

源碼 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 page

登錄頁面

register page

源碼 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>

注: 我不太擅長前端頁面的界面設(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è)二

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,568評(píng)論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,273評(píng)論 6 342
  • 書接上文 微服務(wù)從零開始之登錄與注冊(cè)一, 表現(xiàn)層及前端代碼搞定, 現(xiàn)在開始來設(shè)計(jì)和實(shí)現(xiàn)后端的 Java Web S...
    老瓦在霸都閱讀 3,469評(píng)論 1 2
  • 這些屬性是否生效取決于對(duì)應(yīng)的組件是否聲明為 Spring 應(yīng)用程序上下文里的 Bean(基本是自動(dòng)配置的),為一個(gè)...
    發(fā)光的魚閱讀 1,489評(píng)論 0 14
  • 甲骨文里,不/丕同源,是植物的根部: 后來分化成兩字,不表否定,丕則保留原意,有“大”義,因?yàn)楦?..
    ChanKun閱讀 432評(píng)論 0 0

友情鏈接更多精彩內(nèi)容