微服务从零开始之登录与注册一

�概述

任何一个微服务需要基本的安全保证, 也是遵循AAA原则: 鉴证,授权和计帐

  • Authentication 要求是合法用户
  • Authorization 要求有合法权限
  • Accounting 要求有记录可追踪

让我们从需求分析到代码实现,尽量啰嗦地来说说怎么做一个看似简单的登录注册模块, 假设该服务叫做 Checklist 我的清单

需求分析

用例 Use case

除了使用绘图工具和画用例图, 还有有�几种方法通过脚本来�生成用例图

一是使用在线网站 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, 再运行如下命令

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 注册

  1. 作为一个未注册用户, 我想输入我的电子邮件地址和密码,注册到 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

model

View

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

创建项目

spring boot starter

或者直接用 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

测试用例也简单

  1. 注册
  2. 激活
  3. 登录

度量就只记录

  1. 注册次数
  2. 激活次数
  3. 性能数据

代码结构

废话不多说,上代码 checklist source codes on github

code structure

数据库我们选用两个

  1. h2 作为测试数据库
               <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
  1. 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>

首页

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 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 代码, 参见 微服务从零开始之登录与注册二

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • 书接上文 微服务从零开始之登录与注册一, 表现层及前端代码搞定, 现在开始来设计和实现后端的 Java Web S...
    老瓦在霸都阅读 3,349评论 1 2
  • 这些属性是否生效取决于对应的组件是否声明为 Spring 应用程序上下文里的 Bean(基本是自动配置的),为一个...
    发光的鱼阅读 1,416评论 0 14
  • 甲骨文里,不/丕同源,是植物的根部: 后来分化成两字,不表否定,丕则保留原意,有“大”义,因为根是...
    ChanKun阅读 355评论 0 0