秒杀项目(2) 实现登录功能

文章目录

  • 一、实现登录功能
    • 1.1 数据库设计
      • 1.1.1 建立miaosha_user数据表
      • 1.1.2 MiaoshaUser实体类
      • 1.1.3 MiaoshaUserDao
    • 1.2 明文密码两次MD5处理
    • 1.3 JSR303参数校验+全局异常处理器
    • 1.4 分布式session
  • 二、明文密码两次MD5处理
    • 2.1 两次MD5
    • 2.2 两次MD5的原因
    • 2.3 引入MD5相关依赖
    • 2.4 form表单登录后信息对应实体类
    • 2.5 登录验证步骤
    • 2.6 登录验证流程
    • 2.7 LoginController
  • 三、JSR303参数校验+全局异常处理器
    • 3.1 jQuery.validator
    • 3.2 使用JSR303参数校验步骤
  • 四、异常的拦截
    • 4.1 @ControllerAdvice
    • 4.2 @ExceptionHandler(异常类型.class).
    • 4.3 参数绑定异常
    • 4.4 定义一个全局异常
  • 五、分布式session
    • 5.1 为什么需要分布式session
    • 5.2 处理方式
    • 5.3 用户登陆成功生成对应的cookie
    • 5.4 session有效期(根据token取得MiaoshaUser)
    • 5.4 查看设置的cookie
    • 5.5 session的优化
    • 5.6 登录的测试

一、实现登录功能

1.1 数据库设计

1.1.1 建立miaosha_user数据表

