Java秒杀系统实践学习——实现用户登录

<>用户登录

实现用户登录步骤:

<>1. 数据库的设计

数据库设计的字段主要是用户的手机号码、昵称、密码、salt、头像、注册时间、上次登录时间、登陆次数,详情如下:
CREATE TABLE `miaosha_user` ( `id` bigint(20) NOT NULL COMMENT '用户ID,手机号码',
`nickname` varchar(255) NOT NULL, `password` varchar(32) DEFAULT NULL COMMENT
'MD5(MD5(pass明文+固定salt) salt)', `salt` varchar(10) DEFAULT NULL, `head`
varchar(128) DEFAULT NULL COMMENT '头像,云存储的ID', `register_date` datetime DEFAULT
NULL COMMENT '注册时间', `last_login_date` datetime DEFAULT NULL COMMENT '上登录时间',
`login_count` int(11) DEFAULT '0' COMMENT '登录次数', PRIMARY KEY (`id`) )
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀用户表'; SET FOREIGN_KEY_CHECKS =
1;
<>2. 两次MD5

主要是为了安全,防止密码泄露;第一次MD5是防止用户的明文密码在网络上传输,被别人抓包获取到密码;第二次的MD5是防止在数据库被盗后密码反向破解,保证密码不会泄露。

大体执行过程:用户输入登录信息提交登录后,会在前台实现第一次MD5加密,然后会将数据库中存储随的机salt拿出来和密码拼接进行第二次MD5加密,之后会去判断是否和数据库里二次MD5密码相同。

引入依赖:
<dependency> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> </dependency> <dependency>
<groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId>
<version>3.6</version> </dependency>
创建工具包写Md5工具类(Md5Util):
第一次MD5(明文+salt):采用明文+salt的的方法进步保证密码安全性,salt是固定的方便服务器的操作,代码如下所示:
private static final String SALT = "1a2b3c4d"; public static String md5(String
src){ return DigestUtils.md5Hex(src); } //第一次MD5加密:明文+salt的混合拼接 public static
String inputPassToFormPass(String inputPass){ String src = "" + SALT.charAt(0)
+ SALT.charAt(2)+ inputPass + SALT.charAt(5)+ SALT.charAt(4); return
md5(src);//如明文密码123456经过这个加密,被别人截获解读的结果会是12123456c3 }
第二次MD5(用户输入+随机salt):采用用户输入密码+随机的salt,salt是写入数据库的,代码如下:
//第二次MDS加密:用户输入密码+随机salt public static String formPassToDbPass(String
formPass, String salt){ String src = "" + salt.charAt(0) + salt.charAt(2)+
formPass + salt.charAt(5)+ salt.charAt(4); return
md5(src);//数据库被盗后解读的密码时一次明文加密的不是真正的密码 }
直接把明文两次MD5存入数据:
//直接将用户密码转换成数据库里密码 public static String inputPassToDbPass(String inputPass,
String salt){ String formPass = inputPassToFormPass(inputPass); String dbPass =
formPassToDbPass(formPass, salt); return dbPass; }
具体实现:
在Controller包中创建LoginController类,主要包含两个方法:
to_login(通过thymeleat模板返回到src/main/resources/templates/login.html显示登录界面)
do_login(负责对提交的数据进行参数比较的操作);

