Java秒杀项目——用户登录

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";

    }


}

慕课网Java高并发秒杀(课程) 很好的spring,springMVC,mybatis,bootstrap,jQuery,mysql,Restful学习案例 SQL脚本 CREATE DATABASE seckill; USE seckill; -- todo:mysql Ver 5.7.12for Linux(x86_64)中一个表只能有一个TIMESTAMP CREATE TABLE seckill( `seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID', `name` VARCHAR(120) NOT NULL COMMENT '商品名称', `number` int NOT NULL COMMENT '库存数量', `start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间', `end_time` DATETIME NOT NULL COMMENT '秒杀结束时间', `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (seckill_id), key idx_start_time(start_time), key idx_end_time(end_time), key idx_create_time(create_time) )ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表'; -- 初始化数据 INSERT into seckill(name,number,start_time,end_time) VALUES ('3000元秒杀iphone6',100,'2016-01-01 00:00:00','2016-12-31 00:00:00'), ('2000元秒杀ipad',100,'2016-01-01 00:00:00','2016-05-01 00:00:00'), ('6000元秒杀mac book pro',100,'2016-07-01 00:00:00','2016-12-31 00:00:00'), ('7000元秒杀iMac',100,'2016-05-01 00:00:00','2016-12-31 00:00:00') -- 秒杀成功明细表 -- 用户登录认证相关信息(简化为手机号) CREATE TABLE success_killed( `seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID', `user_phone` BIGINT NOT NULL COMMENT '用户手机号', `state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货', `create_time` TIMESTAMP NOT NULL COMMENT '创建时间', PRIMARY KEY(seckill_id,user_phone),/*联合主键*/ KEY idx_create_time(create_time) )ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表' SHOW CREATE TABLE seckill\G;#显示表的创建信息 Mybatis两个问题?①sql写在哪里?②怎么实现DAO接口?第一个问题:注解或者XML选择XML.第二个问题:Mapper自动实现DAO接口或者API编程方式实现DAO接口.选择Mapper.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值