ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-06-01 20:58:07  阅读:209  来源: 互联网

标签:return String JSR303 参数检验 session 注解 login salt public


文章目录

用户登录

数据库设计

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读取到数据库中的用户信息
在这里插入图片描述

标签:return,String,JSR303,参数检验,session,注解,login,salt,public
来源: https://blog.csdn.net/weixin_44532671/article/details/117368762

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有