login.html主要代码:
<script> function login(){ $("#loginForm").validate({
submitHandler:function(form){ doLogin(); } }); } function doLogin(){
g_showLoading(); var inputPass = $("#password").val(); var salt =
g_passsword_salt; var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass
+salt.charAt(5) + salt.charAt(4); var password = md5(str); //第一次Md5加密 $.ajax({
url: "/login/do_login", //通过ajax提交到do_login方法,有两个参数mobile和和password(值是Md5加密后的)
type: "POST", data:{ mobile:$("#mobile").val(), password: password },
success:function(data){ layer.closeAll(); if(data.code == 0){ layer.msg("成功");
window.location.href="/goods/to_list"; }else{ layer.msg(data.msg); } },
error:function(){ layer.closeAll(); } }); } </script>
do_login()中的参数比较主要是判断手机号不为空和密码不为空(StringUtils.isEmpty()方法判断)
手机号格式是否正确(建立ValidatorUtil类来判断)代码详情如下:
do_login中的参数校验代码:
if(StringUtils.isEmpty(mobile)){ return CodeMsg.MOBILE_EMPTY; }
if(StringUtils.isEmpty(password)){ return CodeMsg.PASSWORD_EMPTY; }
if(!ValidatorUtil.isMobile(mobile)){ return CodeMsg.MOBILE_ERROR; }
验证手机格式ValidatorUtil类:
public class ValidatorUtil { private static Pattern MOBILE_PATTERN =
Pattern.compile("1\\d{10}"); public static boolean isMobile(String mobile){
if(StringUtils.isEmpty(mobile)){ return false; } Matcher matcher =
MOBILE_PATTERN.matcher(mobile); return matcher.matches(); } }
在MiaoshaUserService中判断判断手机号是否为空和验证密码
在LoginVo创建对应的成员变量,在MiaoshaUserDao中写Mapper通过注解的方式用sql语句来查mobile

<>3. JSR303参数校验和全局异常处理器

优化代码,第一个优化将LoginController中do_login()的参数校验通过JSR303参数校验用注解的方式来实现。第二个优化是定义全局异常处理器将异常信息友好的显示给用户以及修改业务逻辑方法让其可以返回表达业务方法含义。

引入依赖:
<dependency> <groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-validation</artifactId> </dependency>
要实现登录,在要验证的参数前面加@valid注解也就是
@RequestMapping("/do_login") @ResponseBody public Result<CodeMsg>
doLogin(@Valid LoginVo loginVo) { log.info(loginVo.toString());
miaoshaUserService.login(loginVo); return Result.success(CodeMsg.SUCCESS);
然后需要验证的变量上加注解验证不为空、验证长度和手机号格式
public class LoginVo { @NotNull @IsMobile private String mobile; @NotNull
@Length(min=32) private String password;
验证手机号格式需要自定义一个验证器@IsMobile(参考注解@NotNull),创建IsMobile和IsMobileValidator类,代码如下:

IsMobile类:
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME) @Documented @Constraint(validatedBy =
IsMobileValidator.class ) public @interface IsMobile { boolean required()
default true; String message() default "手机号码格式不正确"; Class<?>[] groups() default
{ }; Class<? extends Payload>[] payload() default { }; }
IsMobileValidator类:
public class IsMobileValidator implements ConstraintValidator<IsMobile,
String> { private boolean required; @Override public void initialize(IsMobile
isMobile) { required = isMobile.required(); } @Override public boolean
isValid(String value, ConstraintValidatorContext context) { if (!required &&
StringUtils.isEmpty(value)) { return true; } return
ValidatorUtil.isMobile(value); } }

为了将异常信息友好的显示在浏览器页面,定义GlobalExceptionHandler.类,添加exceptionHandler方法,通过@ExceptionHandler(value
= Exception.class)
拦截所有的异常,方法体内先拦截绑定异常,返回具体错误和CodeMsg拼接完后返回,拼接调用接下来的CodeMsg类中的fillArgs方法,其他异常返回系统错误。
代码如下:
@ControllerAdvice @ResponseBody public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class) public Result<String>
handleException(HttpServletRequest request, Exception ex){
ex.printStackTrace(); if(ex instanceof GlobalException){ GlobalException gex =
(GlobalException)ex; return Result.error(gex.getCm()); } else if(ex instanceof
BindException){ BindException bex = (BindException)ex; String message =
bex.getAllErrors().get(0).getDefaultMessage(); return
Result.error(CodeMsg.BIND_ERROR.fillArgs(message)); } else { return
Result.error(CodeMsg.SERVER_ERROR); } } }

