C++常见考点：三大特性static，const关键字C++和java以及python有什么不同，不同在哪里？C++类型转化是否安全？堆和栈哪个更快虚函数知道吗？析构函数为什么要用虚函数？内存的划分，请堆区内存时，所需空间不足抛出异常后怎么办，怎样才能成功申请到内存三大特性封装：把客观事物封装成抽象的类，并且类可以把自己的数据和方法只让可信的类或者对象操作，对不可信的进行信息隐藏。使得代码模块化，隐藏细节。多态：一个类实例的相同方法在不同情形有不同表现形式。继承：可以使用现有类的所有功能，并在无需重新编写原来的类的情况下对这些功能进行扩展。重写：当一个子类继承一父类，而子类中的方法与父类中的方法的名称，参数个数、类型都完全一致时，就称子类中的这个方法重写了父类中的方法。重载：一个类中的方法与另一个方法同名，但是参数表不同，这种方法称之为重载方法。Java和C++的区别：Java是解释型语言，所谓的解释型语言，就是源码会先经过一次编译，成为中间码，中间码再被解释器解释成机器码。对于Java而言，中间码就是字节码(.class)，而解释器在JVM中内置了。C++是编译型语言，所谓编译型语言，就是源码一次编译，直接在编译的过程中链接了，形成了机器码。C++比Java执行速度快，但是Java可以利用JVM跨平台。Java是纯面向对象的语言，所有代码（包括函数、变量）都必须在类中定义。而C++中还有面向过程的东西，比如是全局变量和全局函数。C++中有指针，Java中没有，但是有引用。C++支持多继承，Java中类都是单继承的。但是继承都有传递性，同时Java中的接口是多继承，类对接口的实现也是多实现。C++中，开发需要自己去管理内存，但是Java中JVM有自己的GC机制，虽然有自己的GC机制，但是也会出现OOM和内存泄漏的问题。C++中有析构函数，Java中Object的finalize方法。C++运算符可以重载，但是Java中不可以。同时C++中支持强制自动转型，Java中不行，会出现ClassCastException（类型不匹配）。static和constC++为了管理静态成员，提供了静态函数，静态函数对外提供接口。并且静态函数只能访问静态成员（原因：非静态成员函数在调用时 this指针被当作参数传进。而静态成员函数是属于类，不属于对象，没有 this 指针）。static特点1、 共享 ： static 成员变量实现了同族类对象间信息共享;2、 初始化：static 成员使用时必须初始化，且只能类外初始化。声明与实现分离时;只能初始化在实现部分(cpp 部分);3 、类大小： static 成员类外存储，求类大小，并不包含在内;4、 存储 ： static 成员是命名空间属于类的全局变量，存储在 data 区 rw 段;5、 访问 ：可以通过类名访问(无对象生成时亦可)，也可以通过对象访问。static静态成员可以被类内的普通函数静态函数都可访问。const 修饰数据成员，称为常数据成员。常数据成员可被普通成员函数和常成员函数来使用，不可以更改。const 常数据成员在使用前必须被初始化。也就是定义的同时就要初始化，之后不能再去赋值，只能使用。1、const 修饰函数放在声明之后，实现体之前。2、const 修饰函数不能修改类内的数据成员变量。3、const 修饰函数只能调用 const 函数。非 const 函数可以调用 const 函数。4、const 修饰的全局函数在定义和声明处都需要 const 修饰符。const char ptr, char const ptr和char* const ptr前两者相同，定义一个指向字符常量的指针，第三种，定义一个指向字符的指针常数，不能修改ptr指针，但是可以修改该指针指向的内容。const char *ptr==char const *ptr; 可以直接改变指针指向，但不能直接改变指针指向的值；*ptr=*ss;char *const ptr; 可以直接改变指针指向的值，但不能直接改变指针指向;ptr[0]='s';但两者都可以通过改变所指向的指针的内容，来改变它的值。include头文件时，""和<>的区别“”先在当前目录下查找头文件，若无便去第三方库文件里查找，最后去库文件查找，<>直接到库文件里查找源文件到可执行文件的过程预处理，生成.ii文件；编译，生成汇编文件.s文件；汇编，生成目标文件.o或.obj文件；链接，产生可执行文件.out或.exe文件其中预处理主要：对#define宏展开，处理条件编译指令，处理#include指令。。。C++与C的差别C只能在函数开始处声明变量，C没有bool类型；C++有引用变量，c++强制类型转换多了int（num）这种C和C++的函数在内存中存储的形式不一样，C++是存储函数名加参数，因此可以重载指针和引用指针是一个变量，只不过这个变量存储的是一个地址，指向内存的一个存储单元；而引用跟原来的变量实质上是同一个东西，只不过是原变量的一个别名而已。指针可以有多级，但是引用只能是一级。指针的值可以为空，也可能指向一个不确定的内存空间，但是引用的值不能为空，并且引用在定义的时候必须初始化为特定对象。指针的值在初始化后可以改变，即指向其它的存储单元，而引用在进行初始化后就不会再改变引用对象了。sizeof引用得到的是所指向的变量(对象)的大小，而sizeof指针得到的是指针本身的大小。Lambda表达式[]函数对象参数，()操作符重载函数参数，->返回值类型，{}函数体函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量(包括 Lambda 所在类的 this)[] (int x, int y) { return x + y; } // 隐式返回类型 [] (int& x) { ++x; } // 没有 return 语句 -> Lambda 函数的返回类型是 'void' [] () { ++global_x; } // 没有参数，仅访问某个全局变量 [] { ++global_x; } // 与上一个相同，省略了 (操作符重载函数参数) [] (int x, int y) -> int { int z = x + y; return z; } [],[x, &y],[&],[=],[&, x],[=, &z] auto my_lambda_func = [&](int x) { /* ... */ }; auto my_onheap_lambda_func = new auto([=](int x) { /* ... */ });对 this 的捕获比较特殊，它只能按值捕获而一个没有指定任何捕获的 lambda 函数,可以显式转换成一个具有相同声明形式函数指针auto a_lambda_func = [](int x) { /* ... */ }; void (*func_ptr)(int) = a_lambda_func; func_ptr(4); // calls the lambda函数指针函数的地址就是函数名，要将函数作为参数进行传递，必须传递函数名。函数指针指向的是函数而非对象，和其他指针一样，函数指针指向某种特定类型，函数的类型由它的返回类型和形参类型共同决定，与函数名无关。声明指针时，必须指定指针指向的数据类型，同样，声明指向函数的指针时，必须指定指针指向的函数类型，这意味着声明应当指定函数的返回类型以及函数的参数列表。double cal(int); // prototype double (*pf)(int); // 指针pf指向的函数， 输入参数为int,返回值为double pf = cal; // 指针赋值 void estimate(int lines, double (*pf)(int)); // 函数指针作为参数传递 可以为函数指针赋一个 nullptr或者0 的整型常量表达式，表示该指针没有指向任何一个函数构造函数以下情况编译会生成默认构造函数1.当该类的类对象数据成员有默认构造函数时。2.当该类的基类有默认构造函数时。3.当该类的基类为虚基类时。4.当该类有虚函数时。需要使用拷贝构造函数的情况：对一个 object 做明确的初始化操作：A a; A a1=a;当 object 被当做参数交给某个函数时 X xx; foo(xx);函数传回一个 class object 时，X xx;return xx;深拷贝，浅拷贝成员初始化列表：当初始化一个reference member时；当初始化一个const member时；当调用一个base class的constructor，而它拥有一组参数时；当类成员对象没有无参构造函数；必须使用（生成顺序是由类中的成员声明顺序决定的，而不是初始化列表的顺序）类型转换1.隐式类型转换标准的转换，经常在不经意间就发生了，如int类型和float类型相加时，int类型就会被隐式的转换位float类型，再进行相加运算。2.显式类型转换C++中有四个类型转换符：static_cast、dynamic_cast、const_cast和reinterpret_caststatic_cast <type-id> (expression)expression转换为type-id类型，要用于非多态类型之间的转换，不提供运行时的检查来确保转换的安全性。（1）进行上行转换（把子类的指针或引用转换成基类表示）是安全的；进行下行转换时，由于没有动态类型检查，所以是不安全的（2）用于基本数据类型之间的转换，如把int转换成char，把int转换成enum等等，这种转换的安全性需要程序员来保证（3）把void指针转换成目标类型的指针，是及其不安全的dynamic_cast <type-id> (expression)将expression转换为type-id类型，type-id必须是类的指针、类的引用或者是void *；如果type-id是指针类型，那么expression也必须是一个指针；如果type-id是一个引用，那么expression也必须是一个引用。主要用于类层次间的上行转换和下行转换，还可以用于类之间的交叉转换。（1）最简单的上行转换。比如B继承自A，B转换为A，进行上行转换时，是安全的。（2）多重继承之间的上行转换。C继承自B，B继承自A，这种多重继承的关系。（3）转换成void *。但是，在类A和类B中必须包含虚函数，因为类中存在虚函数，就说明它有想让基类指针或引用指向派生类对象的情况，此时转换才有意义。由于运行时类型检查需要运行时类型信息，而这个信息存储在类的虚函数表中，只有定义了虚函数的类才有虚函数表。如果expression是type-id的基类，使用dynamic_cast进行转换时，在运行时就会检查expression是否真正的指向一个type-id类型的对象，如果是，则能进行正确的转换，获得对应的值；否则返回NULL，如果是引用，则在运行时就会抛出异常。const_cast <type-id> (expression)用于将类型的const、volatile和__unaligned属性移除。常量指针被转换成非常量指针，并且仍然指向原来的对象；常量引用被转换成非常量引用，并且仍然引用原来的对象。！！！不能直接对非指针和非引用的变量使用const_cast操作符去直接移除const、volatile和__unaligned属性reinterpret_cast <type-id> (expression)允许将任何指针类型转换为其它的指针类型。主要用于将一种数据类型从一种类型转换为另一种类型。可以将一个指针转换成一个整数，也可以将一个整数转换成一个指针，在实际开发中，先把一个指针转换成一个整数，在把该整数转换成原类型的指针，还可以得到原来的指针值；；特别是开辟了系统全局的内存空间，需要在多个应用程序之间使用时，需要彼此共享，传递这个内存空间的指针时，就可以将指针转换成整数值，得到以后，再将整数值转换成指针，进行对应的操作虚函数编译器会为每个有虚函数的类创建一个虚函数表，虚函数表被该类的所有对象共享。类的每个虚函数占据虚函数表中的一块。如果类中有N个虚函数，那么其虚函数表将有N*4字节的大小。析构函数需要是虚函数，否则若使用父类指针指向子类对象，在delete时只会调用父类的析构函数，而不能调用子类的析构函数，从而造成内存泄露或达不到预期结果。一般来说不要把虚析构函数写成纯虚析构函数，需要时得有定义虚函数中的const：因为不确定子类会不会更改数据，所以虚函数最好不要声明成const了虚函数的作用主要是实现了多态的机制。也即是被virtual修饰的函数（1）为什么父类的析构函数必须是虚函数？ ​ 当我们动态申请一个子类对象时，使用基类指针指向该对象，如果不用虚函数，子类的析构函数不能得到调用，也就是为了在释放基类指针时可以释放掉子类的空间，防止内存泄漏. （2）为什么C++默认的析构函数不是虚函数？ ​ 因为虚函数需要额外的虚函数表和虚表指针，占用额外的内存。而对于不会被继承的类来说，其析构函数如果是虚函数，就会浪费内存。内联函数inline是C++语言中的一个关键字，可以用于程序中定义内联函数，内联函数是C++中的一种特殊函数，它可以像普通函数一样被调用，但是在调用时并不通过函数调用的机制而是通过将函数体直接插入调用处来实现的，这样可以大大减少由函数调用带来的开销，从而提高程序的运行效率。一般来说inline用于定义类的成员函数。inline适用的函数有两种，一种是在类内定义的成员函数，另一种是在类内声明，类外定义的成员函数(1)类内定义成员函数在类内定义函数时，可以不加inline关键字，编译器会自动将类内定义的函数（构造函数、析构函数、普通成员函数等）设置为内联，具有内联函数调用的性质。(2) 类内声明函数，在类外定义函数这种情况下如果想将该函数设置为内联函数，则可以在类内声明时不加inline关键字，而在类外定义函数时加上inline关键字class temp{ public: int amount; //普通成员函数，在类内声明时前面可以不加inline void print_amount() } //在类外定义函数体，必须在前面加上inline关键字 inline void temp:: print_amount(){ cout << amount << endl; }如果内联函数的函数体过大，一般的编译器会放弃内联方式，而采用普通的方式调用函数（使用内联函数，只不过是向编译器提出一个申请，编译器可以拒绝你的申请）宏是由预处理器对宏进行替代，而内联函数是通过编译器控制来实现的预定义宏函数的问题：没有作用域概念，无法作为一个类的成员函数，也就是说预定义宏没有办法表示类的范围第一个在c中也会出现，宏看起来像一个函数调用，但是会有隐藏一些难以发现的错误。和实际的函数调用不一致第二个问题是c++特有的，预处理器不允许访问类的成员，也就是说预处理器宏不能用作类的成员函数1.不能存在任何形式的循环语句2.不能存在过多的条件判断语句3.函数体不能过于庞大4.不能对函数进行取址操作段错误访问不存在的内存地址；访问系统保护的内存地址；访问只读的内存地址；栈溢出堆和栈栈是向低地址扩展的数据结构，是一块连续的内存区域。栈顶的地址和栈的最大容量是系统预先规定好的，在Window下，大小是2MB，Linux下，默认为8MB。堆是向高地址生长的，不连续，速度相对较慢，也容易内存泄漏，但获得空间大，更灵活。管理方式：对于栈来讲，是由编译器自动管理，无需我们手工控制；对于堆来说，释放工作由程序员控制，容易产生memory leak。碎片问题：对于堆来讲，频繁的new/delete势必会造成内存空间的不连续，从而造成大量的碎片，使程序效率降低。栈则不会存在这个问题，因为栈是先进后出的队列。生长方向：对于堆来讲，生长方向是向上的，也就是向着内存地址增加的方向；对于栈来讲，它的生长方向是向下的，是向着内存地址减小的方向增长分配方式：堆都是动态分配的。栈有2种分配方式：静态分配和动态分配。系统响应方面：堆：操作系统有一个记录空闲内存地址的链表，当系统收到程序的申请时，会遍历该链表，寻找第一个空间大于所申请空间的堆结点，然后将该结点从空闲结点链表中删除，并将该结点的空间分配给程序，另外，对于大多数系统，会在这块内存空间中的首地址处记录本次分配的大小，这样代码中的delete语句才能正确的 释放本内存空间。另外由于找到的堆结点的大小不一定正好等于申请的大小，系统会自动的将多余的那部分重新放入空闲链表中。栈：只要栈的剩余空间大于所申请空间，系统将为程序提供内存，否则将报异常提示栈溢出。内存分布栈：内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数堆：内存使用new进行分配使用delete或delete[]释放。如果未能对内存进行正确的释放，会造成内存泄漏。但在程序结束时，会由操作系统自动回收自由存储区：使用malloc进行分配，使用free进行回收。和堆类似全局/静态存储区：全局变量和静态变量被分配到同一块内存中常量存储区：存储常量，不允许被修改malloc开辟内存失败返回NULL，new开辟内存失败抛出bad_alloc类型的异常，需要捕获异常才能判断内存开辟成功或失败，new运算符其实是operator new函数的调用，它底层调用的也是malloc来开辟内存的，new它比malloc多的就是初始化功能，对于类类型来说，所谓初始化，就是调用相应的构造函数。疑问一：堆和自由存储区是不是同一块区域自由存储区是C++基于new操作符的一个抽象概念，凡是通过new操作符进行内存申请，该内存即为自由存储区。而堆是操作系统中的术语，是操作系统所维护的一块特殊内存，用于程序的内存动态分配，C语言使用malloc从堆上分配内存，使用free释放已分配的对应内存。自由存储区不等于堆，如上所述，布局new就可以不位于堆中。int b; //main.cpp int a = 0; //全局初始化区 char *p1; //全局未初始化区 main(){int b; //栈 char s[] = "abc"; // 栈 char *p2; //栈 char *p3 = "123456"; // 123456/0在常量区，p3在栈上。 static int c = 0； // 全局（静态）初始化区 p1 = (char *)malloc(10) p2 = (char *)malloc(20) // 分配得来得10和20字节的区域就在堆区。 strcpy(p1, "123456"); // 123456/0放在常量区，编译器可能会将它与p3所指向的"123456"优化成一个地方。 }
做道简单的题放松一下。。。题目字符串压缩。利用字符重复出现的次数，编写一种方法，实现基本的字符串压缩功能。比如，字符串aabcccccaaa会变为a2b1c5a3。若“压缩”后的字符串没有变短，则返回原先的字符串。你可以假设字符串中只包含大小写英文字母（a至z）。示例1:输入："aabcccccaaa"输出："a2b1c5a3"示例2:输入：："abbccd"输出：："abbccd"解释："abbccd"压缩后为"a1b2c2d1"，比原字符串长度更长。提示：字符串长度在[0, 50000]范围内。思路设置两个标记，和一个临时存放结果的字符串temp，分别为pre和last，pre指向当前的字母并将其附在temp尾部，last往后搜索相同的字母，直到遇到不同字符或者字符串尾部才停止，然后另last-pre得到相同连续字符的个数，并附在temp尾部，并重复前面的步骤。代码class Solution { public: string compressString(string S) { if (S.length()==0||S.length()==1) { return S; } string temp = ""; int pre = 0, last = 0; while (last<S.length()) { temp += S[pre]; while (last<S.length() && S[last]==S[pre]) { last++; } temp += to_string(last - pre); pre = last; } return temp.length() >= S.length() ? S : temp; } }; 额外：中间一开始使用了temp = temp + to_string(last - pre)，提示超出内存限制，是在字符串很大的情况下，改用temp += to_string(last - pre)后便没有了问题，了解之后，前者会另开辟空间，存储temp + to_string(last - pre)的值，再赋值给temp，而后者相当于直接append到temp字符串上。
题目描述给定一个无序的整数数组，找到其中最长上升子序列的长度。示例输入: [10,9,2,5,3,7,101,18]输出: 4 解释: 最长的上升子序列是 [2,3,7,101]，它的长度是 4。说明:可能会有多种最长上升子序列的组合，你只需要输出对应的长度即可。你算法的时间复杂度应该为 O(n2) 。思路使用动态规划来解题，定义$d(i), (i \in [1,n])$ 表示前$i$个数以$A[i]$结尾的最长上升子序列的长度，而$d(i)=max\{d(1),d(2),……,d(i)\}$于是从i=0开始，往后迭代序列，求得以i结尾的最大上升子序列，代码如下：class Solution { public: int lengthOfLIS(vector<int>& nums) { if (nums.size()==0) { return 0; } int n = nums.size(); //定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度 int dp[n] = {0}; int maxSeq = 0; for (int i = 0; i < n; i++) { maxSeq = 0; for (int j = 0; j <= i;j++) { if (nums[j] < nums[i]) { maxSeq = max(dp[j], maxSeq); } } dp[i] = maxSeq + 1; } for (int i = 0; i < n; i++) { maxSeq = maxSeq > dp[i] ? maxSeq : dp[i]; } return maxSeq; } };
代码如下 如何通过aid获取cid？https://www.bilibili.com/widget/getPageList?aid=对应aid
Shiro框架实现登录验证和授权，以及客户端持久化登录
二、大致内容实现用户登录和登录状态持久化，以及权限验证三、效果首现，通过运用shiro框架并设置好Fliter过滤器，并且设置好过滤规则，可以将所有请求都拦截，并且在配置之后，shiro框架会接管Tomcat的会话管理。因为HTTP请求是没有状态的，因此Tomcat自身会产生一个sessionID来对每个会话进行标记，而shiro框架对这一过程进行了接管，并能按自己的配置设置这个sessionID在cookie中的字段名。然后使用缓存，例如ehcache，redis等，进行这个sessionID的存储。我这里将其命名为sid。shiro框架中，请求经过过滤器后，会交给SecurityManager进行处理，而SecurityManager一般又从Realm中获取验证数据，所以我要结合我的数据接口的情况来自定义一个Realm，而它一般要实现两个功能，身份验证和权限授予，分别在doGetAuthorizationInfo()和doGetAuthenticationInfo()中进行，返回的是AuthorizationInfo对象和AuthenticationInfo对象。这两个分别表示着授权信息和验证信息。在我的代码中，对于前者，我通过setRoles()函数向其中添加了和当前用户有关的角色信息，并且通过setStringPermissions()函数向其中添加了和当前用户有关的权限信息，然后返回。而对于后者，在构建authenticationInfo的时候，使用了四个参数，分别是登录者发送的用户名和密码，还有随机生成的盐值，以及这个realm的类名。再到用户验证，那么要匹配登录者发送的信息和数据库中对应的信息，就会有个验证和匹配的地方，这个匹配的借口我在RetryLimitHashedCredentialsMatcher类中实现，它继承了HashedCredentialsMatcher这个类，这里的匹配和验证本质上是交给父类去做的，而我增加的内容，则是从缓存中取出现在登录的这个用户名，它的连续错误登录次数，若是过多，则会抛出ExcessiveAttemptsException。这些以上的内容，都会在配置文件spring-shiro.xml中用到大概的顺序级和关系大概如下：未命名文件-2然后再到我的登录入口，我经常会用到Subject对象，这个对象存有用户相关的各种信息，例如用户名，sessionID等，登录和登出操作也是以它为入口进行的。而Subject对象为什么能存储呢，通过观看源码可以发现，它从SecurityUtils中返回，而这又是从ThreadContext.getSubject()中获取的，再接着看，创建Subject的时候，是将其和ThreadContext绑定的，相当于与线程绑定了。然后演示一下首先进行登录，试试输错的情况：屏幕快照 2019-06-23 23.47.36.png然后成功登录，并在返回信息中加入用户相关的信息，如昵称，性别，头像url等：屏幕快照 2019-06-23 23.54.47.png然后可以看看cookie中的sid：屏幕快照 2019-06-23 23.48.44.png然后进行信息的请求，这个API是需要用户登录的，因此登录了的用户可以成功通过：屏幕快照 2019-06-23 23.52.57.png然后删除sid，此刻已经相当于未登录状态，即一个新的会话，然后再次请求刚刚的那个API：屏幕快照 2019-06-23 23.53.18.png出现错误了，即表明shiro框架的验证和匹配是成功的。再试试看持久登录，rememberMe的选项：屏幕快照 2019-06-24 00.17.33.png可以看到，cookie中多了rememberMe的key-value，这个就是用来存储在客户端，能够保持持久登录的字段和密文。试试重启服务器，并且删去sid：屏幕快照 2019-06-24 00.20.17.png依然是成功获取了，这表明持久化登录的功能是成功了的四、代码详情和解析我选择的是shiro框架来实现用户登录和验证的功能，正好附带了权限验证的功能，能够在后续实现用户的锁定，设置超级管理员等等，可以方便App的功能升级。而权限验证也方便了Api的开发。首先需要改变表的结构，当然，可以结合之前的表，将之前的表改为userinfo，对应的mapper，service，dao和controller也改为userinfoxxx，然后在数据库中添加如下表格：create table sys_users ( id bigint auto_increment comment '编号', username varchar(100) comment '用户名', password varchar(100) comment '密码', salt varchar(100) comment '盐值', role_id varchar(50) comment '角色列表', locked bool default false comment '是否锁定', constraint pk_sys_users primary key(id) ) charset=utf8 ENGINE=InnoDB; create unique index idx_sys_users_username on sys_users(username); create table sys_roles ( id bigint auto_increment comment '角色编号', role varchar(100) comment '角色名称', description varchar(100) comment '角色描述', pid bigint comment '父节点', available bool default false comment '是否锁定', constraint pk_sys_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create unique index idx_sys_roles_role on sys_roles(role); create table sys_permissions ( id bigint auto_increment comment '编号', permission varchar(100) comment '权限编号', description varchar(100) comment '权限描述', rid bigint comment '此权限关联角色的id', available bool default false comment '是否锁定', constraint pk_sys_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB; create unique index idx_sys_permissions_permission on sys_permissions(permission); create table sys_users_roles ( id bigint auto_increment comment '编号', user_id bigint comment '用户编号', role_id bigint comment '角色编号', constraint pk_sys_users_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table sys_roles_permissions ( id bigint auto_increment comment '编号', role_id bigint comment '角色编号', permission_id bigint comment '权限编号', constraint pk_sys_roles_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB;这是实现之后的表：屏幕快照 2019-06-23 22.32.32其中test和user是测试用表，和项目整体无关。然后要实现User，Role，Permission的mapper，dao和service，这里还是比较基础和底层的内容。然后是Service的实现类：UserServcieImpl：@Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Autowired private PasswordHelper passwordHelper; /** * 添加用户-角色关系 * * @param userId * @param roleIds */ @Transactional public void correlationRoles(Long userId, Long... roleIds) { if (roleIds == null || roleIds.length == 0) { return; } for (Long roleId: roleIds) { if (!exists(userId, roleId)) { userDao.correlationRoles(userId, roleId); } } } /** * 移除用户-角色关系 * * @param userId * @param roleIds */ @Transactional public void uncorrelationRoles(Long userId, Long... roleIds) { if (roleIds == null || roleIds.length == 0) { return; } for (Long roleId: roleIds) { if (exists(userId, roleId)) { userDao.uncorrelationRoles(userId, roleId); } } } /** * 判断当前的用户和角色是否存在 * * @param userId * @param roleId * @return */ @Transactional public boolean exists(Long userId, Long roleId) { return userDao.exists(userId, roleId); } @Transactional @Override public List<Role> findRoles(String username) { return userDao.findRoles(username); } @Transactional @Override public List<Permission> findPermissions(String username) { return userDao.findPermissions(username); } @Transactional @Override public void changePassword(Long id, String newPassword) { //User user = ((UserServiceImpl)AopContext.currentProxy()).findById(id); User user = userDao.findById(id); // 因为数据库中`locked`字段使用的类型：`tinyint(1)`， // 那么使用mybatis查询数据库会自动将数据转换成boolean类型(使用了boolean类型接收)，0：false；1或其他非零数字：true System.out.println("是否锁定：" + user.getLocked()); if (user==null){ return; } user.setPassword(newPassword); passwordHelper.encryptPassword(user); userDao.update(user); } @Transactional @Override public void create(User user) { //加密密码 passwordHelper.encryptPassword(user); userDao.create(user); } @Transactional @Override public void delete(Long id) { userDao.delete(id); } @Transactional @Override public void update(User user) { //加密密码 passwordHelper.encryptPassword(user); userDao.update(user); } @Transactional @Override public List<User> findAll() { return userDao.findAll(); } @Transactional @Override public User findByName(String username) { User user = userDao.findByName(username); if (user == null) { return null; } return user; } @Transactional @Override public User findById(Long id) { User user = userDao.findById(id); // 因为数据库中`locked`字段使用的类型：`tinyint(1)`， // 那么使用mybatis查询数据库会自动将数据转换成boolean类型(使用了boolean类型接收)，0：false；1或其他非零数字：true System.out.println("是否锁定：" + user.getLocked()); if (user == null) { return null; } return user; } @Transactional @Override public void deleteAllUserRoles(Long id) { userDao.deleteAllUserRoles(id); }RoleServiceImpl：@Service public class RoleServiceImpl implements RoleService { @Autowired private RoleDao roleDao; @Transactional @Override public void correlationPermissions(Long roleId, Long... permissionIds) { if(permissionIds == null || permissionIds.length == 0){ return; } for(Long permissionId : permissionIds){ if(!exists(roleId, permissionId)){ roleDao.correlationPermissions(roleId,permissionId); } } } @Transactional @Override public void uncorrelationPermissions(Long roleId, Long... permissionIds) { if(roleId == null || permissionIds.length == 0){ return; } for(Long permissionId : permissionIds){ if(exists(roleId, permissionId)){ roleDao.uncorrelationPermissions(roleId, permissionId); } } } @Transactional @Override public Role findById(Long id) { return roleDao.findById(id); } /** * 查询表中是否存在此数据 * @param roleId * @param permissionId * @return */ private boolean exists(Long roleId, Long permissionId) { return roleDao.exists(roleId, permissionId); } @Transactional @Override public void create(Role role) { if (role.getPid() == null){ role.setPid(0L); } roleDao.create(role); } @Transactional @Override public void delete(Long id) { //先将和角色相关的表删除 roleDao.deleteUserRole(id); //再删除角色表数据 roleDao.deleteRole(id); } @Transactional @Override public List<Role> findAll() { return roleDao.findAll(); } @Transactional @Override public List<Permission> findRolePermissionByRoleId(Long id) { return roleDao.findRolePermissionByRoleId(id); } @Transactional @Override public List<Permission> findPermissionByRoleId(Long id) { return roleDao.findPermissionByRoleId(id); } @Transactional @Override public void update(Role role) { roleDao.update(role); } @Transactional @Override public void deleteAllRolePermissions(Long id) { roleDao.deleteAllRolePermissions(id); } @Transactional @Override public void updateUserRole_Id(Role role) { roleDao.updateUserRole_Id(role); } }以及PermissionServiceImpl：@Service public class PermissionServiceImpl implements PermissionService { @Autowired private PermissionDao permissionDao; @Transactional @Override public void create(Permission permission) { permissionDao.create(permission); } @Transactional @Override public void delete(Long id) { //先将和Permission相关的表数据删除 permissionDao.deleteRolePermission(id); //再删除Permission表数据 permissionDao.deletePermission(id); } @Transactional @Override public List<Permission> findAll() { return permissionDao.findAll(); } @Transactional @Override public void update(Permission permission) { permissionDao.update(permission); } @Transactional @Override public Permission findById(Long id) { return permissionDao.findById(id); } @Transactional @Override public List<Role> findRoleByPermissionId(Long id) { return permissionDao.findRoleByPermissionId(id); } @Transactional @Override public void deleteAllPermissionsRoles(Long id) { permissionDao.deleteAllPermissionsRoles(id); } @Transactional @Override public void correlationRoles(Long permissionId, Long roleId) { permissionDao.correlationRoles(permissionId, roleId); } }然后在创建用户的时候，要将用户的用户名和密码存入数据库，密码并不明文存储，那么就要通过一个工具类来进行加密：@Component public class PasswordHelper { //实例化RandomNumberGenerator对象，用于生成一个随机数 private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); //散列算法名称 private String algorithName = "MD5"; //散列迭代次数 private int hashInterations = 2; public RandomNumberGenerator getRandomNumberGenerator() { return randomNumberGenerator; } public void setRandomNumberGenerator(RandomNumberGenerator randomNumberGenerator) { this.randomNumberGenerator = randomNumberGenerator; } public String getAlgorithName() { return algorithName; } public void setAlgorithName(String algorithName) { this.algorithName = algorithName; } public int getHashInterations() { return hashInterations; } public void setHashInterations(int hashInterations) { this.hashInterations = hashInterations; } //加密算法 public void encryptPassword(User user){ if (user.getPassword() != null){ //对user对象设置盐：salt；这个盐值是randomNumberGenerator生成的随机数，所以盐值并不需要我们指定 //spring-shiro.xml中,storedCredentialsHexEncoded=true 则需要.toHex() //spring-shiro.xml中,storedCredentialsHexEncoded=false 则需要.toBase64() user.setSalt(randomNumberGenerator.nextBytes().toHex()); //调用SimpleHash指定散列算法参数：1、算法名称；2、用户输入的密码；3、盐值（随机生成的）；4、迭代次数 //user实体的方法getCredentialsSalt获取的salt'=username+salt，而salt为一串随机序列 String newPassword = new SimpleHash( algorithName, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), hashInterations).toHex(); user.setPassword(newPassword); } } }然后就要实现shiro框架中最重要的内容了，Realm类：UserRealm：public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 权限校验 * @param principals * @return */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("权限校验--执行了doGetAuthorizationInfo..."); String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //注意这里的setRoles和setStringPermissions需要的都是一个Set<String>类型参数 Set<String> role = new HashSet<String>(); List<Role> roles = userService.findRoles(username); for (Role r : roles){ role.add(r.getRole()); } authorizationInfo.setRoles(role); Set<String> permission = new HashSet<String>(); List<Permission> permissions = userService.findPermissions(username); for (Permission p : permissions){ permission.add(p.getPermission()); } authorizationInfo.setStringPermissions(permission); return authorizationInfo; } /** * 身份校验 * @param token * @return * @throws AuthenticationException */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("身份校验--执行了goGetAuthenticationInfo..."); //token.getCredentials().toString(); //credentials这个属性，在UsernamePasswordToken中其实是个Object，查看源代码，getCredentials()方法返回的就是password //若要正确得到UsernamePasswordToken的password，可以将credentials转为char[]再String.valof()方法获得String String username = (String) token.getPrincipal(); User user = userService.findByName(username); if (user == null) { throw new UnknownAccountException(); //没有找到账号 } if (Boolean.TRUE.equals(user.getLocked())) { throw new LockedAccountException(); //账号锁定 } //交给AuthenticationRealm使用CredentialsMatcher进行密码匹配 //SimpleAccount则可以直接获取明文 //user实体的方法getCredentialsSalt获取的salt'=username+salt，而salt为一串随机序列 //在PasswordHelper中，同样是用这种方式作为盐值传给SimpleHash来加密 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user.getUsername(), //用户名 user.getPassword(), //密码 ByteSource.Util.bytes(user.getCredentialsSalt()), getName() //realm name ); return authenticationInfo; } @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } public void clearAllCachedAuthorizationInfo() { getAuthorizationCache().clear(); } public void clearAllCachedAuthenticationInfo() { getAuthenticationCache().clear(); } public void clearAllCache() { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }Authentication的目的是对用户的密码进行校验，Authorization的目的是对用户的权限进行校验，前者主要在登录的过程进行，后者则在用户请求设定了权限要求的API的实现进行。这里是负责获取数据库的信息和用户传入的信息，真正校验的类，在另一个地方。RetryLimitHashedCredentialsMatcher：public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager){ passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String) token.getPrincipal(); //return count+1 AtomicInteger retryCount = passwordRetryCache.get(username); System.out.println("prove the cache is OK: " + retryCount); if(retryCount == null){ retryCount = new AtomicInteger(0); passwordRetryCache.put(username,retryCount); } if(retryCount.incrementAndGet() > 5){ throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token,info); if(matches){ //clear retry count passwordRetryCache.remove(username); } return matches; } }这里会从缓存中调取用户登录的信息，看看是否存在多次登录的状况。然后调用父类的方法进行验证。然后，就要配置了，这方面还是很麻烦的。首先是ehcache.xml，这是为了能够让session持久化，以及能够提升效率<?xml version="1.0" encoding="UTF-8"?> <ehcache name="shirocache"> <diskStore path="java.io.tmpdir"/> <cache name="passwordRetryCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authorizationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authenticationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> </ehcache>然后是log4j.properties：#Console log4j.appender.Console=org.apache.log4j.ConsoleAppender log4j.appender.console.Target=System.out log4j.appender.Console.layout=org.apache.log4j.PatternLayout log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n log4j.logger.org.apache=ERROR log4j.logger.org.mybatis=ERROR log4j.logger.org.springframework=ERROR # Default Shiro logging log4j.logger.org.apache.shiro=ERROR #这个须要 log4j.logger.log4jdbc.debug=ERROR log4j.logger.com.gk.mapper=ERROR log4j.logger.jdbc.audit=ERROR log4j.logger.jdbc.resultset=ERROR #这个打印SQL语句非常重要 log4j.logger.jdbc.sqlonly=DEBUG log4j.logger.jdbc.sqltiming=ERROR log4j.logger.jdbc.connection=FATAL最后是最重要的spring-shiro.xml：<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:other/ehcache.xml"/> </bean> <bean id="credentialsMatcher" class="com.mySSM.credentials.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager"/> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> <property name="storedCredentialsHexEncoded" value="true"/> </bean> <bean id="userRealm" class="com.mySSM.realm.UserRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"/> <property name="cachingEnabled" value="true"/> <property name="authenticationCachingEnabled" value="true"/> <property name="authenticationCacheName" value="authenticationCache"/> <property name="authorizationCachingEnabled" value="true"/> <property name="authorizationCacheName" value="authorizationCache"/> </bean> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="sid"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="-1"/> </bean> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="2592000"/> </bean> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/> <property name="cookie" ref="rememberMeCookie"/> </bean> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/> <property name="sessionIdGenerator" ref="sessionIdGenerator"/> </bean> <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler"> <property name="sessionValidationInterval" value="1800000"/> <property name="sessionManager" ref="sessionManager"/> </bean> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="globalSessionTimeout" value="1800000"/> <property name="deleteInvalidSessions" value="true"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> <property name="sessionDAO" ref="sessionDAO"/> <property name="sessionIdCookieEnabled" value="true"/> <property name="sessionIdCookie" ref="sessionIdCookie"/> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm"/> <property name="sessionManager" ref="sessionManager"/> <property name="cacheManager" ref="cacheManager"/> <property name="rememberMeManager" ref="rememberMeManager"/> </bean> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/> <property name="arguments" ref="securityManager"/> </bean> <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"> <property name="usernameParam" value="username"/> <property name="passwordParam" value="password"/> <property name="loginUrl" value="/login"/> <property name="rememberMeParam" value="rememberMe"/> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <!--<property name="successUrl" value="/index.do"/>--> <property name="unauthorizedUrl" value="/unauthorized"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /static/** = anon /lib/** = anon /js/** = anon /resources/** = anon /login.jsp = anon /login.do = anon /login = anon /userinfo/** = anon /testsid = anon /testsid.jsp = anon /testsid.do = anon /initial = anon /logout = logout /index.jsp = user /authenticated.jsp = authc /** = user </value> </property> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> </beans>然后在spring-web.xml中添加如下内容： <aop:config proxy-target-class="true"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>最后在web.xml中添加过滤器： <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 然后完成对应的controller的编写即可，例如登录：@Controller public class LoginController { /** * 用户登录的入口 * * @param username * @param password * @param model * @return */ @RequestMapping("/login") public String login( @RequestParam(value = "username", required = false) String username, @RequestParam(value = "password", required = false) String password, @RequestParam(value = "remember", required = false) String remember, Model model) { System.out.println("登陆用户输入的用户名：" + username + "，密码：" + password); String error = null; if (username != null && password != null) { //初始化 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); if (remember != null){ if (remember.equals("on")) { //说明选择了记住我 token.setRememberMe(true); } else { token.setRememberMe(false); } }else{ token.setRememberMe(false); } try { //登录，即身份校验，由通过Spring注入的UserRealm会自动校验输入的用户名和密码在数据库中是否有对应的值 subject.login(token); System.out.println("用户是否登录：" + subject.isAuthenticated()); return "redirect:index.do"; } catch (UnknownAccountException e) { e.printStackTrace(); error = "用户账户不存在，错误信息：" + e.getMessage(); } catch (IncorrectCredentialsException e) { e.printStackTrace(); error = "用户名或密码错误，错误信息：" + e.getMessage(); } catch (LockedAccountException e) { e.printStackTrace(); error = "该账号已锁定，错误信息：" + e.getMessage(); } catch (DisabledAccountException e) { e.printStackTrace(); error = "该账号已禁用，错误信息：" + e.getMessage(); } catch (ExcessiveAttemptsException e) { e.printStackTrace(); error = "该账号登录失败次数过多，错误信息：" + e.getMessage(); } catch (Exception e){ e.printStackTrace(); error = "未知错误，错误信息：" + e.getMessage(); } } else { error = "请输入用户名和密码"; } //登录失败，跳转到login页面 model.addAttribute("error", error); return "login"; } // /** // * 退出登录，我们不需要实现，Shiro的Filter过滤器会帮我们生成一个logout请求， // * 当浏览器访问`/logout`请求时，Shiro会自动清空缓存并重定向到配置好的loginUrl页面 // * // * @return // */ // @RequestMapping("/logout") // public String logout() { // Subject subject = SecurityUtils.getSubject(); // subject.logout(); // return "index"; // } /** * 登录成功，跳转到首页 * * @return */ @RequestMapping("/index") public String index() { return "index"; }四、遇到的问题1.首先在配置完，启动的时候，报错了，报错信息是Error creating bean with name 'shiroFilter': Requested bean is currently in creation: Is there an unresolvable circular reference?，然后差了一下，发现各种可能性都有，最后发现，我之前为了实现在service中能够调用自身的方法，<aop:aspectj-autoproxy expose-proxy="true"/> ，而在配置shiro的时候，又用了 <aop:config proxy-target-class="true"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>因此混用产生了冲突，去掉之前那个就好，然后改了一下service。参考：Spring AOP 不同配置方式产生的冲突问题2.接着是sessionID的问题，我用的服务器是Tomcat，会自动产生一个sessionID并存入cookie中， 然而shiro配置了之后也会产生一个sessionID，我定义的名字是sid，而tomcat产生的叫JSESSIONID，然后我在controller中打印HttpSession和Subject.GetSession的id，发现是一样的，意味着JSESSIONID应该是没有用的，也即shiro接管了sessionid的产生等一系列过程。那么要如何去除tomcat自己产生的sessionid呢，在jsp文件中加入session="false"即可，运行了一次之后，发现往后所有连接的cookie中都不带有JSESSIONID了。五、感想这次因为量有点大，并且bug出的多。不过收获还是很足的，用户登录功能可以说是基础，后续只要在这上面添砖加瓦就可以了。
一、目录实现用户文件和图片上传二、实验结果和学习内容首先，文件上传的功能主要作用是为了实现用户能够上传相关图片和自定义的头像。而为了方便后续功能的实现，接收文件的时候就默认全部按照多文件上传的方式处理，虽然可能效率会比单文件处理要低一些。首先，在SpringMVC中实现文件上传的功能需要用到几个包，添加进pom.xml： <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.9</version> </dependency>然后需要进行相应的配置，在spring-web.xml文件中： <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="utf-8"/> <property name="maxInMemorySize" value="10240"/> <property name="maxUploadSize" value="10485760"/> <property name="uploadTempDir" value="/uploadTemp"></property> </bean>然后就可以开始开发图片上传功能了。不过我在这之前，做了点别的事情，因为询问过字节跳动的老师，他们存储图片的时候是以文件的形式存储在服务器上的，而不是base64或是其他编码方式存在数据库中。那么为了方便，需要创建一个新的目录，而为了方便管理以及后续的迁移，使得图片目录和项目目录分开，我创建了一个虚拟目录。屏幕快照 2019-06-07 03.39.01然后就能直接通过localhost:8080/mySSMphoto/xxx.jpg来直接获取到图片了。然后开始实现文件上传的代码首先是一个工具类FileUploadUtil.javapublic class FileUploadUtil { // 使用日志工厂获取日志对象 private static Log log = LogFactory.getLog(FileUploadUtil.class); private static String imgPath = "/Users/wangjing/Desktop/mySSMphoto"; /** * 批量上传文件 * * @param request * 用户名; 用于区分用户上传的图片 * 模块名称; 用于区分该图片是位于那个模块进行上传 * @return * @throws FileNotFoundException */ public static List<String> uploadFile(HttpServletRequest request) throws FileNotFoundException { // 创建list集合，用于接收上传文件的路径 List<String> filePathList = new ArrayList<String>(); // 拼接文件上传位置，这里使用Tomcat服务器，将文件上传到webapps中，和项目同目录，files将用于保存上传的文件，将上传的文件于项目分开 //String strPath = ",webapps,files," + moduleName + "," + username; // 解析出文件存放路径位置 //String filepath = System.getProperty("catalina.base") + strPath.replace(',', File.separatorChar); String filepath = imgPath; log.debug("文件上传路劲位置-------->>>>>>>>>>>>" + filepath); // 转换request，解析出request中的文件 MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; // 获取文件map集合 Map<String, MultipartFile> fileMap = multipartRequest.getFileMap(); String fileName = null; // 循环遍历，取出单个文件 for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) { // 获取单个文件 MultipartFile mf = entity.getValue(); // 获得原始文件名 fileName = mf.getOriginalFilename(); // 截取文件类型; 这里可以根据文件类型进行判断 String fileType = fileName.substring(fileName.lastIndexOf('.')); try { // 截取上传的文件名称 String newFileName = fileName.substring(0, fileName.lastIndexOf('.')); log.debug("上传来的文件名称------->>>>>>>>>" + newFileName); // 拼接上传文件位置 String newfilePath = filepath + File.separatorChar + newFileName + fileType; log.debug("拼接好的文件路径地址------------->>>>>>>>" + newfilePath); // 重新组装文件路径，用于保存在list集合中 String filepathUrl = "mySSMphoto" + File.separatorChar + newFileName + fileType; log.debug("文件位置---------------->>>>>>>>>>" + filepathUrl); // 创建文件存放路径实例 File dest = new File(filepath); // 判断文件夹不存在就创建 if (!dest.exists()) { dest.mkdirs(); } // 创建文件实例 File uploadFile = new File(newfilePath); // 判断文件已经存在，则删除该文件 if (uploadFile.exists()) { uploadFile.delete(); } log.debug("start upload file-------------->>>>>>> " + fileName); // 利于spring中的FileCopyUtils.copy()将文件复制 // FileCopyUtils.copy(mf.getBytes(), uploadFile); mf.transferTo(uploadFile); // 将文件路径存入list集合中 filePathList.add(filepathUrl); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); log.error("upload failed. filename: " + fileName+"---->>>error message ----->>>>> "+ e.getMessage()); return null; } } return filePathList; } }然后在Controller中实现对应的Handler： /** * 批量文件上传 * * @param request * @param session * @return */ @RequestMapping(value = "/secure/upload-file", method = RequestMethod.POST, produces = "application/json;charset=UTF-8") @ResponseBody public List<String> uploadFiles(Model model,HttpServletRequest request, HttpSession session) { // 获取session中保存的用户信息 //User user = (User) session.getAttribute("user"); // 创建list集合用于获取文件上传返回路径名 List<String> list = new ArrayList<String>(); try { // 获取上传完文件返回的路径,判断module模块名称是否为空，如果为空则给default作为文件夹名 list = FileUploadUtil.uploadFile(request); // model属性也行 model.addAttribute("fileUrlList", list); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); logger.error("上传文件发生错误=》》" + e.getMessage()); } // 转发到uploadTest.jsp页面 return list; }这里能够直接返回List是用到了jackson包。而我测试的时候是通过jsp测试的，在一个web网页上进行文件上传，并将图片位置放在model中返回给用户，然后通过el表达式可以直接附到img的src中，就能显示出图片了。效果如下：屏幕快照 2019-06-07 03.47.10然后选择文件：屏幕快照 2019-06-07 03.47.51然后提交：屏幕快照 2019-06-07 03.49.16就能显示图片了，然后看看虚拟目录下的情况：屏幕快照 2019-06-07 03.50.02就成功了三、实验思考及感想底层功能基本都实现了，数据也基本完善了，可能还要用到redis或者ehcache，还有kafka等，算是额外功能吧，目的当然是为了体验更加舒适，使得获取数据更加快速和敏捷。
一、目录用户表的设计重定向和转发shiro框架初探二、实验结果和学习内容用户表的设计一开始，用于测试的时候，用户表就是简单的username加password两个字段，而为了后续的功能实现，我需要增加一个userID，并且尽量使得不能存在溢出的可能（当作亿级用户量来处理）。并且如果应用支持第三方登录，或者只使用第三方登录，那么第三方的 User ID 有可能是 64 位的。虽然自己的应用不一定能增长到超过 32 位，但是第三方很可能是 64 位的。然后还要增加用户状态 status 判断用户是否被封禁，注册时间和注册IP地址、上次登录时间和IP地址备查（并衍生出登录记录表，用来判断是否异地登录等，在此不表），用户角色/权限 role （又衍生出用户角色权限关系），业务也需要个人的个人信息如真实姓名、地址等也要往上添加，现在形成了一个很完整的用户关系表。在这之后，微博开放了第三方网站登录，用微博帐号就能登录。那就要增加个微博用户登录表，并且和本来的用户表关联，则这个微博用户信息表如下：id 自增ID user_id 关联本站用户ID uid 微博唯一ID access_token access_expire不仅如此，算上QQ和微信，一下子要接入好多家第三方登录了，只能就着“微博用户信息表”继续加类型加判断，如果是每个第三方登录都新建一个表，肯定会很冗杂。而作为iOS应用，必须要支持手机号登录。所以表结构如下：用户表 id username email phone … 用户第三方登录表 id user_id app_type app_user_id access_token进行改进无论username+password，还是phone+password，都是一种用户信息+密码的验证形式；再来理解第三方登录，其实它也是用户信息+密码的形式，用户信息即第三方系统中的ID（第三方登录一定会给一个在他们系统中的唯一标识），密码即access_token，只不过是一种有使用时效定期修改的密码。所以我们把它抽象出了用户基础信息表加上用户授权信息表的形式。用户基础信息表 users id nickname avatar 用户授权信息表 user_auths id user_id identity_type 登录类型（手机号 邮箱 用户名）或第三方应用名称（微信 微博等） identifier 标识（手机号 邮箱 用户名或第三方应用的唯一标识） credential 密码凭证（站内的保存密码，站外的不保存或保存token）这个系统最大的特色就是，用户信息表不保存任何密码，不保存任何登录信息（如用户名、手机号、邮箱），只留有昵称、头像等基础信息。所有和授权相关（且基本前端展示无关的），都放在用户信息授权表，用户信息表和用户授权表是一对多的关系。例如：usersidnicknameavatar1慕容雪村http://…/avatar.jpg2魔力鸟http://…/avatar2.jpg3科比http://…/avatar3.jpguser_authsiduser_ididentity_typeidentifiercredential11email123@example.compassword_hash(密码)21phone13888888888password_hash(密码)31weibo微博UID微博access_token42usernamemoliniaopassword_hash(密码)53weixin微信UserName微信token具体处理如下，当用户发来邮箱/用户名/手机号和密码请求登录的时候，依然是先判断类型，以某用户使用了手机号登录为例，使用 SELECT * FROM user_auths WHERE type=’phone’ and identifier=’手机号’ 查找条目，如有，取出并判断password_hash(密码)是否和该条目的credential相符，相符则通过验证，随后通过user_id获取用户信息。如果使用第三方登录，则只要判断 SELECT * FROM user_auths WHERE type=’weixin’ and identifier=’微信UserName’ ，如果有记录，则直接登录成功，使用新的token更新原token。假设与微信服务器通信不被劫持的情况下无需判断凭证问题。--重定向和转发然后由于功能实现需要用到转发或者重定向的功能，因此了解了一下：1.使用HttpServletResponse 重定向到另一个视图 @RequestMapping("/resp") public void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception { resp.sendRedirect("index.jsp"); }2.使用HttpServletRequest 转发（默认访问/下的index.jsp页面 不受渲染器的影响） @RequestMapping("/resp") public void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception { req.setAttribute("message","it's forword "); req.getRequestDispatcher("index.jsp").forward(req,resp); }3.直接返回jsp页面的名称（无渲染器） @RequestMapping("/nice") public String hello1(){ //转发方式1 return "home.jsp"; //转发方式2 return "forward:index.jsp"; //重定向方式 return "redirect:index.jsp"; } 4.当有渲染器指定的时候@RequestMapping("/nice") public String hello1(){ //转发方式1 return "home"; //转发方式2 return "forward:index"; //重定向方式 hello指的是requsrmapping return "redirect:hello"; }5.使用ModelAndView需要视图解析器 能指定跳转页面public class HelloController implements Controller { @Override public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception { ModelAndView mv = new ModelAndView(); //封装要显示到视图的数据 mv.addObject("msg","hello myfirst mvc"); //视图名 mv.setViewName("hello"); return mv; } }需要进行配置： <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" /> <bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <bean name="/hello.do" class="com.jsu.mvc.HelloController"></bean> 6.使用ModelView //通过modelmap方式 @RequestMapping("/modelmap") public String modelHello(String name,ModelMap map){ map.addAttribute("name",name); System.out.println(name); return "index.jsp"; }并且重定向和转发的具体效果都是跳转到别的页面，但实际上两者的区别不尽相同。sendRedirect()是在用户的浏览器端工作，同时它可以重定向至不同的主机上，sendRedirect()可以重定向有frame的jsp文件。假设转发代码包含于注册的servlet－url为/ggg/tt；jsp为/ggg/tt.jsp。绝对路径：response.sendRedirect("http://www.brainysoftware.com")发送至http://www.brainysoftware.com根路径：response.sendRedirect("/ooo")发送至http://localhost:8080/ooo相对路径：response.sendRedirect("ooo")发送至http://localhost:8080/Test/ggg/ooo。forward()无法重定向至有frame的jsp文件,可以重定向至有frame的html文件,只有在客户端没有输出时才可以调用forward方法。如果当前页面的缓冲区（buffer）不是空的，那么你在调用forward方法前必须先清空缓冲区。"/"代表相对与web应用路径RequestDispatcher rd = request.getRequestDispatcher("/ooo"); rd.forward(request, response); //提交至http://localhost:8080/Test/ooo RequestDispatcher rd = getServletContext().getRequestDispatcher("/ooo"); rd.forward(request, response); //提交至http://localhost:8080/Test/ooo RequestDispatcher rd =getServletContext().getNamedDispatcher("TestServlet"); //（TestServlet为一个<servlet-name>） rd.forward(request, response); //提交至名为TestServlet的servlet而转发和重定向也叫直接请求转发(Forward)和间接请求转发(Redirect)直接转发方式（Forward），客户端和浏览器只发出一次请求，Servlet、HTML、JSP或其它信息资源，由第二个信息资源响应该请求，在请求对象request中，保存的对象对于每个信息资源是共享的。间接转发方式（Redirect）实际是两次HTTP请求，服务器端在响应第一次请求的时候，让浏览器再向另外一个URL发出请求，从而达到转发的目的。shiro框架初探基本上涉及到用户参与的系统都要进行权限管理，权限管理属于系统安全的范畴，权限管理实现对用户访问系统的控制，按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。权限管理包括用户身份认证和授权两部分，简称认证授权。对于需要访问控制的资源用户首先经过身份认证，认证通过后用户具有该资源的访问权限方可访问。身份认证，就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令，看其是否与系统中存储的该用户的用户名和口令一致，来判断用户身份是否正确。对于采用指纹等系统，则出示指纹；对于硬件Key等刷卡系统，则需要刷卡。关键对象：Subject：主体访问系统的用户，主体可以是用户、程序等，进行认证的都称为主体；Principal：身份信息是主体（subject）进行身份认证的标识，标识必须具有唯一性，如用户名、手机号、邮箱地址等，一个主体可以有多个身份，但是必须有一个主身份（Primary Principal）。credential：凭证信息是只有主体自己知道的安全信息，如密码、证书等。授权，即访问控制，控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源，对于某些资源没有权限是无法访问的。shiro能做什么？认证：验证用户的身份授权：对用户执行访问控制：判断用户是否被允许做某事会话管理：在任何环境下使用 Session API，即使没有 Web 或EJB 容器。加密：以更简洁易用的方式使用加密功能，保护或隐藏数据防止被偷窥Realms：聚集一个或多个用户安全数据的数据源单点登录（SSO）功能。为没有关联到登录的用户启用 "Remember Me“ 服务Shiro 的四大核心部分Authentication(身份验证)：简称为“登录”，即证明用户是谁。Authorization(授权)：访问控制的过程，即决定是否有权限去访问受保护的资源。Session Management(会话管理)：管理用户特定的会话，即使在非 Web 或 EJB 应用程序。Cryptography(加密)：通过使用加密算法保持数据安全shiro的三个核心组件： Subject ：正与系统进行交互的人，或某一个第三方服务。所有 Subject 实例都被绑定到（且这是必须的）一个SecurityManager 上。SecurityManager：Shiro 架构的心脏，用来协调内部各安全组件，管理内部组件实例，并通过它来提供安全管理的各种服务。当 Shiro 与一个 Subject 进行交互时，实质上是幕后的 SecurityManager 处理所有繁重的 Subject 安全操作。Realms ：本质上是一个特定安全的 DAO。当配置 Shiro 时，必须指定至少一个 Realm 用来进行身份验证和/或授权。Shiro 提供了多种可用的 Realms 来获取安全相关的数据。如关系数据库(JDBC)，INI 及属性文件等。可以定义自己 Realm 实现来代表自定义的数据源。三、实验思考及感想系统正在逐步完善，当然问题也出现了，就是了解的东西实在有限，需要查阅很多资料，而且需要不停的踩坑，因为网上很多的资料都是存在问题的，或者说有很多虎头蛇尾的内容，例如在查阅controller入参和返回类型的时候，发现存在返回Map类型，有点意料之外的感觉。并且配置方面因为不是特别深入地了解，只能慢慢摸索，发现想要测试的时候，无法获取js文件，即除了jsp外的静态资源，就需要进行相应的配置，并进行测验。后续会完成shiro框架的加入，以及尝试完成用户头像的上传和收藏列表的实现
一、目录实现通过POST请求进行账号注册通过POST发送账号返回密码了解各种注解作用和POST详情二、实验结果和学习内容首先因为要解析JSON数据以及返回JSON数据，因此我在选择依赖包的时候，在JSON-lib，Jackson和fastjson中选择了Jackson，那么添加依赖，这时候发现之前用的阿里云的maven仓库好像没有最新的版本，于是在下面的网站中查找，换了一个库。AliRepo添加依赖： <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency> 然后因为我要实现注册用户，以及返回用户密码，那么就要在Dao层和Service层进行修改，然后发现Dao层的两个方法分别是通过用户获取整个用户对象，以及直接向数据库添加用户。然后在Service层增加函数 public String findUser(String username) { User user = userDao.findByUsername(username); if(user != null){ return user.getPassword(); } return "Not Found!"; } 这样就能返回用户密码了。然后在Controller中实现我所要实现的功能。首先是用户注册，这里我返回的一组测试数据。 @ResponseBody @RequestMapping(value = "/trypost2", method = RequestMethod.POST) public List<User> trypost2(@RequestBody User user){ System.out.println(user.getId()+" "+user.getPassword()+" "+user.getPassword()); List<User> list = new ArrayList<User>(); User user1 = new User(); user1.setUsername("00001111"); user1.setPassword("tttttttt"); User user2 = new User(); user2.setUsername("22221111"); user2.setPassword("pppppppp"); list.add(user1); list.add(user2); userServivce.Regist(user); return list; }其中注解@ResponseBody的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后，写入到response对象的body区，通常用来返回JSON数据或者是XML数据，而需要注意的是，在使用此注解之后不会再走试图处理器，而是直接将数据写入到输入流中，他的效果等同于通过response对象输出指定格式的数据。 @RequestMapping("/login") @ResponseBody public User login(User user){ return user; } //User字段：userName pwd //那么在前台接收到的数据为：'{"userName":"xxx","pwd":"xxx"}' //效果等同于如下代码： @RequestMapping("/login") public void login(User user, HttpServletResponse response){ response.getWriter.write(JSONObject.fromObject(user).toString()); } 然后可以进行测试了，因为直接使用curl比较麻烦，因此我下载了一个Postman，用于测试API。效果如下：这是请求：然后得到的结果：说明返回成功了，然后看看服务器的输出：可以看的出成果连接数据库并成功插入了数据，再看看数据库的表的变化：pic7.png产生了变化了说明这个API是成功了的然后是第二个API： @ResponseBody @RequestMapping(value = "/trypost", method = RequestMethod.POST) public List<String> trypost(@RequestParam(value = "username") String username){ List<String> list = new ArrayList<String>(); System.out.println("login: "+username); String pas = userServivce.findUser(username); list.add(pas); return list; } 这里的@RequestParam用于将请求参数区数据映射到功能处理方法的参数上，常用于表格。这里我一开始也像之前一样，通过发送JSON数据来请求，但发现报错了，然后就了解到了POST四种常用数据的区别：application/x-www-form-urlencoded这应该是最常见的 POST 提交数据的方式了。浏览器的原生 form 表单，如果不设置 enctype 属性，那么最终就会以 application/x-www-form-urlencoded 方式提交数据。multipart/form-data这又是一个常见的 POST 数据提交的方式。我们使用表单上传文件时，必须让 form 的 enctyped 等于这个值。这种方式一般用来上传文件，各大服务端语言对它也有着良好的支持。 上面提到的这两种 POST 数据的方式，都是浏览器原生支持的，而且现阶段原生 form 表单也只支持这两种方式。但是随着越来越多的 Web 站点，尤其是 WebApp，全部使用 Ajax 进行数据交互之后，我们完全可以定义新的数据提交方式，给开发带来更多便利。application/jsonapplication/json 这个 Content-Type 作为响应头我们都不陌生。实际上，现在越来越多的人把它作为请求头，用来告诉服务端消息主体是序列化后的 JSON 字符串。由于 JSON 规范的流行，除了低版本 IE 之外的各大浏览器都原生支持 JSON.stringify，服务端语言也都有处理 JSON 的函数，使用 JSON 不会遇上什么麻烦。JSON 格式支持比键值对复杂得多的结构化数据，这一点也很有用。这种方案，可以方便的提交复杂的结构化数据，特别适合 RESTful 的接口。各大抓包工具如 Chrome 自带的开发者工具、Firebug、Fiddler，都会以树形结构展示 JSON 数据，非常友好。text/xmlXML-RPC（XML Remote Procedure Call）。它是一种使用 HTTP 作为传输协议，XML 作为编码方式的远程调用规范。典型的 XML-RPC 请求是这样的：POST http://www.example.com HTTP/1.1 Content-Type: text/xml <methodcall> <methodname>examples.getStateName</methodname> <params> <value><i4>41</i4></value> </params> </methodcall> XML-RPC 协议简单、功能够用，各种语言的实现都有。它的使用也很广泛，如 WordPress 的 XML-RPC Api，搜索引擎的 ping 服务等等。JavaScript 中，也有现成的库支持以这种方式进行数据交互，能很好的支持已有的 XML-RPC 服务。然后更改了requestbody的数据类型之后，就成功了：请求存在的用户：结果：不存在的用户：而且可以看到，请求的头部是有很多数据的：三、实验思考及感想本次试验主要是为了App的登录和其他功能做准备，下一步应该是多用户登录的实现，具体方法还不清楚，但简单的雏形已经完成，包括新闻数据，后期还要加上Kafka消息队列来实现下拉更新的功能，我觉得需要学习的内容还有很多。
一、目录通过SSM框架实现用户登录注册的功能部署到tomcat服务器上二、实验结果和学习内容首先前期准备当然是下载并安装好MySQL，Tomcat8，还有Maven了首先配置好maven之后，通过maven来构建项目：然后构建好项目目录：首先是依赖文件pom.xml，即包含了要用到的包，会下载到maven的本地仓库中：<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>sysu.wangjing</groupId> <artifactId>mySSM</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>mySSM Maven Webapp</name> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.30</version> <scope>runtime</scope> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.8</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.1.7.RELEASE</version> </dependency> </dependencies> <build> <finalName>mySSM</finalName> <pluginManagement> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> </plugins> </pluginManagement> </build> </project> 然后是UserController.java的实现：package com.mySSM.controller; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.SessionAttributes; import com.mySSM.entity.User; import com.mySSM.service.UserService; @Controller @RequestMapping("/user") //这里用了@SessionAttributes，可以直接把model中的user(也就key)放入其中 //这样保证了session中存在user这个对象 @SessionAttributes("user") public class UserController { @Autowired private UserService userServivce; //正常访问login页面 @RequestMapping("/login") public String login(){ return "login"; } //表单提交过来的路径 @RequestMapping("/checkLogin") public String checkLogin(User user,Model model){ //调用service方法 user = userServivce.checkLogin(user.getUsername(), user.getPassword()); //若有user则添加到model里并且跳转到成功页面 if(user != null){ model.addAttribute("user",user); return "success"; } return "fail"; } //测试超链接跳转到另一个页面是否可以取到session值 @RequestMapping("/anotherpage") public String hrefpage(){ return "anotherpage"; } //注销方法 @RequestMapping("/outLogin") public String outLogin(HttpSession session){ //通过session.invalidata()方法来注销当前的session session.invalidate(); return "login"; } @RequestMapping("/regist") public String regist(){ return "regist"; } @RequestMapping("/doRegist") public String doRegist(User user,Model model){ System.out.println(user.getUsername()); userServivce.Regist(user); return "success"; } }然后是UserDao的实现：package com.mySSM.dao; import org.apache.ibatis.annotations.Param; import com.mySSM.entity.User; public interface UserDao { /** * 查找用户名和密码 * @param username 登录用户名 * @return */ User findByUsername(String username); /** * 注册用户和密码 */ void registerByUsernameAndPassword(@Param("username")String username, @Param("password")String password); }然后是实体User.java的实现：package com.mySSM.entity; public class User { private String id; private String username; private String password; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }接着是service层接口的实现UserService.java：package com.mySSM.service; import com.mySSM.entity.User; //Service层接口 public interface UserService { //检验用户登录 User checkLogin(String username,String password); void Regist(User user); }及其实现UserServiceImpl.java：package com.mySSM.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.mySSM.dao.UserDao; import com.mySSM.entity.User; import com.mySSM.service.UserService; @Service public class UserServiceImpl implements UserService{ @Autowired private UserDao userDao; /* * 检验用户登录业务 * */ public User checkLogin(String username, String password) { User user = userDao.findByUsername(username); if(user != null && user.getPassword().equals(password)){ return user; } return null; } @Override public void Regist(User user) { userDao.registerByUsernameAndPassword(user.getUsername(),user.getPassword()); } }然后在resources文件夹中，分别实现spring-dao.xml，spring-service.xml和spring-web.xml，三个和spring框架相关的配置文件。首先是spring-dao.xml：<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driverClass}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maxPoolSize" value="30"/> <property name="minPoolSize" value="10"/> <property name="autoCommitOnClose" value="false"/> <property name="checkoutTimeout" value="1000"/> <property name="acquireRetryAttempts" value="2"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!-- 扫描entity包 使用别名 com.sy.entity.User ->User --> <property name="typeAliasesPackage" value="com.mySSM.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <property name="basePackage" value="com.mySSM.dao"/> </bean> </beans> 然后是spring-service.xml：<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.mySSM.service"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>最后是spring-web.xml：<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd "> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <context:component-scan base-package="com.mySSM.controller" /> </beans>然后还有几个简单的jsp文件，分别是注册和登录页面。其中登录界面如下：<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> Insert title here 用户名: 密码: 然后要手动连接数据库：然后是配置本地的Tomcat，并且为了方便开发，部署模式选择war exploded：最后Build Project之后就成功部署了，然后可以看到界面成功展示了：三、实验思考及感想这次试验的目的在于先架起一个后端的框架，然后去慢慢地完善它。而这次的内容虽然不多，但很多地方都还是第一次上手，完全不熟悉，网上看到的内容也大多不尽相同，实在是有点摸不着头脑，特别是spring框架的配置文件，不同的人有不同的写法，可以说是五花八门了。然后实际上，功能方面的代码其实还不算多，倒是配置框架所花费的精力要更多，也不知道是好是坏，对于小项目来说，可能会有点杀鸡用牛刀的感觉，也许随着我们项目开发的逐步深入，框架的好处会体现出来吧。
一、目录kafka详解用户登录方案二、实验结果和学习内容Kafka 详解Kafka是一种分布式的，基于发布/订阅的消息系统。主要设计目标如下：以时间复杂度为O(1)的方式提供消息持久化能力，即使对TB级以上数据也能保证常数时间的访问性能高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输支持Kafka Server间的消息分区，及分布式消费，同时保证每个partition内的消息顺序传输同时支持离线数据处理和实时数据处理而我们的项目目标是完成一款Feed流App，显然消息队列系统是非常重要的。送达保证 消息队列提供的冗余机制保证了消息能被实际的处理，只要一个进程读取了该队列即可。在此基础上，部分消息系统提供了一个”只送达一次”保证。无论有多少进程在从队列中领取数据，每一个消息只能被处理一次。这之所以成为可能，是因为获取一个消息只是”预定”了这个消息，暂时把它移出了队列。除非客户端明确的表示已经处理完了这个消息，否则这个消息会被放回队列中去，在一段可配置的时间之后可再次被处理。顺序保证 在大多使用场景下，数据处理的顺序都很重要。消息队列本来就是排序的，并且能保证数据会按照特定的顺序来处理。部分消息系统保证消息通过FIFO（先进先出）的顺序来处理，因此消息在队列中的位置就是从队列中检索他们的位置。缓冲 在任何重要的系统中，都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行–写入队列的处理会尽可能的快速，而不受从队列读的预备处理的约束。该缓冲有助于控制和优化数据流经过系统的速度。结构Broker Kafka集群包含一个或多个服务器，这种服务器被称为brokerTopic 每条发布到Kafka集群的消息都有一个类别，这个类别被称为topic。（物理上不同topic的消息分开存储，逻辑上一个topic的消息虽然保存于一个或多个broker上但用户只需指定消息的topic即可生产或消费数据而不必关心数据存于何处）Partition parition是物理上的概念，每个topic包含一个或多个partition，创建topic时可指定parition数量。每个partition对应于一个文件夹，该文件夹下存储该partition的数据和索引文件Producer 负责发布消息到Kafka brokerConsumer 消费消息。每个consumer属于一个特定的consumer group（可为每个consumer指定group name，若不指定group name则属于默认的group）。使用consumer high level API时，同一topic的一条消息只能被同一个consumer group内的一个consumer消费，但多个consumer group可同时消费这一消息。kafka的四个核心Api应用程序使用producer API发布消息到1个或多个topic中。应用程序使用consumer API来订阅一个或多个topic,并处理产生的消息。应用程序使用streams API充当一个流处理器,从1个或多个topic消费输入流,并产生一个输出流到1个或多个topic,有效地将输入流转换到输出流。connector API允许构建或运行可重复使用的生产者或消费者,将topic链接到现有的应用程序或数据系统。kafka配置说明ProducerProperties props = new Properties(); props.put("bootstrap.servers", "master:9092,slave1:9092,slave2:9092"); props.put("acks", "all"); props.put("retries", 0); props.put("batch.size", 16384); props.put("key.serializer", StringSerializer.class.getName()); props.put("value.serializer", StringSerializer.class.getName()); KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);其中：bootstrap.servers： kafka的地址。acks:消息的确认机制，默认值是0；acks=0：如果设置为0，生产者不会等待kafka的响应。acks=1：这个配置意味着kafka会把这条消息写到本地日志文件中，但是不会等待集群中其他机器的成功响应。acks=all：这个配置意味着leader会等待所有的follower同步完成。这个确保消息不会丢失，除非kafka集群中所有机器挂掉。这是最强的可用性保证。retries：配置为大于0的值的话，客户端会在消息发送失败时重新发送。batch.size:当多条消息需要发送到同一个分区时，生产者会尝试合并网络请求。这会提高client和生产者的效率。key.serializer: 键序列化，默认org.apache.kafka.common.serialization.StringDeserializer。value.deserializer:值序列化，默认org.apache.kafka.common.serialization.StringDeserializer。配置好之后，便可以生产数据了：producer.send(new ProducerRecord<String, String>(topic,key,value));其中的参数：topic：消息队列的名称，可以先行在kafka服务中进行创建。如果kafka中并未创建该topic，那么便会自动创建！key：键值，也就是value对应的值，和Map类似。value：要发送的数据，数据格式为String类型的。ConsumerProperties props = new Properties(); props.put("bootstrap.servers", "master:9092,slave1:9092,slave2:9092"); props.put("group.id", GROUPID); props.put("enable.auto.commit", "true"); props.put("auto.commit.interval.ms", "1000"); props.put("session.timeout.ms", "30000"); props.put("max.poll.records", 1000); props.put("auto.offset.reset", "earliest"); props.put("key.deserializer", StringDeserializer.class.getName()); props.put("value.deserializer", StringDeserializer.class.getName()); KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);其配置如下：group.id：组名 不同组名可以重复消费。例如你先使用了组名A消费了kafka的1000条数据，但是你还想再次进行消费这1000条数据，并且不想重新去产生，那么这里你只需要更改组名就可以重复消费了。enable.auto.commit：是否自动提交，默认为true。auto.commit.interval.ms: 从poll(拉)的回话处理时长。session.timeout.ms：超时时间。max.poll.records：一次最大拉取的条数。auto.offset.reset：消费规则，默认earliest 。earliest：当各分区下有已提交的offset时，从提交的offset开始消费；无提交的offset时，从头开始消费 。latest：当各分区下有已提交的offset时，从提交的offset开始消费；无提交的offset时，消费新产生的该分区下的数据 。none：topic各分区都存在已提交的offset时，从offset后开始消费；只要有一个分区不存在已提交的offset，则抛出异常。首先需要需要先订阅一个topic，也就是指定消费哪一个topic：consumer.subscribe(Arrays.asList(topic));订阅之后，即可从kafka中拉取数据：ConsumerRecords<String, String> msgList=consumer.poll(1000);可以使用for(;;)循环来进行进行监听：for (;;) { msgList = consumer.poll(1000); if (null != msgList && msgList.count() > 0) { for (ConsumerRecord < String, String > record: msgList) { //消费100条就打印 ,但打印的数据不一定是这个规律的 if (messageNo % 100 == 0) { System.out.println(messageNo + "=======receive: key = " + record.key() + ", value = " + record.value() + " offset===" + record.offset()); } //当消费了1000条就退出 if (messageNo % 1000 == 0) { break; } messageNo++; } } else { Thread.sleep(1000); } }用户登录方案一个App首先除了内容就是个人账号了，而用户注册登录的方案是值得探讨的。而登录就需要使用到用户名和密码，为了安全起见，在登录过程中暴漏密码的次数越少越好。首先，为了保证登录的安全性，最起码要使用Https协议HTTPS（Secure Hypertext Transfer Protocol）安全超文本传输协议，是一个安全通信通道，基于HTTP开发，用于在客户计算机和服务器之间交换信息，实际上应用了安全套接字层（SSL）作为HTTP应用层的子层，简单来说它是HTTP的安全版。但是，https协议需要到ca申请证书，一般免费证书很少，需要交费。可以看到所有大型网站，例如京东，淘宝，支付宝，涉及到登录和支付的页面，url都是以https开头，这就意味着，这次通讯是使用https。开放平台的api，例如新浪微博，腾讯等，api请求都是以https开头的。https是业界常用的保证安全性的协议。因此，涉及安全问题的api，都应该使用https协议。虽然，https为了保证安全性，在效率上是比http协议低。基本的用户登录方案在传统的web网站中，可以使用cookie+session来实现用户的登录维护而在app后端，需要避免每次验证用户身份都需要传输用户名和密码，流程如下：此时App已经获取到了token值，为了安全，不在网络上传输token，而使用签名校验（这里使用URL签名）的方式，API请求加上URL签名sign和用户id后如下：test.com/user/update?uid=2&sign=3f1e736bc4ae958ae7e8500b45aefdbb&age=22这样，token就不需要附在URL上了。App后台签名校验流程如下：除此之外，还可以设置时间戳，这样在一定长度的时间之后，URL就失效了，也能提升安全性。三、实验思考及感想本周主要学习了常用的消息队列系统Kafka的架构和使用方法，当然，Kafka可能主要用在集群或者分布式系统中，并且在其中的效果也能体现的更好一些，而我们的项目由于设备限制和能力限制，可能最终只会是单服务器的后台。除此之外，也是项目开头的一个内容，即是App的用户登录方案，和我以前所学的Web开发中运用的方法不同，App中用到了token和sign来进行用户验证，对我来说是一个全新的方案，这是接下来要实现的内容，还有ssm框架的学习。