JAVA项目(使用SSM实现)各部分详细分析(CRM)|| part1
文章目录
项目结构:
1、首页功能
首页功能比较简单,可以在springmvc.xml文件中设:视图控制器,或者写一个controller实现这个功能。
<!--
视图控制器:为当前的请求直接设置视图名称实现页面跳转,这就不需要再controller里面多写一个方法,来实现跳转到首页了!
/ 表示从项目名开始输入的内容,即tomcat配置的那个开始 http://localhost:8080/crm 都省略
若设置视图控制器,则只有视图控制器所设置的请求会被处理,其他的请求将全部404
此时必须在配置一个标签:<mvc:annotation-driven />
-->
<mvc:view-controller path="/" view-name="index"/>
2、用户登入
2.1 设计要求
- 用户名和密码不能为空
- 用户名或者密码错误,用户已过期,用户状态被锁定,ip受限 都不能登录成功
- 登录成功之后,所有业务页面显示当前用户的名称
- 实现10天记住密码
- 登录成功之后,跳转到业务主页面
- 登录失败,页面不跳转,提示信息
2.2 设计思路:
1. 前端提交的内容只有三个
用户名、密码、是否十天免登入(前端首页那边只能提交这三个东西); 这样一来要想到:controller那边只要接收这三个参数(复习使用map传递数据),并根据其中的一些参数查数据库(用户名、密码)
2. 配置mybatis-generator
能自动生成对应的实体类、mapper和xml
只要配置一个xml文件(和properties文件),在文件里面填写:实体类、mapper和xml自动生成后放的位置。注意对应表进行生成后,将该表注释,否则下次生成其他表的时候会覆盖,自己修改的mappe和xml就被覆盖了!
<javaModelGenerator targetPackage="cn.edu.uestc.crm.settings.domain"
targetProject="D:\\JAVA\\SSM\\CRM\\code\\crm-project\\crm\\src\\main\\java">
<!-- 是否允许子包,即targetPackage.schemaName.tableName -->
<property name="enableSubPackages" value="false"/>
<!-- 是否对model添加 构造函数 true添加,false不添加-->
<property name="constructorBased" value="false"/>
<!-- 是否对类CHAR类型的列的数据进行trim操作 -->
<property name="trimStrings" value="true"/>
<!-- 建立的Model对象是否 不可改变 即生成的Model对象不会有 setter方法,只有构造方法 -->
<property name="immutable" value="false"/>
</javaModelGenerator>
<table tableName="tbl_clue" domainObjectName="Clue"
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false">
</table>
3. 根据用户名密码查数据库
想法是封装为一个对象RetObj,并以json的形式返回(在ajax请求中设置dataType:'json’即可,其实很简单的)。将返回信息存到一个类中,该类的属性有:
private String code;//处理成功获取失败的标记:1---成功,0---失败
private String message;//提示信息
private Object retData;//其它数据(以后可能用到)
注:不将查询结果信息存到map而存到对象的原因是:
map效率比较低,它又得计算下标(数组那边:key通过hashcode得到哈希值,哈希值又通过算法转为下标),又得equals找链表
4. 具体查询过程的逻辑(满足设计要求2等)
如何设计要求2?要求2中的需要很多在数据库那边就封装好了,查出来返回一个对象到controller,再判断对象对应的属性是否满足要求。如果其中一个不满足要求,那上面返回给前端的类RetObj, code=0,message设置为对应属性哪个不满足要求了…
@RequestMapping("/settings/qx/user/login.do")
public @ResponseBody Object login(String loginAct, String loginPwd, String isRemPwd, HttpServletRequest request, HttpServletResponse response, HttpSession session){
//封装参数,注意使用泛型,每个Obj都要又名字(key)
Map<String,Object> map=new HashMap<>();
map.put("loginAct",loginAct);
map.put("loginPwd",loginPwd);
//调用service层方法,查询用户
User user=userService.queryUserByLoginActAndPwd(map);
//根据查询结果,生成响应信息
ReturnObject returnObject=new ReturnObject();
if(user==null){
//登录失败,用户名或者密码错误
returnObject.setCode(Contants.RETURN_OBJECT_CODE_FAIL);
returnObject.setMessage("用户名或者密码错误");
}else{//进一步判断账号是否合法
//user.getExpireTime() //2019-10-20
// new Date() //2020-09-10
if(DateUtils.formateDateTime(new Date()).compareTo(user.getExpireTime())>0){
//登录失败,账号已过期
returnObject.setCode(Contants.RETURN_OBJECT_CODE_FAIL);
returnObject.setMessage("账号已过期");
}else if("0".equals(user.getLockState())){
//登录失败,状态被锁定
returnObject.setCode(Contants.RETURN_OBJECT_CODE_FAIL);
returnObject.setMessage("状态被锁定");
}else if(!user.getAllowIps().contains(request.getRemoteAddr())){
//登录失败,ip受限
returnObject.setCode(Contants.RETURN_OBJECT_CODE_FAIL);
returnObject.setMessage("ip受限");
}else{
//登录成功
returnObject.setCode(Contants.RETURN_OBJECT_CODE_SUCCESS);
//把user保存到session中
session.setAttribute(Contants.SESSION_USER,user);
//如果需要记住密码,则往外写cookie
if("true".equals(isRemPwd)){
Cookie c1=new Cookie("loginAct",user.getLoginAct());
c1.setMaxAge(10*24*60*60);
response.addCookie(c1);
Cookie c2=new Cookie("loginPwd",user.getLoginPwd());
c2.setMaxAge(10*24*60*60);
response.addCookie(c2);
}else{
//把没有过期cookie删除
Cookie c1=new Cookie("loginAct","1");
c1.setMaxAge(0);
response.addCookie(c1);
Cookie c2=new Cookie("loginPwd","1");
c2.setMaxAge(0);
response.addCookie(c2);
}
}
}
return returnObject;
}
5. 前端工作
5.1 解决设计要求1:用户名和密码不能为空
- 如果用户名密码为空,直接不让提交请求,以免给后台造成太大的负担,不为空才提交ajax请求,继续下一步去后台查数据库。
- 前端一共三个输入需要处理:用户名(需要处理空格问题)、密码、十天免登入。
- 使用ajax异步请求,所以在按钮界面上,不能使用submit(会直接提交同步请求),应该使用button,设置id,之后使用jQuery的id选择器对该id进行函数封装处理。
- 注意细节:这里的代码实际是JavaScript弱类型语言,所有比较都是使用==进行比较,而在java中字符串比较是使用.equals()进行比较
- 前端这边完成后,进行ajax访问后,如果数据成功,那应该进行同步跳转,跳到CRM系统里面去,这个比较简单,使
window.location.href="workbench/index.do"
完成跳转
5.2 前端提交请求到后端查数据库期间,显示:正在努力验证…
在登入过程中去查询数据库,返回ajax前会有一段空档期时间,这时候最好提示一下 “正在努力登入之类的信息”,这段信息既要在点击按钮之后,又要在查询完信息之前,使用:
//当ajax向后台发送请求之前,会自动执行本函数;
//该函数的返回值能够决定ajax是否真正向后台发送请求:
//如果该函数返回true,则ajax会真正向后台发送请求;否则,如果该函数返回false,则ajax放弃向后台发送请求。
beforeSend:function () {
$("#msg").text("正在努力验证....");
return true;
}
5.3 前端登入页面代码
$(function () {
//给整个浏览器窗口添加键盘按下事件
$(window).keydown(function (e) {
//如果按的是回车键,则提交登录请求
if(e.keyCode==13){
$("#loginBtn").click();
}
});
//给"登录"按钮添加单击事件
$("#loginBtn").click(function () {
//收集参数
var loginAct=$.trim($("#loginAct").val());
var loginPwd=$.trim($("#loginPwd").val());
var isRemPwd=$("#isRemPwd").prop("checked");
//表单验证
if(loginAct==""){
alert("用户名不能为空");
return;
}
if(loginPwd==""){
alert("密码不能为空");
return;
}
//显示正在验证
//$("#msg").text("正在努力验证....");
//发送请求
$.ajax({
url:'settings/qx/user/login.do',
data:{
loginAct:loginAct,
loginPwd:loginPwd,
isRemPwd:isRemPwd
},
type:'post',
dataType:'json',
success:function (data) {
if(data.code=="1"){
//跳转到业务主页面
window.location.href="workbench/index.do";
}else{
//提示信息
$("#msg").text(data.message);
}
},
//下面的代码思想注意学习
beforeSend:function () {//当ajax向后台发送请求之前,会自动执行本函数;
//该函数的返回值能够决定ajax是否真正向后台发送请求:
//如果该函数返回true,则ajax会真正向后台发送请求;否则,如果该函数返回false,则ajax放弃向后台发送请求。
$("#msg").text("正在努力验证....");
return true;
}
});
});
});
6.代码可移植性优化
在写代码时,经常会遇到写死一个字符串的情况:
- 年月日的格式:
yyyy-MM-dd HH:mm:ss
如果有一天要改为yyyy/MM/dd
的形式那要在整个工程中改就太麻烦了,写一个Utils,下次改只需要改这个就行(里面的方法使用静态方法,直接用类名去调用)
public static String formateDateTime(Date date){
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr=sdf.format(date);
return dateStr;
}
- 常量也类似:比如在controller封装一个类返回给前端里面规定了一个属性,值=1的时候代表登入成功,值=0的时候代表失败,前端ajax进行处理。如果有一天:规定返回0000是登入失败,返回1111是登入成功怎么办? 只要在这里改就行了。不用整个项目去找。
- 常量:登入成功后,进入页面,需要在顶上显示:欢迎xxx用户。这个功能中,需要使用session.setAttribute(“key”,user); 这里key的名字是一个字符串,不能写死,以后可能改掉!所以也写为常量。
public static final String RETURN_OBJECT_CODE_SUCCESS="1";//成功
public static final String RETURN_OBJECT_CODE_FAIL="0";//失败
//保存当前用户的key
public static final String SESSION_USER="sessionUser";
//备注的修改标记
public static final String REMARK_EDIT_FLAG_NO_EDITED="0";//0---没有修改过
public static final String REMARK_EDIT_FLAG_YES_EDITED="1";//1--已经被修改过
7.实现10天记住密码
-
主要思路:前端复选框选中记住密码,后端收到后,在登入成功后的代码中,设置cookie。
-
注意一个逻辑,用户点击了"十天记录密码",我们设定了cookie,如果下一次用户再登入的时候(复选框也要默认打勾),把复选框取消了,那这时候以前设置的cookie也要删除!还有如果点击了记住密码,下次打开这个页面,记录密码也是默认勾上的!
-----实现方法:如果上次点击登入了,那cookie就存在,使用jstl标签库就可以读取到,如果有cookie,那就checkbox 设置为check状态。
<c:if test="${not empty cookie.loginActCookie and not empty cookie.loginPwdCookie}"> <input type="checkbox" id="isRemPwd" checked> </c:if>
-
request是来到服务器的数据,response是从服务器响应到浏览器的数据。
1、使用response:response.addCookie(c1)将cookie响应到浏览器去
2、使用request:
2.1、request.getRemoteAddr()可以用于获取用户的ip地址等。
2.2、request.getCookie 浏览器将还没过期的cookie响应到服务器进行处理,然后再返回给前端,当然这种办法比较麻烦,可以使用el表达式(以前都是在作用域中获取,现在在cookie中获取:${cookie.loginAct.val},类似前面的sessionScope…),直接在前端解析出cookie,对文本框进行赋值。
代码如下:
//如果需要记住密码,则往外写cookie
if("true".equals(isRemPwd)){
Cookie c1=new Cookie("loginAct",user.getLoginAct());
c1.setMaxAge(10*24*60*60);
response.addCookie(c1);
Cookie c2=new Cookie("loginPwd",user.getLoginPwd());
c2.setMaxAge(10*24*60*60);
response.addCookie(c2);
}else{
//把没有过期cookie删除
Cookie c1=new Cookie("loginAct","1");
c1.setMaxAge(0);
response.addCookie(c1);
Cookie c2=new Cookie("loginPwd","1");
c2.setMaxAge(0);
response.addCookie(c2);
}
前端:使用jstl标签库还有el表达式
<div class="form-group form-group-lg">
<div style="width: 350px;">
<input class="form-control" id="loginAct" type="text" value="${cookie.loginAct.value}" placeholder="用户名">
</div>
<div style="width: 350px; position: relative;top: 20px;">
<input class="form-control" id="loginPwd" type="password" value="${cookie.loginPwd.value}" placeholder="密码">
</div>
<div class="checkbox" style="position: relative;top: 30px; left: 10px;">
<label>
<c:if test="${not empty cookie.loginAct and not empty cookie.loginPwd}">
<input type="checkbox" id="isRemPwd" checked>
</c:if>
<c:if test="${empty cookie.loginAct or empty cookie.loginPwd}">
<input type="checkbox" id="isRemPwd">
</c:if>
十天内免登录
</label>
<span id="msg" style="color: red"></span>
</div>
<button type="button" id="loginBtn" class="btn btn-primary btn-lg btn-block" style="width: 350px; position: relative;top: 45px;">登录</button>
</div>
8.登入后:所有业务页面显示当前用户的名称
- 需求:登入成功后,进入页面,需要在顶上显示:欢迎xxx用户。
- 解决思路:把控制层(controller)代码中处理好的数据传递到视图层(jsp),使用作用域传递:四个作用域非常重要)
8.1 作用域
– pageContext:用来在同一个页面的不同标签之间传递数据。
– request:在同一个请求过程中间传递数据。(使用model.setAttribute()效果一样,都是request作用域)
– session: 同一个浏览器窗口的不同请求之间传递数据。一般一个用户是一个窗口(一个用户登入一次)
application:所有用户共享的数据,并且长久频繁使用的数据。
- session获取方式:
void HttpSession.setAttribute(String name, Object value) 向session中保存信息
HttpSession HttpServletRequest.getSessio() 获取当前请求所在的session的对象。
- 具体做法:
– 在登入成功后,session.setAttribute(“key”,value); 之后在前端取出,显示到整体的页面上面。(session:同一个浏览器 窗口 可对应多个请求!)
后端:(在访问数据库,验证用户名密码ip状态等等通过后就设置,和cookie位置一样)
session.setAttribute(Contants.SESSION_USER,user);
前端:成功登入后在index.jsp页面上设置
${sessionScope.sessionUser.name}
3、安全退出
安全退出系统的主要思想是:1、清空session域(invalidate()方法),session对应的是多个request。2、清空cookie(sexMaxAge(0)方法)。思路就是专门写一个Controller方法,当页面点击“安全退出的时候”,进行上述操作,最后跳转到登入页面。
3.1 请求转发:
对应的是一次请求;浏览器上的URL不变
3.2 重定向:
对应的是两次请求(把url发送到前台,让前台再发送一次请求)。
-
什么时候使用请求转发,什么时候使用重定向?
第一个servlet没处理完,使用请求转发;处理完了,使用重定向
如果使用请求转发,刷新页面的话,还是访问Controller的RequestMapping对应的地址进行操作,但是重定向的话,就是一个新的页面了,再刷新也是对这个新页面的刷新,我们希望用户退出后url显示登入页面的url,而不是logout的url(转发的话不变),而且刷新的时候不要重复去执行清空session、cookie操作,只简单地刷新登入页面,所以这里使用重定向 -
这里发送同步请求,同步请求三个方式:地址栏(window.location.href=“对应的controller,或者页面”)、form表单,超级链接
-
代码如下
@RequestMapping("/settings/qx/user/logout.do")
public String logout(HttpServletResponse response, HttpSession session){
//两件事,清空session、cookie
Cookie actCookie = new Cookie("loginActCookie","1");
Cookie pwdCookie = new Cookie("loginPwdCookie","1");
actCookie.setMaxAge(0);
pwdCookie.setMaxAge(0);
response.addCookie(actCookie);
response.addCookie(pwdCookie);
session.invalidate();
return "redirect:/"; //回到首页,自动拼接前后缀后,首页的jsp也会自动跳转(之前写的了,复习)
}
4、登入验证
现在遇到这样的一个问题:上述代码在写的时候,页面全部都是通过访问对应的controller,之后使用controller的返回值进行前后缀拼接从而跳转的。也就是说登入后的页面可以通过访问对应的controller进行跳转,而不是说因为在web-inf下面就不能被访问(比如成功登入的页面就是ajax获取成功请求后访问一个controller,然后跳转到index.jsp页面的,如果直接记住那个controller的url,也可以直接跳转到目标页面),这就存在风险。
解决思路:配置过滤器、拦截器。
4.1 要求
登录验证.
用户访问任何业务资源,都需要进行登录验证.
只有登录成功的用户才能访问业务资源
没有登录成功的用户访问业务资源,跳转到登录页面
4.3 实现分析
- 什么样的类能够在用户访问目标资源之前先执行?----过滤器、拦截器
其中过滤器比较麻烦,且功能不足,需要实现接口Filter,重写方法(init…doFilter…destroy…),然后在web.xml 中进行配置,这里使用拦截器interceptor实现。 - 拦截器:
a)提供拦截器类:
....implements HandlerInterceptor{
--preHandle (请求到达目标资源之前执行) 只有它有返回值,返回true就放行,false就拦截
--postHandle (执行目标资源之后执行)
--afterHandle (响应回去后,最后执行)
}
b)配置拦截器:springmvc.xml
- 当初用户登入成功后干了三件事:很明显,使用session来解决当前问题。
//运行登入
retObj.setCode(Constants.RETURN_OBJECT_CODE_SUCCESS);
//成功登入以后,在页面左上角需要显示: 欢迎xxx用户,是针对多个不同请求的操作,使用session~
session.setAttribute(Constants.SESSION_USER,user);//sessionUser这个字符串名字不要写死,使用常量!
//记住密码复选框的处理
if ("true".equals(isRemPwd)){
Cookie actCookie = new Cookie("loginActCookie",user.getLoginAct());
Cookie pwdCookie = new Cookie("loginPwdCookie",user.getLoginPwd());
//十天
actCookie.setMaxAge(10*24*60*60);
pwdCookie.setMaxAge(10*24*60*60);
//传前端
response.addCookie(actCookie);
response.addCookie(pwdCookie);
}else...
4.4 具体实现
- 配置文件(在springmvc.xml中进行配置)
<mvc:interceptors>
<mvc:interceptor>
<!--
配置拦截的请求
之前说controller的RequestMapping都有对应的命名规范,这里就体现出了优势!
注意一个*代表一层子目录,两个**代表任意层子目录
-->
<mvc:mapping path="/settings/**"/>
<mvc:mapping path="/workbench/**"/>
<!--配置排除拦截的请求(优先级高)-->
<mvc:exclude-mapping path="/settings/qx/user/toLogin.do"/>
<mvc:exclude-mapping path="/settings/qx/user/login.do"/>
<!--拦截器类-->
<bean class="com.bjpowernode.crm.settings.web.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
- LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//如果用户没有登录成功,则跳转到登录页面
HttpSession session=httpServletRequest.getSession();
User user=(User) session.getAttribute(Contants.SESSION_USER);
if(user==null){
//重定向时,url必须加项目的名称(springmvc会自动加,所以写一个斜杆就行,这个倒是可以不用),如果是请求转发那也不用
httpServletResponse.sendRedirect(httpServletRequest.getContextPath());//获取的结果就算 /上下文
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}