在MiaoshaUserServic类中login方法的返回值为CodeMsg类型,但是应该返回表达业务方法含义的方法,而不应该是CodeMsg类型。可以通过定义全局异常类GlobalException
进一步优化,将异常直接抛出去,交给异常处理器处理。

在GlobalException类中封装CondeMsg类型变量,供抛出时实例化使用;在GlobalExceptionHandler类修改exceptionHandler方法,增加处理GlobalException异常的逻辑。
public class GlobalException extends RuntimeException{ private static final
long serialVersionUID = 1L; private CodeMsg cm; public GlobalException(CodeMsg
cm){ this.cm = cm; } public CodeMsg getCm() { return cm; } public static long
getSerialVersionUID() { return serialVersionUID; } public void setCm(CodeMsg
cm) { this.cm = cm; } }

在MiaoshaUserService类中修改login方法返回类型为boolean,方法体内的错误直接通过实例化GlobalException类将异常抛出去;LoginController中修改doLogin方法,优化登录逻辑。
优化后的MiaoshaUserService:
public boolean login(LoginVo loginVo){ if(loginVo == null){ //return
CodeMsg.SERVER_ERROR; throw new GlobalException(CodeMsg.SERVER_ERROR); } String
mobile = loginVo.getMobile(); String password = loginVo.getPassword();
MiaoshaUser user = miaoshaUserDao.getById(Long.parseLong(mobile)); if(user ==
null){ //return CodeMsg.MOBILE_NOT_EXIST; throw new
GlobalException(CodeMsg.MOBILE_NOT_EXIST); } String salt = user.getSalt();
String dbPass = user.getPassword(); String md5Pass =
Md5Util.formPassToDbPass(password, salt); if(!dbPass.equals(md5Pass)){ //return
CodeMsg.PASSWORD_ERROR; throw new GlobalException(CodeMsg.PASSWORD_ERROR); }
return true;
优化后的登录逻辑:
@Controller @RequestMapping("/login") public class LoginController { private
static Logger log = LoggerFactory.getLogger(LoginController.class); @Autowired
UserService userService; @Autowired MiaoshaUserService miaoshaUserService;
@RequestMapping("/to_login") public String toLogin(){ return "login"; }
@RequestMapping("/do_login") @ResponseBody public Result<Boolean>
doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
log.info(loginVo.toString()); //登录 miaoshaUserService.login(response,loginVo);
return Result.success(true); }
<>4. 分布式Session

分布式Session实现是将session单独放到一个缓存中,通过redis来管理。
思路:登陆成功后,通过UUIDUtil为用户生成token标识用户,写到cookie中,传递给客户端,客户端每次都上传这个
token,服务器端根据token获取到用户信息。

定义工具类uuid:
public class UUIDUtil { public static String uuid(){ return
UUID.randomUUID().toString().replace("-", ""); } }
在MiaoshaUserService的login方法中添加:
//生成cookie String token = UUIDUtil.uuid();
redisSevice.set(MiaoshaUserKey.token, token, user); Cookie cookie = new
Cookie(COOKIE_TOKEN_NAME, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSecconds()); cookie.setPath("/");
response.addCookie(cookie); return true;
在MiaoshaUserService添加getByToke()获取user对象,通过addCookie()延长有效期
public MiaoshaUser getByToke(String token,HttpServletResponse response) {
if(StringUtils.isEmpty(token)){ return null; } //延长有效期 MiaoshaUser user =
redisSevice.get(MiaoshaUserKey.token, token, MiaoshaUser.class); if(user !=
null){ addCookie(token, response, user); } return user; } //
//延长有效期的实现,向缓存重新生成一个新cookie private void addCookie(String token,
HttpServletResponse response, MiaoshaUser user){
redisSevice.set(MiaoshaUserKey.token, token, user); Cookie cookie = new
Cookie(COOKIE_TOKEN_NAME, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSecconds()); cookie.setPath("/");
response.addCookie(cookie); }
登录成功后跳转到商品列表,对应的Controller(根据上传cookie来获取用户信息)如下:
@Controller @RequestMapping("/goods") public class GoodsController { private
static Logger log = LoggerFactory.getLogger(GoodsController.class); @Autowired
private MiaoshaUserService miaoshaUserService; @RequestMapping("/to_list")
public String toList(Model model, @CookieValue(name =
MiaoshaUserService.COOKIE_TOKEN_NAME, required = false) String cookieToken, //
@RequestParam 是为了兼容默写手机端会把cookie信息放入请求参数中 @RequestParam(name =
MiaoshaUserService.COOKIE_TOKEN_NAME, required = false) String paramToken) {
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){ return
"/login/to_login"; } String token = StringUtils.isEmpty(paramToken) ?
cookieToken : paramToken; MiaoshaUser miaoshaUser =
miaoshaUserService.getByToke(token); model.addAttribute("user", miaoshaUser);
return "goods_list"; } }
代码优化:

