java秒杀高并发------用户登录功能实现 分布式Session 异常处理器 根据token获取用户信息

使用两次MD5

1.用户端:PASS = MD5(明文+固定Salt) 防止用户明文密码在网络中传输

2.服务端:PASS = MD5(用户输入+随机Salt) 防止被脱裤

引入MD5工具类,添加MD5Util

<!--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>

MD5Util

public class MD5Util {
    public static String md5(String src){
        return DigestUtils.md5Hex(src);
    }

    private static final String salt ="1a2b3c4d";

    //这个是在网络中传输的,哪怕被截获到了。反向获取的话还是找不到真正的密码的
    public static String inputPassToFormPass(String inputPass){

        //拼个串在做md5.当然这个拼接自定义的。
        String str = ""+salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
        return md5(str);
    }
    //把传上来的密码加上随机 salt再次md5存入mysql
    public static String formPassToDBPass(String formPass,String salt){
        String str = ""+salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
        return md5(str);
    }
    //把明文二次MD5为存入数据库
    public static String inputPassToDbPass(String input,String saltDB){
        String formPass = inputPassToFormPass(input);
        return formPassToDBPass(formPass,saltDB);

    }

这里写图片描述

使用 JSR303参数校验

先pom依赖

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

先在参数中加上注解

doLogin(@Valid LoginVo loginVo)

在实体类中校验

@NotNull
private String mobile;

@NotNull
@Length(min=32)
private String 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<? extends Payload>[] payload() default { };
}

再定义一个真正校验的
IsMobileValidator.class

要实现 ConstraintValidator 接口

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    private boolean required = false;
    //获取注解中的required(这个是不是必须的意思)
    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);
            }
        }
    }

现在这样会有异常,定义一个全局异常拦截

拦截所有异常,如果是绑定异常就返回对应的错误
如果是其他异常就返回服务端错误

异常处理器

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    @ExceptionHandler(value = Exception.class)//拦截什么异常就写什么
    public Result<String> exceptionHandler(HttpServletRequest request,Exception e){
        //拦截绑定异常
        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);

        }

    }

之后我们就可以抛出异常然后让其捕获就可以了

我们创建一个全局异常,然后抛出,可以在上面捕获在进行指定输出

public class GlobalException extends RuntimeException {

    private static final long serialVersionUID=1L;

    private CodeMsg cm;

    public GlobleException(CodeMsg cm){
        super(cm.toString());
        this.cm = cm;

    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public CodeMsg getCm() {
        return cm;
    }

    public void setCm(CodeMsg cm) {
        this.cm = cm;
    }
}

分布式Session

登录成功 给这个用户生成一个UUID,也就是token,传递给客户端。
客户端每次都上传这个 token就能获取到用户信息了。

这个token对应哪个 用户信息存到 redis中

将信息存到第三方缓存中

cookie增加成功,添加成功 token

这里写图片描述
但是没有上传上去,因为没有设置有效时间

获取 过去时间和前缀的。

public class MiaoshaUserKey extends BasePrefix{
    public static int TOKEN_EXPIRE = 3600*24*2;//设置token过期时间为2天

    public MiaoshaUserKey(int expireSeconds,String prefix) {
        super(expireSeconds,prefix);
    }
    public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE,"tk");
}

登录成功后获取 token,在根据token去redis中读取 用户信息,再延长token过期时间

控制器

@RequestMapping("/to_list")
public String toLogin(Model model,
                      @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false)String cookieToken,
                      @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false)String paramToken,
                      HttpServletResponse response){
    if (StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
        return "do_login";
    }
    //优先取cookie中的token,两种都取是因为可能手机端是发的参数而不是cookie中。required = false 表示可以传值为空
    String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
    MiaoshaUser user =userService.getByToken(response,token);

    model.addAttribute("user",user);

    return "to_list";
}

Service

@Service
public class MiaoshaUserService {

    public static final String COOKI_NAME_TOKEN = "token";

    @Autowired
    MiaoshaUserDao miaoshaUserDao;

    @Autowired
    RedisService redisService;


    public MiaoshaUser getByMobile(String mobile) {
        return miaoshaUserDao.getByMobile(mobile);
    }


    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, token, user);
        }
        return user;
    }


    public boolean login(HttpServletResponse response, LoginVo loginVo) {
        if(loginVo == null) {
            throw new GlobleException(CodeMsg.SERVER_ERROR);
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        //判断手机号是否存在
        MiaoshaUser user = getByMobile(mobile);
        if(user == null) {
            throw new GlobleException(CodeMsg.MOBILE_NOT_EXIST);
        }
        //验证密码
        String dbPass = user.getPassword();
        String saltDB = user.getSalt();
        String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
        if(!calcPass.equals(dbPass)) {
            throw new GlobleException(CodeMsg.PASSWORD_ERROR);
        }
        //生成cookie
        String token    = UUIDUtil.uuid();
        //随机生成 token加入到cookie中
        addCookie(response, token, user);
        return true;
    }

    private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
        //存到redis中,key是 tk:token值,值是序列化的用户
        redisService.set(MiaoshaUserKey.token, token, user);
        Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
        //设置cookie过期时间为toeken过期时间
        cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
        cookie.setPath("/");
        response.addCookie(cookie);
    }

redis中的key

这里写图片描述

将根据token获取用户信息分离出来

写一个配置类

重写 addArgumentResolvers方法

这个方法就是给 控制器中的定义的参数赋值
先定义
UserArgumentResolver

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver{

    @Autowired
    MiaoshaUserService userService;

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        //如果参数类型是这个实体类返回 true,才能执行下面的那个函数
        Class<?> clazz = methodParameter.getParameterType();
        return clazz== MiaoshaUser.class;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {

        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response =nativeWebRequest.getNativeResponse(HttpServletResponse.class);

        String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
        String cookieToken = getCookieValue(request,MiaoshaUserService.COOKI_NAME_TOKEN);
        if(StringUtils.isEmpty(cookieToken)&& StringUtils.isEmpty(paramToken)){
            return null;
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = userService.getByToken(response, token);
        return user;
    }

    private String getCookieValue(HttpServletRequest request, String cookieName) {

        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(cookieName)) {
                return cookie.getValue();
            }
        }
        return null;
    }


}

再在配置类中注入

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{

    @Autowired
    UserArgumentResolver userArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(userArgumentResolver);
    }
}

这么写后就不用在控制器中做判断了

控制器方法就可以写成这样了。
因为重写了对参数赋值的方法,如果在cookie中或者参数中找到了 token就直接从 redis中获取并
赋值给参数中的 user

@RequestMapping("/to_list")
public String toLogin(Model model,
                     MiaoshaUser user) {
    model.addAttribute("user", user);

    return "goods_list";
}

这样如果修改cookie获取方式直接修改这个地方就行,不需要修改业务代码。

分布式Session就是生成token对应用户,存在redis中。
根据token获取用户,获取出信息。
这个地方重写了框架的方法,将这些获取写到了这个方法里,
与业务分离,还有对redis的操作,对每个业务的前缀的设置+过期时间。

期间遇到了一个问题

springboot整合mybatis mapper注入时显示could not autowire的解决

参考
https://blog.csdn.net/qq_21853607/article/details/72802080

不然总是报错说没有注入

猜你喜欢

转载自blog.csdn.net/qq_28295425/article/details/80239907