Springboot+mybatis-plus+Redis实现登录功能,解决分布式Session问题
声明:代码是项目开始的一部分,想要后续的持续关注哦,或者留言你遇到的问题
- 使用mybatis-plus的代码生成生成相关代码
- 建立vo包建立登录功能所传递的登录参数
package com.wangxiaoxuan.seckill.vo;
import lombok.Data;
/**
* @author 王小轩
* @version 1.0
* 登录参数
*/
@Data
public class LoginVo {
private Long mobile;
private String password;
}
其次是公共返回对象枚举类,包含状态码,信息
package com.wangxiaoxuan.seckill.vo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* @author 王小轩
* @version 1.0
* 公共返回对象枚举
*/
@Getter
@ToString
@AllArgsConstructor
public enum ResBeanEnum {
SUCCESS(200,"SUCCESS"),
ERROR(500,"服务端异常"),
LOGIN_ERROR(600,"用户名或密码有误"),
BIND_ERROR(700,"参数校验异常");
private final Integer code;
private final String message;
}
以及公共返回对象,在该类中定义了登录成功以及失败的方法
package com.wangxiaoxuan.seckill.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author 王小轩
* @version 1.0
* 公共返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 成功返回结果
* @return
*/
public static RespBean success(){
return new RespBean(ResBeanEnum.SUCCESS.getCode(),ResBeanEnum.SUCCESS.getMessage(),null);
}
public static RespBean success(Object obj){
return new RespBean(ResBeanEnum.SUCCESS.getCode(),RespBean.success().getMessage(),obj);
}
/**
* 失败
* @param resBeanEnum
* @return
*/
public static RespBean error(ResBeanEnum resBeanEnum){
return new RespBean(resBeanEnum.getCode(),resBeanEnum.getMessage(),null);
}
public static RespBean error(ResBeanEnum resBeanEnum,Object obj){
return new RespBean(resBeanEnum.getCode(),resBeanEnum.getMessage(),obj);
}
}
3.接下来该写具体的登录方法在IUserService中定义方法在impl包中的实现类UserService中实现该登录方法,在此之前我们先建立相关的工具类
如CookieUtil,该类为我们提供了一些好用的方法,可直接拷贝,使用该类我们可以在登录功能中实现设置cookie和得到cookie
package com.wangxiaoxuan.seckill.Util;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* Cookie工具类
*
* @author zhoubin
* @since 1.0.0
*/
public final class CookieUtil {
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置Cookie的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}
/**
* 设置Cookie的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
* 删除Cookie带cookie域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0) {
cookie.setMaxAge(cookieMaxage);
}
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
// 通过request对象获取访问的url地址
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
// 将url地下转换为小写
serverName = serverName.toLowerCase();
// 如果url地址是以http://开头 将http://截取
if (serverName.startsWith("http://")) {
serverName = serverName.substring(7);
}
int end = serverName.length();
// 判断url地址是否包含"/"
if (serverName.contains("/")) {
//得到第一个"/"出现的位置
end = serverName.indexOf("/");
}
// 截取
serverName = serverName.substring(0, end);
// 根据"."进行分割
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
其次还有uuid这不可或缺的宝贝
package com.wangxiaoxuan.seckill.Util;
import java.util.UUID;
/**
* UUID工具类
*
* @author zhoubin
* @since 1.0.0
*/
public class UUIDUtil {
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
一切准备就绪后,当然是实现类了,当然方法中会抛出相应异常,比如密码不匹配或者服务端异常等,所以我们先建立相应的异常的包以及类
建立ex包并在包下建立相应的异常类
先建立基础公共异常类
package com.wangxiaoxuan.seckill.service.ex;
import com.wangxiaoxuan.seckill.vo.ResBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author 王小轩
* @version 1.0
* 基础异常
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseException extends RuntimeException{
private ResBeanEnum resBeanEnum;
}
再建立其他你所需要的异常,就不做赘述了
最后建立处理异常的类作统一异常处理使用@RestControllerAdvice注解
以及@ExceptionHandler(Exception.class)不明白这两注解的兄弟可以百度
package com.wangxiaoxuan.seckill.service.ex;
import com.wangxiaoxuan.seckill.vo.ResBeanEnum;
import com.wangxiaoxuan.seckill.vo.RespBean;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.net.BindException;
/**
* @author 王小轩
* @version 1.0
* 该类处理程序运行过程中的异常
*/
@RestControllerAdvice
public class BaseExceptionHandler {
@ExceptionHandler(Exception.class)
public RespBean ExceptionHandler(Exception e){
//判断异常属于哪类异常并返回对应的公共返回对象,这里举了两个例子
if (e instanceof BaseException){
BaseException ex = (BaseException) e;
return RespBean.error(ex.getResBeanEnum());
}else if (e instanceof BindException){
BaseException ex = (BaseException) e;
RespBean respBean = RespBean.error(ResBeanEnum.BIND_ERROR);
return respBean;
}
return RespBean.error(ResBeanEnum.ERROR);
}
}
异常定义好之后就是实现类的实现方法了
package com.wangxiaoxuan.seckill.service.impl;
import com.wangxiaoxuan.seckill.Util.CookieUtil;
import com.wangxiaoxuan.seckill.Util.UUIDUtil;
import com.wangxiaoxuan.seckill.pojo.User;
import com.wangxiaoxuan.seckill.mapper.UserMapper;
import com.wangxiaoxuan.seckill.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wangxiaoxuan.seckill.service.ex.BaseException;
import com.wangxiaoxuan.seckill.vo.LoginVo;
import com.wangxiaoxuan.seckill.vo.ResBeanEnum;
import com.wangxiaoxuan.seckill.vo.RespBean;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>
* 服务实现类
* </p>
*
* @author wangxiaoxuan
* @since 2022-03-20
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 登录
* @param loginVo
* @param request
* @param response
* @return user
*/
@Override
public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
//拿到浏览器传过来的数据,登录参数以LoginVo对象的形式传递过来
Long mobile = loginVo.getMobile();
String password = loginVo.getPassword();
//判断传递过来的数据是否为null
if (mobile==null||password==null){
//抛出异常
throw new BaseException(ResBeanEnum.LOGIN_ERROR);
}
User user = userMapper.selectById(loginVo.getMobile());
Long id = user.getId();
String pwd = user.getPassword();
if (user == null){
throw new BaseException(ResBeanEnum.LOGIN_ERROR);
}
if (!(mobile.equals(id)&password.equals(pwd))){
throw new BaseException(ResBeanEnum.LOGIN_ERROR);
}
//生成cookie
String ticket = UUIDUtil.uuid();
//request.getSession().setAttribute(ticket,user);
//将用户信息存入redis,String类型
redisTemplate.opsForValue().set("user:"+ticket,user);
//使用cookie工具类,生成缓存
CookieUtil.setCookie(request,response,"userTicket",ticket);//不设置生效时间默认浏览器关闭即失效,也不编码
return RespBean.success();
}
/**
* 通过cookie得到user对象
* @param userTicket
* @param request
* @param response
* @return
*/
@Override
public User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response) {
if (StringUtils.isEmpty(userTicket)){
return null;
}
User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
if (user!=null){
CookieUtil.setCookie(request,response,"userTicket",userTicket);
}
return user;
}
}
实现类解决之后就轮到controller层了,建立LoginController,并实现相应操作
package com.wangxiaoxuan.seckill.controller;
import com.sun.deploy.net.HttpResponse;
import com.wangxiaoxuan.seckill.pojo.User;
import com.wangxiaoxuan.seckill.service.IUserService;
import com.wangxiaoxuan.seckill.vo.LoginVo;
import com.wangxiaoxuan.seckill.vo.RespBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* @author 王小轩
* @version 1.0
*/
//@RestController会给所有方法加上 @ResponseBody返回对象
@Controller
@RequestMapping("/login")
@Slf4j
public class LoginController {
@Autowired
private IUserService userService;
/**
* 跳转登录页面
* @return
*/
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
/**
* 登录
* @param
* @return
*/
@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response){
log.info("{}",loginVo);//这个实在后端控制太打印登录页面传过来的数据,可以注释掉
userService.doLogin(loginVo,request,response);//调用service层的方法,此处如果抛出异常会被拦截至相应处理类
return new RespBean().success();
}
}
最后为防止无聊提供测试页面
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- jquery-validator -->
<script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
<script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- md5.js -->
<script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<form name="loginForm" id="loginForm" method="post" style="width:50%; margin:0 auto">
<h2 style="text-align:center; margin-bottom: 20px">用户登录</h2>
<div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入手机号码</label>
<div class="col-md-5">
<input id="mobile" name="mobile" class="form-control" type="text" placeholder="手机号码" required="true"
minlength="11" maxlength="11"/>
</div>
<div class="col-md-1">
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入密码</label>
<div class="col-md-5">
<input id="password" name="password" class="form-control" type="password" placeholder="密码"
required="true" minlength="6" maxlength="16"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-5">
<button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
</div>
<div class="col-md-5">
<button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
</div>
</div>
</form>
</body>
<script>
function login() {
$("#loginForm").validate({
submitHandler: function (form) {
doLogin();
}
});
}
function doLogin() {
g_showLoading();
$.ajax({
url: "/login/doLogin",
type: "POST",
data: {
mobile: $("#mobile").val(),
password: $("#password").val()
},
success: function (data) {
layer.closeAll();
if (data.code == 200) {
layer.msg("成功");
alert("成功,欢迎");
window.location.href="/goods/toList";
} else {
layer.msg(data.message);
}
},
error: function () {
layer.closeAll();
}
});
}
</script>
</html>
至此功能实现完毕,还有可以完善的地方请多多琢磨,上述代码也解决了分布式session问题,在此贴出本人的pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wangxiaoxuan</groupId>
<artifactId>seckill_pro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>seckill_pro</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
<!--commons-pool2对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--spring-session依赖-->
<!-- <dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.4</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
以及application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=w20010125
mybatis-plus.mapper-locations=classpath:/mapper/*Mapper.xml
user.address.max-count=20
# 服务器向客户端不响应为null的属性
spring.jackson.default-property-inclusion=NON_NULL
spring.redis.host=124.223.21.132
spring.redis.port=6379
spring.redis.database=0
spring.redis.connect-timeout=10000
# server.servlet.context-path=/store
# spring.servlet.multipart.maxFileSize=10MB
# spring.servlet.multipart.maxRequestSize=10MB