登录后,用户的商品信息都需要在GoodsController中获取上传的cookie,进行参数校验,优化这一部分,在GoodsController的参数传递中直接传入user对象,可通过WebMvcConfigurerAdapter的addArgumentResolvers进行实现。

WebConfig类继承WebMvcConfigurerAdapter(重写addArgumentResolvers()):
package com.example.demo.config; import
org.springframework.beans.factory.annotation.Autowired; import
org.springframework.context.annotation.Configuration; import
org.springframework.web.method.support.HandlerMethodArgumentResolver; import
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List; @Configuration public class WebConfig extends
WebMvcConfigurerAdapter{ @Autowired private UserArgumentResolver
userArgumentResolver; @Override public void
addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver); } }
UserArgumentResolver类(实现接口HandlerMethodArgumentResolver,解析user对象):
@Service public class UserArgumentResolver implements
HandlerMethodArgumentResolver{ @Autowired private MiaoshaUserService
miaoshaUserService; @Override public boolean supportsParameter(MethodParameter
parameter) { Class<?> clazz = parameter.getParameterType(); return clazz ==
MiaoshaUser.class; } @Override public Object resolveArgument(MethodParameter
parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest
request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response =
webRequest.getNativeResponse(HttpServletResponse.class); String paramToken =
request.getParameter(miaoshaUserService.COOKIE_TOKEN_NAME); String cookieToken
= getCookieValue(request, miaoshaUserService.COOKIE_TOKEN_NAME);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){ return
null; } String token = StringUtils.isEmpty(paramToken) ? cookieToken :
paramToken; return miaoshaUserService.getByToke(token, response); } private
String getCookieValue(HttpServletRequest request, String cookieName) { Cookie[]
cookies = request.getCookies(); if(cookies != null){ for(Cookie cookie :
cookies){ if(cookie.getName().equals(cookieName)){ return cookie.getValue(); }
} } return null; } }
优化后GoodsController:
@Controller @RequestMapping("/goods") public class GoodsController { private
static Logger log = LoggerFactory.getLogger(GoodsController.class); @Autowired
private MiaoshaUserService miaoshaUserService; @Autowired RedisSevice
redisSevice; @RequestMapping("/to_list") public String toLogin(Model
model,MiaoshaUser user){ model.addAttribute("user", user); return "goods_list";
} }

技术
©2019-2020 Toolsou All rights reserved,
vue vue-element-admin项目踩坑小结airflow问题系列2 —— task保持running假死状态ELementUI select多选下拉框获取选中项的全部属性[RK3399][Android7.1] 学习笔记 DRM驱动程序开发(介绍)Map 判断key对应的value值是否存在-containsKey()【Python】读取txt文件,获取指定行中指定位置数据PowerShell中使用WebClient 下载文件并获取下载进度(精华)2020年6月26日 C#类库 Enum(扩展方法)【JAVA】【华为校园招聘笔试-软件】2020-09-09mysql 递归查找父类的所有子节点