DROP TABLE IF EXISTS `miaosha_user`;
CREATE TABLE `miaosha_user` (
  `id` BIGINT(20) NOT NULL,
  `nickname` VARCHAR(255) NOT NULL,
  `pwd` VARCHAR(32) DEFAULT NULL,
  `salt` VARCHAR(10) DEFAULT NULL, //password的盐
  `head` VARCHAR(128) DEFAULT NULL, //头像
  `register_date` DATETIME DEFAULT NULL,  //注册时间
  `last_login_date` DATETIME DEFAULT NULL, //上次登录时间
  `login_count` INT(11) DEFAULT '0',  //登录次数
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

1.1.2 MiaoshaUser实体类

public class MiaoshaUser {
	private Long id;
	private String nickname;
	private String pwd;
	private String salt;
	private String head;
	private Date registerDate;
	private Date lastLoginDate;
	private Integer loginCount;
    .....
}

1.1.3 MiaoshaUserDao

@Mapper
public interface MiaoshaUserDao {
	@Select("select * from miaosha_user where id=#{id}")  //这里#{id}通过后面参数来为其赋值
	public MiaoshaUser getById(@Param("id") long id);    //绑定
	
	//绑定在对象上面了----@Param("id")long id,@Param("pwd")long pwd 效果一致
	@Update("update miaosha_user set pwd=#{pwd} where id=#{id}")
	public void update(MiaoshaUser toupdateuser);
	
	//public boolean update(@Param("id")long id);    //绑定
	
}

1.2 明文密码两次MD5处理

1.3 JSR303参数校验+全局异常处理器

1.4 分布式session

二、明文密码两次MD5处理

2.1 两次MD5

http在网络上是明文传输的。所以要加密

  • 用户端:密码做MD5加密(明文+固定salt),防止明文密码在网络上传输。
  • 服务端:MD5后的密码再次MD5和随机sallt存到数据里(用户输入 + 随机salt),一次MD5可能被破解。

2.2 两次MD5的原因

第一次密码与固定的salt拼接进行MD5加密得到的结果是str1,再对str1与随机的salt进行MD5加密得到真正要存到数据库的密码。并将2次MD5之后的密码和随机的salt存到数据库中。使用两次MD5的原因是,如果只有一次MD5的话,容易被彩虹表破解,两次MD5再加上随机的salt,破解密码的难度大大增大。

2.3 引入MD5相关依赖

    
      commons-codec
      commons-codec
      1.9
    
    
      org.apache.commons
      commons-lang3
      3.6
    

2.4 form表单登录后信息对应实体类

//form表单登录后拿到的经过一次md5的密码和mobile
public class LoginEntity{
    private String mobile;
    private String password;
   ......
   }

2.5 登录验证步骤

  • 登录
    form表单点击登录按钮,得到一个LoginEntity对象,LoginEntity对象有两个参数,一个是经过一次MD5后的密码,一个是手机号。该数据传递到后台服务器。
  • 普通验证
    验证手机号,密码是否为空,再验证手机号的格式是否正确(手机号的长度,开头是否为1),验证密码的格式(密码的长度是32位)。
  • 进一步验证:根据手机号得到对应的MiaoshaUser对象,判断该对象是否存在,即是不是已注册对象,判断的思路是:先从缓存中找,找到了返回,再从MiaoshaUser表中去找,找到了返回,找不到返回null
  • 2次MD5密码验证,将从LoginEntity对象得到的密码进行一次MD5,(salt是从数据库取出的),与得到的MiaoshaUser对象的密码进行验证,返回验证结果。

2.6 登录验证流程

  • /login/to_login 进步到登录页面
  • /login/do_login 在这里面进行登录验证
  • /goods/to_list 成功后进行跳转

对应前端代码:

	$.validator.setDefaults( {
			submitHandler: function () {
				var pass=$("#password").val();
				//pass='111111';
				var salt='1a2b3c4d';
				var str=""+salt.charAt(0)+salt.charAt(2)+pass+salt.charAt(5)+salt.charAt(4);
				var password=md5(str);
				//alert(salt);
				//alert(pass);
				//alert(password);
				//与后台Md5规则一致
				//var str=""+salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
				$.ajax({
					url:"/login/do_login",
            type:"POST",
            data:{
                mobile:$("#phone").val(),
                password:password,
            },
            success:function(data){
                if(data.code==0){
                    alert("success");
                    //成功后跳转
                    window.location.href="/goods/to_list";
                }else{
                    alert(data.msg);
                }
            },
            error:function(data){
                alert("error");
                //alert(data.msg);
            }
        });

2.7 LoginController

负责登录的验证

@Controller
public class LoginController {
    @Autowired
    MiaoShaUserService miaoShaUserService;
    @RequestMapping("/login/to_login")
    public String doLogin(){
        return "login";
    }
    @RequestMapping("/login/do_login")
    @ResponseBody
    public Result judge_Login(HttpServletResponse response, LoginEntity loginEntity){
        Result login = miaoShaUserService.Login(response, loginEntity);
        return login;
    }
}

三、JSR303参数校验+全局异常处理器

3.1 jQuery.validator

jQuery.validator 验证规则详解

3.2 使用JSR303参数校验步骤

  • 加入依赖
  • 在要校验的参数前加@Valid
	public Result doLogin(HttpServletResponse response,@Valid LoginVo loginVo)
  • 对要验证的参数加上对应的注解内容
public class LoginVo {
	private String mobile;
	private String password;
	@NotNull
	@IsMobile
	public String getMobile() {
		return mobile;
	}
	
	public void setMobile(String mobile) {
		this.mobile = mobile;
	}
	@NotNull
	@Length(min=32)
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
}
  • 可能需要我们自定义注解
@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[] payload() default { };
}
public class IsMobileValidator implements ConstraintValidator{

	private boolean required=false;
	//是否可以为空
	public void initialize(IsMobile constraintAnnotation) {
		constraintAnnotation.required();
	}

	public boolean isValid(String value, ConstraintValidatorContext context) {
		if(required) {//查看值是否是必须的
			return ValidatorUtil.isMobile(value);
		}else {
			if(StringUtils.isEmpty(value)) {//required
				return true;
			}else {
				return ValidatorUtil.isMobile(value);
			}
		}
	}
}

四、异常的拦截

4.1 @ControllerAdvice

它能够让你统一在一个地方处理应用程序所发生的所有异常。
而不是是在单个的控制器中分别处理。

4.2 @ExceptionHandler(异常类型.class).

这个注解的功能是:自动捕获controller层出现的指定类型异常,并对该异常进行相应的异常处理.

4.3 参数绑定异常

  • 判断异常是不是参数绑定异常
  • 通过getAllErrors()获得异常信息数组
  • 获得异常信息数组的第一个error对象
  • 通过fillArgs(msg)方法与错误信息链接返回
if(e instanceof BindException) {//是绑定异常的情况
			//强转
			BindException ex=(BindException) e;
			//获取错误信息
			List 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);
		}

4.4 定义一个全局异常

public class GlobalException extends RuntimeException{

	private static final long serialVersionUID = 1L;
	private CodeMsg cm;
	public GlobalException(CodeMsg cm){
		super(cm.toString());
		this.cm=cm;
		
	}
	public CodeMsg getCm() {
		return cm;
	}
	public void setCm(CodeMsg cm) {
		this.cm = cm;
	}
	
}

有异常直接抛出异常,全局处理器拦截到异常会去处理异常

MiaoshaUser user=getById(id);
		if(user==null) {
			throw new GlobalException(CodeMsg.MOBILE_NOTEXIST);
		}

五、分布式session

5.1 为什么需要分布式session

用户的第一个请求落到了第一个服务器,用户的第二个请求落到了第二个服务器,用户的session信息就会丢失

5.2 处理方式

  • session同步
    性能有问题,需要同步到多台服务器上
  • 登录成功,生成类似sessionid的东西,存到cookie中,传递到客户端,并且以sessionID作为key,MiaoshaUser作为value存到缓存中,客户端在随后的访问中,从request中取到sessionID,根据sessionID从缓存中获取到对应的用户信息。
  • cookie需要设置有效期
  • @CookieValue获取cookie对象,cookie也可能从参数中获取,这时需要使用@PathVarible。我们可以设置取cookie的优先级,先从url路径中去取,再从请求体中去获得cookie。

5.3 用户登陆成功生成对应的cookie

登录成功后,生成token,与对应的user存到redis中,再把token添加进cookie中。

  • 通过cookie拿到sessionID,根据sessiodID在redis中取到对应的MiaoshaUser
public Result Login(HttpServletResponse response, LoginEntity loginEntity) {
    .................
    //添加sessionID信息
    public void addCookie(HttpServletResponse response, MiaoshaUser miaoshaUser, String token) {
        service.set(MiaoshaUserKey.token, token, miaoshaUser);
        Cookie cookie = new Cookie(COOKIE_NAME, token);
        cookie.setMaxAge(MiaoshaUserKey.token.getExpireTime());
        cookie.setPath("/");
        response.addCookie(cookie);
    }

5.4 session有效期(根据token取得MiaoshaUser)

  • 进入商品列表之前,需要判断用户是否已经登陆,就会根据token去取用户的信息,如果用户已经登陆则可以延长token有效期。
    最后一次访问的时间加上过期时间
  • 从redis取得时候,延长有效期
 //根据sessionID从数据库中取出对应的MiaoshaUSser
    public MiaoshaUser getByToken(String token, HttpServletResponse response) {
        //token的有效期是最后一次登录加上token的寿命
        if (!StringUtils.isEmpty(token)) {
            return null;
        }
        MiaoshaUser miaoshaUser = service.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
        //重回设置token的有效期
        if (miaoshaUser != null) {
            addCookie(response, miaoshaUser, token);
        }
        return miaoshaUser;
    }

5.4 查看设置的cookie

秒杀项目(2) 实现登录功能_第1张图片

5.5 session的优化

  • 背景:每进入到一个新的页面就需要经历从reqest中或者url参数中取得cookie,根据cookie从缓存中取得对象信息,以此来判断用户是否已经登陆。比较麻烦
  • 解决方法:直接将需要判断的MiaoshaUser对象传到参数中

先创建:UserArgumentResolver 类

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    @Autowired					//既然能注入service,那么可以用来容器来管理,将其放在容器中
    MiaoShaUserService miaoShaUserService;


    public Object resolveArgument(MethodParameter arg0, ModelAndViewContainer arg1, NativeWebRequest webRequest,
                                  WebDataBinderFactory arg3) throws Exception {
        HttpServletRequest request=webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response=webRequest.getNativeResponse(HttpServletResponse.class);
        String paramToken=request.getParameter(MiaoShaUserService.COOKIE_NAME);
        System.out.println("@UserArgumentResolver-resolveArgument  paramToken:"+paramToken);
        //获取cookie
        String cookieToken=getCookieValue(request,MiaoShaUserService.COOKIE_NAME);
        System.out.println("@UserArgumentResolver-resolveArgument  cookieToken:"+cookieToken);
        if(StringUtils.isEmpty(cookieToken)&& StringUtils.isEmpty(paramToken))
        {
            return null;
        }
        String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
        //System.out.println("goods-token:"+token);
        //System.out.println("goods-cookieToken:"+cookieToken);
        MiaoshaUser user=miaoShaUserService.getByToken(token,response);
        System.out.println("@UserArgumentResolver--------user:"+user);

        //去取得已经保存的user,因为在用户登录的时候,user已经保存到threadLocal里面了,因为拦截器首先执行,然后才是取得参数
        //MiaoshaUser user=UserContext.getUser();
        return user;
    }

    public String getCookieValue(HttpServletRequest request, String cookie1NameToken) {//COOKIE1_NAME_TOKEN-->"token"
        //遍历request里面所有的cookie
        Cookie[] cookies=request.getCookies();
        if(cookies!=null) {
            for(Cookie cookie :cookies) {
                if(cookie.getName().equals(cookie1NameToken)) {
                    System.out.println("getCookieValue:"+cookie.getValue());
                    return cookie.getValue();
                }
            }
        }
        System.out.println("No getCookieValue!");
        return null;
    }
    public boolean supportsParameter(MethodParameter parameter) {
        //返回参数的类型
        Class clazz=parameter.getParameterType();
        return clazz==MiaoshaUser.class;
    }
}

再配置mvc:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Autowired
    UserArgumentResolver userArgumentResolver;
    /*@Autowired
    AccessInterceptor accessInterceptor;
*/
    /**
     * 设置一个MiaoshaUser参数给,toList使用
     */
    @Override
    public void addArgumentResolvers(List argumentResolvers) {
        //将UserArgumentResolver注册到config里面去
        argumentResolvers.add(userArgumentResolver);
    }

    /**
     * 注册拦截器
     */
    /*@Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册
        //registry.addInterceptor(accessInterceptor);
        super.addInterceptors(registry);
    }*/
}

5.6 登录的测试

前端代码:



  

    
    AA


后端代码:

@Controller
@RequestMapping("/goods")
public class GoodsControler {
    @Autowired
    MiaoShaUserService miaoShaUserService;
    //这里防止通过商品叶页面的url,进入商品页面
    @RequestMapping("/to_list")
    public String to_login(HttpServletResponse response, Model model/* @CookieValue(name = MiaoShaUserService.COOKIE_NAME, required = false)String token,
                           @PathVariable(name = MiaoShaUserService.COOKIE_NAME,required = false)String token1*/,MiaoshaUser user){
        //得不到任何的cookie信息,则说明未登陆过,返回到登录界面
       /* if(StringUtils.isEmpty(token)&&StringUtils.isEmpty(token1)){
            return "login";
        }
        String realToken = StringUtils.isEmpty(token1) ? token : token1;
        MiaoshaUser byToken = miaoShaUserService.getByToken(token, response);
        System.out.println("拿到token对应的user");*/
        model.addAttribute("user",user);
        return "user_judge";
    }
}

结果:
秒杀项目(2) 实现登录功能_第2张图片

你可能感兴趣的:(项目)