1.功能分析
首先客户端登录页面如下,我们所要实现的一个效果以及功能要求
1.支持用户名密码登陆
2.为防止机器人恶意访问网站或者服务器,保障用户信息安全性拓展验证码登陆
3.用户登陆后信息存储有效时间为半个小时,超过有效时间需重新登陆
4.如果用户未登陆无法访问其他页面,需重定向到登陆页面
5.用户名,密码,验证码要做非空验证
6.验证码验证失败后需自动刷新验证码,验证码有效期设置为5分钟
7.如登陆异常需给出错误原因,并对账户进行最大可尝试登陆5次设置,如超过设置则锁定10分钟,之后可再次尝试登陆
8.登陆成功后自动重定向到工作台(首页)
2.功能实现
2.1. 初期准备
2.1.1. 创建数据库 zh_user
首先我们先创建一个用户表,设置基础信息包含用户名密码可用于登陆校验
CREATE TABLE `zh_user` (
`id` bigint NOT NULL,
`user_name` varchar(255) DEFAULT NULL COMMENT '用户名/登录名',
`pass_word` varchar(255) DEFAULT NULL COMMENT '密码',
`user_status` tinyint DEFAULT NULL COMMENT '用户状态 0 禁用 1启用 2锁定',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
2.1.2. 创建实体映射entity
package com.zhuhuo.modual.entity;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Id;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "zh_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Long id;
/**
* 用户名/登录名
*/
private String userName;
/**
* 密码
*/
private String passWord;
/**
* 用户状态
*/
private Byte userStatus;
}
2.1.3. 创建mapper,mapper.xml
package com.zhuhuo.modual.mapper;
import com.zhuhuo.core.frame.mapper.BasicsMapper;
import com.zhuhuo.modual.entity.User;
public interface UserMapper extends BasicsMapper<User>{
}
<?xml versinotallow="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zhuhuo.modual.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.zhuhuo.modual.entity.User">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_name" property="userName" jdbcType="VARCHAR"/>
<result column="pass_word" property="passWord" jdbcType="VARCHAR"/>
<result column="user_status" property="userStatus" jdbcType="TINYINT"/>
</resultMap>
<sql id="base_column_list">
id , user_name, pass_word,user_status
</sql>
</mapper>
2.1.4. 创建service,serviceImpl
package com.zhuhuo.modual.service;
public interface UserService{
}
package com.zhuhuo.modual.service.impl;
import com.zhuhuo.modual.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
}
2.1.5. 准备账户
初始化账户zhangsan,并设置密码为123456,并通过密码加密,加密方法如下
DigestUtils.md5DigestAsHex("123456".trim().getBytes()
sql语句
INSERT INTO `zhuhuo-blog`.`zh_user` (`id`, `user_name`, `pass_word`, `user_status`) VALUES (1, 'zhangsan','e10adc3949ba59abbe56e057f20f883e', 1);
2.2.接口设计
2.3. 普通登陆
2.3.1. 静态页面
2.3.1.1. 页面引入
首先通过hplus引入登陆页面login,hplus提供2种登陆页面模式,我们选择login.html
2.3.1.2. 资源引入
在登陆页面中我们主要引入资源,放到/static/lib包下
样式:bootstrap.min.css,font-awesome.min.css,animate.min.css,hplus.css,layui.css
js:jquery.min.js,bootstrap.min.js,layui.js
2.3.1.3. 页面调整
对引入后的资源进行thymeleaf读取方式进行调整
css调整
<link rel="shortcut icon" href="favicon.ico">
<link href="../../../static/lib/bootstrap/css/bootstrap.min.css" th:href="@{/lib/bootstrap/css/bootstrap.min.css}" rel="stylesheet">
<link href="../../../static/lib/font-awesome/css/font-awesome.min.css" th:href="@{/lib/font-awesome/css/font-awesome.min.css?v=4.7.0}" rel="stylesheet">
<link href="../../../static/lib/animate/animate.min.css" th:href="@{/lib/animate/animate.min.css}" rel="stylesheet">
<link href="../../../static/lib/layui/css/layui.css" rel="stylesheet" th:href="@{/lib/layui/css/layui.css}"/>
<link href="../../../static/lib/hplus/css/hplus.css" th:href="@{/lib/hplus/css/hplus.css}" rel="stylesheet">
js调整
<script src="../../../static/lib/jquery/jquery.min.js" th:src="@{/lib/jquery/jquery.min.js}"></script>
<script src="../../../static/lib/bootstrap/js/bootstrap.min.js" th:src="@{/lib/bootstrap/js/bootstrap.min.js}"></script>
<script src="../../../static/lib/layui/layui.js" th:src="@{/lib/layui/layui.js}"></script>
2.3.1.4. 修改登陆表单区域
<div class="middle-box text-center loginscreen animated fadeInDown">
<div>
<h3>欢迎登陆烛火博客管理后台</h3>
<form class="m-t" role="form" id="login-form">
<!-- 用户名 -->
<div class="form-group">
<input type="text" class="form-control" placeholder="用户名" id="username" name="username">
</div>
<!-- 密码 -->
<div class="form-group">
<input type="password" class="form-control" placeholder="密码" id="password" name="password">
</div>
<button type="button" class="btn btn-primary block full-width m-b">登 录</button>
</form>
</div>
</div>
2.3.1.5. 添加背景图
由于hplus的login页面是无背景图的,因此我们给页面添加一个背景图,背景图也是通过hplus下自带的,可以自行转换成自己想要的图片
<body class="gray-bg" style="background-image: url('/img/login-background.jpg')">
2.3.1.6. 静态资源映射
package com.zhuhuo.core.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration("configWebConfig")
@Primary
public class WebConfiguration implements WebMvcConfigurer {
/**
* 配置访问本地资源映射
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/*").addResourceLocations("classpath:/static/**");
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}
2.3.1.7. 查看效果
2.3.1.7. 添加登陆成功后的index.html
<!DOCTYPE html>
<html>
<body>
登陆进来了
</body>
</html>
2.3.2. 登陆功能
2.3.2.1.创建登陆控制层 LoginController
package com.zhuhuo.modual.controller.manager;
@Slf4j
@Controller
@RequestMapping(value = "/m")
public class LoginController {
@Autowired
private LoginService loginService;
}
2.3.2.2. 跳转登陆页面 loginPage方法
/**
* <h2>跳转登陆页面</h2>
* @return
*/
@GetMapping(value = "/loginPage")
public String loginPage(){
return "/manager/login";
}
2.3.2.3. 登陆提交功能doLogin方法
/**
* 执行登陆操作
* @param loginBO {@link LoginBO}登陆请求对象
* @return
*/
@ResponseBody
@PostMapping(value = "/dologin")
public RespJsonData<String> doLogin(@RequestBody LoginBO loginBO, HttpServletRequest httpServletRequest){
return loginService.doLogin(loginBO,httpServletRequest);
}
service
package com.zhuhuo.modual.service;
import com.zhuhuo.core.frame.resp.RespJson;
import com.zhuhuo.core.frame.resp.RespJsonData;
import com.zhuhuo.modual.model.bo.login.LoginBO;
import javax.servlet.http.HttpServletRequest;
public interface LoginService {
/**
* <h2>登陆操作</h2>
* @param loginBO
* @return
*/
RespJsonData<String> doLogin(LoginBO loginBO, HttpServletRequest httpServletRequest);
}
package com.zhuhuo.modual.service.impl;
import com.zhuhuo.common.ObjectUtil;
import com.zhuhuo.core.constant.ZhuHuoConstant;
import com.zhuhuo.core.constant.ZhuHuoResponseCode;
import com.zhuhuo.core.frame.exectpion.base.BizException;
import com.zhuhuo.core.frame.resp.RespJson;
import com.zhuhuo.core.frame.resp.RespJsonData;
import com.zhuhuo.modual.entity.User;
import com.zhuhuo.modual.mapper.UserMapper;
import com.zhuhuo.modual.model.bo.login.LoginBO;
import com.zhuhuo.modual.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Service("LoginService")
public class LoginServiceImpl implements LoginService {
@Autowired
private UserMapper userMapper;
@Override
public RespJsonData<String> doLogin(LoginBO loginBO, HttpServletRequest httpServletRequest) {
//1.获取用户信息
User user = userMapper.findUserInfoByUserName(loginBO.getUsername());
//2.判断用户是否存在
if(ObjectUtil.isNotEmpty(user)){
//判断用户密码是否正确,后面集成spring security的时候可以替换
String md5Pwd = DigestUtils.md5DigestAsHex(loginBO.getPassword().trim().getBytes());
if(!md5Pwd.equals(user.getPassWord())){
throw new BizException(ZhuHuoResponseCode.USER_NAME_OR_PASS_WORD_IS_ERROR);
}
//返回响应结果,实际上在客户端响应的是重定向的内容.
return RespJsonData.success("/m/index");
}else {
throw new BizException(ZhuHuoResponseCode.USER_NOT_EXIST);
}
}
}
2.3.2.4.创建登陆成功后跳转的首页控制层 IndexController
package com.zhuhuo.modual.controller.manager;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping(value = "/m/index")
public String index(){
return "manager/index";
}
}
2.3.3. 前端接口开发和校验
2.3.3.1. 引入校验插件
<script src="../../../static/lib/validate/jquery.validate.min.js" th:src="@{/lib/validate/jquery.validate.min.js}"></script>
<script src="../../../static/lib/validate/messages_zh.min.js" th:src="@{/lib/validate/messages_zh.min.js}"></script>
2.3.3.2. 添加登陆方法js
button添加点击事件notallow="login()"
<button type="button" class="btn btn-primary block full-width m-b" notallow="login()">登 录</button>
登陆事件和校验规则
$('#login-form').validate({
onkeyup: false,
rules: {
username: {
required: true,
},
password:{
required: true,
},
},
messages: {
username: {
required: "用户名不能为空",
},
password:{
required: "密码不能为空",
},
},
focusCleanup: true
});
function login() {
if($("#login-form").validate().form()){
let username = $("#username").val();
let password = $("#password").val();
let requestData = {
"username":username,
"password":password
}
console.log('requestData',requestData)
//验证通过,提交请求
$.ajax({
url:"/m/dologin",
type:"post",
data:JSON.stringify(requestData),
dataType:"json",
contentType: "application/json",
success: function(result) {
if(result.responseCode == "200"){
window.location.href = result.responseData;
}else {
top.layer.msg(result.responseMessage,{icon:2,time:1000,shift:5});
}
}
})
}
}
2.3.4. 测试登陆
2.3.4.1. 必填校验
2.3.4.2. 登陆结果
2.4. 添加验证码
2.4.1. 静态页面
2.4.1.1. 页面调整
login.html 表单区域添加验证码信息
<div class="row form-group">
<div class="col-xs-6">
<input type="text" name="verifycode" id="verifycode" class="form-control" placeholder="验证码" maxlength="5" />
</div>
<div class="col-xs-6">
<img src="" alt="换一张" id="verify-code-img" style="cursor: pointer;">
</div>
</div>
2.4.1.2. 查看效果
其中标红为验证码显示区域,在填写完验证码相关功能后即可出现
2.4.2. 登陆调整
2.4.2.1. 新增验证码控制层CaptchaController
package com.zhuhuo.modual.controller.manager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/m/captcha")
public class CaptchaController {
}
2.4.2.2. pom文件引入依赖
<!-- hutool 工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
<!-- 缓存 redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
2.4.2.3. 添加获取验证码方法
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping(value = "/findCaptcha")
public void findCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("image/jpeg");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
//定义图形验证码的长、宽、验证码字符数、干扰线宽度
ShearCaptcha shearCaptcha = CaptchaUtil.createShearCaptcha(130, 34, 5, 4);
//图形验证码写出,可以写出到文件,也可以写出到流
shearCaptcha.write(response.getOutputStream());
//验证码可以放到session中,也可放到缓存中,依据需要自行定义,我们采用缓存方式(实际项目中更多的是应用缓存
//而不是session,并不是说session不好而是实际项目中很多项目的都比较复杂缓存可以更方便更合适的运用)
//设置验证码有效期为5分钟
stringRedisTemplate.opsForValue().set("verifycode",shearCaptcha.getCode(),5, TimeUnit.SECONDS);
log.info("生成的验证码 -> verifycode:{}",shearCaptcha.getCode());
}
2.4.2.4. 修改doLogin方法
//判断验证码信息
if(!stringRedisTemplate.hasKey("verifycode")){
throw new BizException(ZhuHuoResponseCode.VERIFY_CODE_EXPIRED);
}
//获取验证码信息
String verifycode = stringRedisTemplate.opsForValue().get("verifycode");
//判断验证码准确性
if(!verifycode.equals(loginBO.getVerifycode())){
throw new BizException(ZhuHuoResponseCode.ERROR_VERIFY_CODE_WRONG);
}
//验证码验证通过后清除缓存中的验证码信息
stringRedisTemplate.delete("verifycode");
2.4.3. 前端接口开发和校验
2.4.3.1. 修改js添加验证码相关内容
$("#verify-code-img").on("click",function () {
console.log("点击了验证吗")
$(this).attr("src","/m/captcha/findCaptcha?id="+Math.random());
})
$("#verify-code-img").trigger("click");
2.4.3.2. 修改js添加验证码校验
rules下添加
verifycode:{
required: true,
}
messages下添加
verifycode:{
required: "验证码不能为空",
}
login方法下添加
let verifycode = $("#verifycode").val();
let requestData = {
"username":username,
"password":password,
"verifycode":verifycode
}
login方法返回值添加
if((result.responseCode == "100002") || (result.responseCode = '100003')){
//刷新验证码
$("#verify-code-img").trigger("click");
}
2.4.4. 测试登陆
为了进行测试验证我们对验证码的有效期调整为30s,在findCaptcha方法下修改
stringRedisTemplate.opsForValue().set("verifycode",shearCaptcha.getCode(),30, TimeUnit.SECONDS);
2.5. 添加登陆次数
2.5.1. 修改doLogin方法
第一种是实现方式
//在验证码验证通过后清除缓存中的验证码信息下添加
//判断用户是否因登陆次数过多而被锁定
String logingErrorKey = "login:error:"+loginBO.getUsername();
if(stringRedisTemplate.hasKey(logingErrorKey)){
int value = Integer.parseInt(stringRedisTemplate.opsForValue().get(logingErrorKey));
if(value > 5){
log.error("{}用户登录错误次数({})过多,请联系管理员",loginBO.getUsername(),value);
throw new BizException(ZhuHuoResponseCode.LOGIN_LOCKED);
}
}
//修改密码判断
if(!md5Pwd.equals(user.getPassWord())){
String logingErrorCount =stringRedisTemplate.opsForValue().get(logingErrorKey);
int value = ObjectUtil.isEmpty(logingErrorCount) ? 0 :Integer.parseInt(logingErrorCount);
stringRedisTemplate.opsForValue()
.set(logingErrorKey,String.valueOf((value+1)),30*120,TimeUnit.SECONDS);
throw new BizException(ZhuHuoResponseCode.USER_NAME_OR_PASS_WORD_IS_ERROR.getResponseCode(),ZhuHuoResponseCode.USER_NAME_OR_PASS_WORD_IS_ERROR.getResponseMessage()+"还剩"+(5-value)+"次机会");
}
第二种实现方式,适用于次数累计,并发场景,通过incr去处理
//判断用户是否因登陆次数过多而被锁定
String logingErrorKey = "login:error:"+loginBO.getUsername();
if(stringRedisTemplate.hasKey(logingErrorKey)){
log.error("{}用户登录错误次数({})过多,请联系管理员",loginBO.getUsername(),logingErrorKey);
throw new BizException(ZhuHuoResponseCode.LOGIN_LOCKED);
}
if(!md5Pwd.equals(user.getPassWord())){
Long count = stringRedisTemplate.opsForValue().increment(loginBO.getUsername(),1);
if(count >= 5){
stringRedisTemplate.opsForValue().set(logingErrorKey, String.valueOf(count),30*120,TimeUnit.SECONDS);
}
throw new BizException(ZhuHuoResponseCode.USER_NAME_OR_PASS_WORD_IS_ERROR.getResponseCode()
,ZhuHuoResponseCode.USER_NAME_OR_PASS_WORD_IS_ERROR.getResponseMessage()+"还剩"+(5-count)+"次机会");
}
2.5.2. 测试验证
2.6. 存储用户信息
2.6.1. 修改doLogin方法
在响应结果前添加用户信息到缓存并设置有效时间为30分钟
stringRedisTemplate.opsForValue().set("user", JSON.toJSONString(user),30*60, TimeUnit.SECONDS);
2.7.2. 测试验证
2.7. 登陆拦截
2.7.1. 添加UserAuthInterceptor类
package com.zhuhuo.core.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class UserAuthInterceptor extends HandlerInterceptorAdapter {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("请求地址校验拦截",request.getRequestURI());
//可以做更多的事情,例如用户每次请求后把用户相关信息提取并在业务中使用
Object obj = stringRedisTemplate.opsForValue().get("user");
if (obj == null) {
response.sendRedirect("/m/loginPage");
return false;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
2.7.2. 配置资源过滤
在WebConfiguration下重写方法addInterceptors
@Autowired
private UserAuthInterceptor userAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userAuthInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(getExcludePathPatterns())
}
private List<String> getExcludePathPatterns() {
List<String> excludePathPatterns = new ArrayList<>();
//管理后台登陆相关放行
excludePathPatterns.add("/m/loginPage");
excludePathPatterns.add("/m/dologin");
excludePathPatterns.add("/m/captcha/findCaptcha");
//静态资源放行
excludePathPatterns.add("/lib/**");
excludePathPatterns.add("/local/**");
excludePathPatterns.add("/img/**");
excludePathPatterns.add("/static/**");
//客户端访问放行
excludePathPatterns.add("/c/**");
//其他不需要拦截的资源可自行添加
return excludePathPatterns;
}