您好,欢迎访问代理记账网站
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

秒杀系统 - 实现用户登录(两次MD5,JSR303参数检验,全局异常处理器)和分布式session功能

文章目录

  • 用户登录
    • 数据库设计
    • 明文密码两次MD5处理
      • 加密思路
      • 安全性
      • 加密过程
        • 导入MD5依赖
        • 封装MD5Utils
      • 实现登录页面
    • JSR303参数检验
      • 简介
      • 流程
        • 导入依赖
        • 常用注解
        • 自定义注解
      • 测试
    • 全局异处理器
      • 原因
      • 流程
        • GlobalException类
        • GlobalExceptionHandler类
        • 接口
      • 测试
  • 分布式session
    • session和 cookie
    • 流程
      • 写login接口
      • miaoshaUserService类
      • 登录成功跳转到list
      • 通过session信息查用户
      • 新增HandlerMethodArgumentResolver
    • 测试结果

用户登录

数据库设计

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL COMMENT '用户ID,手机号码',
  `nickname` varchar(255) NOT NULL COMMENT '昵称',
  `password` varchar(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt) + salt)',
  `salt` varchar(10) DEFAULT NULL COMMENT '第二次加密的salt',
  `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

明文密码两次MD5处理

加密思路

http在网络是以明文来传输的,数据包可能被劫持,是不安全的。
第一次加密是客户端发送请求之前, 采用的是固定salt,MD5(密码+固定salt) ,加密后发送请求传给服务端(加盐混淆密码,MD5明文转为密文)。
第二次加密是写到数据库之前,要先生成一个随机salt,MD5(密码+随机salt) ,把随机salt和加密结果同时存数据库。

安全性

两次加密增加了破解难度,并不是无法破解的。
如果想更安全,可以采用https,浏览器插件ActiveX(网银的那些安全控件),控件破解难度高,相对来说更安全。二通过js是无法做到数据安全的,以为js本身都是明文。

加密过程

导入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>

封装MD5Utils

MD5Utils类用于加密。

public class MD5Util  {
	//固定salt
    private static final String salt = "1d2c3b4a";
    public static String md5(String src){
        return DigestUtils.md5Hex(src);
    }
	//第一次加密
    public static String inputToForm(String input){
        String str = salt.charAt(0) + salt.charAt(2) + input + salt.charAt(4) + salt.charAt(5);
        return(md5(str));
    }
	//第二次加密
    public static String formToDB(String form,String salt){
        String str = ""+salt.charAt(0)+salt.charAt(2)+form + salt.charAt(3) + salt.charAt(5);
        return md5(str);
    }
	//两次加密
    public static String inputToDB(String input,String salt){
        String formPass = inputToForm(input);
        return formToDB( formPass,salt );
    } 
}

测试代码

    public static void main(String[] args){
        String i = "214143";
        System.out.println(md5(i));
        System.out.println(inputToForm(i));
        System.out.println(formToDB( inputToForm(i) ,"12345678"));
        System.out.println( inputToDB(i,"12345678") );
    }

测试结果
在这里插入图片描述

实现登录页面

bootstrap画页面。
jquery-validation做form表单验证。
layer做弹框。
md5.js做md5加密。
在这里插入图片描述
直接用教程的演示代码了。

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head></head>
<body> 
<form name="loginForm" id="loginForm" method="post"  style="width:30%; 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-8">
                <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-8">
                <input id="password" name="password" class="form-control" type="password"  placeholder="密码" required="true" minlength="6" maxlength="16" />
            </div>
        </div>
    </div>

    <div class="row" style="margin-top:40px;">
        <div class="col-md-6">
            <button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
        </div>
        <div class="col-md-6">
            <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();
        var inputPass = $("#password").val();
        var salt = "1d2c3b4a";
        var str = salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(4) + salt.charAt(5);
        var password = md5(str); 
        $.ajax({
            url: "/login/do_login",
            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>
</html>

Controller加loginController类,这样浏览器访问 http://localhost:8080/login/to_login 就可以看到登录页。

@Controller
@RequestMapping("/login")
public class LoginController { 
    @RequestMapping("/to_login")
    public String toLogin() {
        return "login";
    } 
}

在这里插入图片描述

JSR303参数检验

简介

前端传过来的字段如何在后台做效验,最老的方法就是if else,但显得不是很灵活。如果前端传来100个字段就得写许多多余的代码。
可以在后台创建的实体和前端传来的字段做对应映射,加上JSR303注解来做灵活的效验。

JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

流程

导入依赖

	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

常用注解

验证信息的常用注解主要有:

@Null	限制只能为null
@NotNull	限制必须不为null
@AssertFalse	限制必须为false
@AssertTrue	限制必须为true
@DecimalMax(value)	限制必须为一个不大于指定值的数字
@DecimalMin(value)	限制必须为一个不小于指定值的数字
@Digits(integer,fraction)	限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future	限制必须是一个将来的日期
@Max(value)	限制必须为一个不大于指定值的数字
@Min(value)	限制必须为一个不小于指定值的数字
@Past	限制必须是一个过去的日期
@Pattern(value)	限制必须符合指定的正则表达式
@Size(max,min)	限制字符长度必须在min到max之间
@Past	验证注解的元素值(日期类型)比当前时间早
@NotEmpty	验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank	验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email	验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 

自定义注解

我们可以自定义isMobile注解,检查是否符合手机号的格式。
IsMobile 注解:

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) //适用范围:方法,遍历,注解,构造方法,方法参数
@Retention(RUNTIME)  //运行期间保留
@Documented         //文档显示注解
@Constraint(validatedBy = {IsMobileValidator.class })  //通过自定义IsMobileValidator类注解约束
public @interface  IsMobile { 
	boolean required() default true;  
	String message() default "手机号码格式错误"; // 约束注解验证时的输出消息
	Class<?>[] groups() default { }; // 约束注解在验证时所属的组别
	Class<? extends Payload>[] payload() default { };// 约束注解的有效负载
}

IsMobile 注解关联的验证器IsMobileValidator类,继承了ConstraintValidator接口,需要指定两个参数,第一个自定义注解类,第二个为需要校验的数据类型。
实现接口后要override两个方法,分别为initialize方法和isValid方法。方法 initialize 对验证器进行实例化,它必须在验证器的实例在使用之前被调用,并保证正确初始化验证器,isValid方法就是我们最终需要的校验方法了。

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
	private boolean required = false;
	public void initialize(IsMobile constraintAnnotation) {
		required = constraintAnnotation.required();
	}
	public boolean isValid(String value, ConstraintValidatorContext context) {
		if(required) {
			return ValidatorUtil.isMobile(value);
		}else {
			if(StringUtils.isEmpty(value)) {
				return true;
			}else {
				return ValidatorUtil.isMobile(value);
			}
		}
	}
} 

ValidatorUtil类,用来检查是否符合手机号格式:

public class ValidatorUtil { 
	private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}"); //表示1开头,并且后面跟10个数字。
	public static boolean isMobile(String src) {
		if(StringUtils.isEmpty(src)) {
			return false;
		}
		Matcher m = mobile_pattern.matcher(src);
		return m.matches();
	} 
}

现在只要加上@Valid注解,就可以吧参数校验部分删除了。
在这里插入图片描述
在登陆的时候,会抛出BindException的异常,可以在控制台看到,但是doLogin方法没办法把Result.error(CodeMsg)返回,页面也就看不到“密码错误”的报错信息了。

测试

请求参数前加注解@Valid,表示我们对这个对象属性需要进行验证
在这里插入图片描述
在LoginVo类中加注解(这里爆红是应为截图的时候还没写好IsMobile注解)。
在这里插入图片描述

全局异处理器

原因

最常见的异常处理方式,就是使用try{}catch。一个Controller下面,满屏幕的try{}catch,看着一点都不优雅,所以可以对所有异常实施统一处理的方案。

流程

在这里插入图片描述

GlobalException类

自定义全局异常类,数据格式不规范会被我们手动抛出。
serialVersionUID

public class GlobalException extends RuntimeException{ 
	private static final long serialVersionUID = 1L;  //序列化id
	private CodeMsg codeMsg; 
	public GlobalException(CodeMsg codeMsg) {
		super(codeMsg.toString());
		this.codeMsg= codeMsg;
	} 
	public CodeMsg getCodeMsg () {
		return codeMsg;
	} 
}

GlobalExceptionHandler类

用来捕获,处理异常。

@ControllerAdvice  //表示实现:全局异常处理,全局数据绑定,全局数据预处理
@ResponseBody  //java对象转为json格式的数据
public class GlobalExceptionHandler {
	@ExceptionHandler(value=Exception.class)  //所有异常都拦截
	public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
		e.printStackTrace();
		if(e instanceof GlobalException) {
			GlobalException ex = (GlobalException)e;  //全局异常
			return Result.error(ex.getCm());
		}else if(e instanceof BindException) {        //绑定异常
			BindException ex = (BindException)e;
			List<ObjectError> errors = ex.getAllErrors();  //参数校验可能有很多错误,这里只返回第一个。
			ObjectError error = errors.get(0);
			String msg = error.getDefaultMessage();
			return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
		}else {
			return Result.error(CodeMsg.SERVER_ERROR);//其他异常一律视为服务器错误
		}
	}
}

接口

有了全局异常处理器,接口就可以写的更清晰了,直接执行userService.login函数,如果有问题直接抛出异常,然后被全局异常处理器捕获,并进一步处理。
接口:

    @RequestMapping("/do_login")
    @ResponseBody
    public Result<Boolean> doLogin(@Valid LoginVo loginVo) { 
        userService.login(loginVo);
        return Result.success(true);
    }

userService.login函数:

	public void login(LoginVo loginVo){
		if(loginVo == null){
			throw new GlobalException( CodeMsg.SERVER_ERROR );
		}
		String mobile = loginVo.getMobile();
		String password = loginVo.getPassword();``
		MiaoshaUser miaoshaUser = getById(Long.parseLong(mobile ));
		if( miaoshaUser == null ){
			throw new GlobalException( CodeMsg.MOBILE_NOT_EXIST );
		}
		//验证密码
		String dbPass = miaoshaUser.getPassword();
		String salt = miaoshaUser.getSalt();
		String pass = MD5Util.formToDB(password,salt);
		if(pass.equals(dbPass)){
			throw new GlobalException( CodeMsg.SUCCESS );
		}else{
			throw new GlobalException( CodeMsg.PASSWORD_ERROR );
		}
	}

测试

可以看到异常已经处理好了。
在这里插入图片在这里插入图片描述描述

分布式session

session和 cookie

  1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制(Session)来识具体的用户。 典型的场景比如购物车,当你点击下单按钮时,并不知道是哪个用户操作的,所以服务端要为特定的用户创建特定的Session,用用于标识这个用户。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
  2. 每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。如果客户端的浏览器禁用了 Cookie,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
  3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。

简单来说,用户登陆之后,服务端给用户生成一个sessionid(token)来标识用户,写到cookie中传递客户端,客户端在随后访问中在cookie中上传token,服务端拿到token从而辨别出唯一的用户。

流程

写login接口

Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。
request和response对象即然代表请求和响应,那我们要获取客户机提交过来的数据,只需要找request对象就行了。要向客户机输出数据,只需要找response对象就行了。
登录的时候因为要给客户机发送session信息,所以多传一个参数HttpServletResponse 。

	//跳转到login页面
 	@RequestMapping("/to_login")
    public String toLogin() {
        return "login";
    }

	//登录接口
    @RequestMapping("/do_login")
    @ResponseBody
    public Result<Boolean> doLogin(HttpServletResponse response,@Valid LoginVo loginVo) {
        miaoshaUserService.login(response ,loginVo);
        return Result.success(true);
    }

miaoshaUserService类

getById(): 通过id从mysql中查询用户信息
getByToken(): 通过token从redis中查询用户信息,并且把生成的Cookie放入HttpServletResponse 。
login(): 参数校验,如果密码不对就抛异常,会被全局异常捕获识别。
addCookie(): redis中存入键值对(session, user实体),同时给HttpServletResponse 赋值session信息,存活时间,根目录。

@Service
public class MiaoshaUserService {
	@Autowired
	MiaoshaUserDao miaoshaUserDao;
	@Autowired
	RedisService redisService;

	public static final String COOKI_TOKEN_NAME = "name"; 
	public MiaoshaUser getById(Long id){
		return miaoshaUserDao.getById(id);
	} 
	
	public MiaoshaUser getByToken(HttpServletResponse response,String token){
	    if(StringUtils.isEmpty(token)){
	        return null;
        }
	    MiaoshaUser user = redisService.get(MiaoshaUserKey.token,token,MiaoshaUser.class);
	    //延长有效期
        if(user != null){
            addCookie(response,user);
        }
        return user;
    }

	public boolean login(HttpServletResponse response, LoginVo loginVo){
		if(loginVo == null){
			throw new GlobalException( CodeMsg.SERVER_ERROR );
		}
		String mobile = loginVo.getMobile();
		String password = loginVo.getPassword();
		MiaoshaUser miaoshaUser = getById(Long.parseLong(mobile ));
		if( miaoshaUser == null ){
			throw new GlobalException( CodeMsg.MOBILE_NOT_EXIST );
		}
		//验证密码
		String dbPass = miaoshaUser.getPassword();
		String salt = miaoshaUser.getSalt();
		String pass = MD5Util.formToDB(password,salt);
		if(!pass.equals(dbPass)){
			throw new GlobalException( CodeMsg.PASSWORD_ERROR );
		}
		//生成cookie
        addCookie(response,miaoshaUser);
		return true;
	}
	
	private void  addCookie(HttpServletResponse response,MiaoshaUser miaoshaUser){
        String token = UUIDUtil.uuid();
        redisService.set(MiaoshaUserKey.token,token,miaoshaUser);
        Cookie cookie = new Cookie(COOKI_TOKEN_NAME,token);
        cookie.setMaxAge(MiaoshaUserKey.token.getExpireSeconds());
        cookie.setPath("/"); //设置根目录
        response.addCookie(cookie);
    } 
}

登录成功跳转到list

ajax请求成功之后,会跳转到/goods/to_list接口。

$.ajax({
    url: "/login/do_login",
    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();
    }
 });

通过session信息查用户

对应的/goods/to_list接口:

	@RequestMapping("/to_list")
	public String toList(HttpServletResponse response,Model model,
						  @CookieValue(value = MiaoshaUserService.COOKI_TOKEN_NAME,required = false) String cookieToken,
						  @RequestParam(value = MiaoshaUserService.COOKI_TOKEN_NAME,required = false) String paramToken
	){
		if(StringUtils.isEmpty(cookieToken)&& StringUtils.isEmpty(paramToken))
			return "login";
		String token = StringUtils.isEmpty(paramToken)? cookieToken:paramToken;//paramToken优先
		MiaoshaUser miaoshaUser = userService.getByToken(response,token); //根据token信息查出miaoshaUser详细信息
		model.addAttribute("user",miaoshaUser);
		return "goods_list";
	}

但是这种写法比较复杂,如果其他部分也需要通过session查到miaoshaUser,就会重复写很多类似的语句。

新增HandlerMethodArgumentResolver

改进写法如下,也就是省略HttpServletResponse,String cookieToken, String paramToken 这三个参数。多了MiaoshaUser 这个参数。

改进步骤1:
WebConfig.java addArgumentResolvers是给controller方法的参数赋值的。上文的toList方法有参数MiaoshaUser,遍历参数,如果有MiaoshaUser类,我们就通过addArgumentResolvers方法给他赋值。

@Configuration
public class WebConfig  extends WebMvcConfigurerAdapter { 
	@Autowired
	UserArgumentResolver userArgumentResolver; 
	@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
		argumentResolvers.add(userArgumentResolver);
	}

}

改进步骤2:
UserArgumentResolver类实现HandlerMethodArgumentResolver 接口,重写两个方法supportsParameter resolveArgument 。supportsParameter用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。而resolveArgument就可以放我们之前toList()方法中的逻辑。

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

	@Autowired
	MiaoshaUserService userService;

	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> clazz = parameter.getParameterType();
		return clazz== MiaoshaUser.class;
	}

	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.COOKI_TOKEN_NAME);
		String cookieToken = getCookieValue(request,MiaoshaUserService.COOKI_TOKEN_NAME);
		if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
			return null;
		}
		String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
		return userService.getByToken(response, token);
	}

	private String getCookieValue(HttpServletRequest request, String cookiTokenName) {
		Cookie[] cookies = request.getCookies();
		for(Cookie cookie : cookies){
			if(cookie.getName().equals(cookiTokenName)){
				return cookie.getValue();
			}
		}
		return null;
	} 
}

改进完成,现在代码就精简许多了。

	@RequestMapping("/to_list")
	public String toList(Model model,MiaoshaUser miaoshaUser
	){
		model.addAttribute("user",miaoshaUser);
		return "goods_list";
	}

测试结果

goods_list.html如下:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head></head>
<body>
<p th:text = "'nickname,'+${user.nickname}"></p>
<p th:text = "'id,'+${user.id}"></p>
<p th:text = "'password,'+${user.password}"></p>
<p th:text = "'salt,'+${user.salt}"></p>
<p th:text = "'head,'+${user.head}"></p>
<p th:text = "'registerDate,'+${user.registerDate}"></p>
<p th:text = "'lastLoginDate,'+${user.lastLoginDate}"></p>
<p th:text = "'loginCount,'+${user.loginCount}"></p> 
</body>
</html> 

可以看到已经通过session读取到数据库中的用户信息
在这里插入图片描述


分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进