Java高性能高并发实战之登录功能设计

前言

翻看了之前买的课程,今天又重温了一遍之前的代码,每次查看也总是能发现自己不足的地方,这里也开始做一个记录。也算是开始对项目复习。

两次MD5加密设置。

之前学习过就是以用户登录时候设置是以用户名作为盐值来实现MD5加密服务,现在使用固定的盐值(当然这个盐值还是在数据库中进行读取)下面可以看一下数据库的设计:
在这里插入图片描述
这里我们指定一个盐值,在进行加密时候加入到其中。这里我们进行的两次MD5操作一方面是保证对于数据在网络上传输时候,我们输入的密码传输到服务端时候在向底层传输时候进行一级的加密操作。这个时候在服务端操作的密码也不是明文。第二个方面是在防止别人对我们的数据库盗取时候,不会因为一级加密就简单进行反解密拿到我们的数据库密码,在服务端中存到底层的密码是二度加密过。

前端设计

在拿到用户输入的密码以后我们先进行一个初级加密,这个加密的流程和盐值都是和后端相同的。
在这里插入图片描述
前端这样设计,保证拿到后端的密码是进行过一级加密的。
在这里插入图片描述
这里介绍一下自己定义的工具类。


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) {
		String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
		System.out.println(str);
		// 一级加密
		return md5(str);
	}

	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);
	}

	public static String inputPassToDbPass(String inputPass, String saltDB) {
		String formPass = inputPassToFormPass(inputPass);
		String dbPass = formPassToDBPass(formPass, saltDB);
		// 结合起来的一次完整加密。
		return dbPass;
	}
	
}

这样,在密码传到后台时候是已经进行过一级的加密操作。然后再进行自定义工具类的二级加密,判断和从数据库中根据用户名取出的密码是否相同来进行下一步操作。打印出错误的信息,还是登录成功。

数据合法性验证

既然前端表示是电话号码,所以必须是11位电话号码,并且第一位必须是一,所以我们用到了JSR303参数校验算是一个原生的代码库,我们在使用时候还是先进行一个引用处理。然后在我们想要进行校验的参数前加上@Valid

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

如下图所示我们在想要进行校验地方加上对应的注解:
在这里插入图片描述
然后去进行具体的使用之前先查看都有什么方法可以进行使用,如下图所示左边的就是可以进行使用的方法
在这里插入图片描述
具体含义:
在这里插入图片描述
但是我们思考一下对于电话号码来说并不能只是单纯的不能为空,我们需要判断值为11位,减轻后台的负担,思考一下,对于格式不符合标准的电话号码,我们都不用请求数据库,因为此时请求数据库,也是电话号码不存在,所在在前端就进行验证,不正确不予以通过。这里我们可以仿照他们的格式来自己编写一个判断是否是正确电话号码格式的注解。

判断是否是11位电话号码

点击源码进行查看,发现就一些注解和原生的两个函数一个默认值。我们仿照进行完成自己的校验器。
在这里插入图片描述
@isMobiles

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
isMobVa.class}//指向具体的实现
)
public @interface isMobiles  {
    boolean required() default true;

    String message() default "手机号码格式错误";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

isMobVa

public class isMobVa implements ConstraintValidator<isMobiles,String> {

    private  boolean required=false;
    @Override
    public void initialize(isMobiles isMobiles) {
        required=isMobiles.required();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if(required){// 表示传入了电话号码值。
            return ValidatorUtil.isMobile(s);
        }else {
            if(StringUtils.isEmpty(s))
            {
                return  true;
            }
            else {
                return ValidatorUtil.isMobile(s);
            }
        }


    }
}

具体实现

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

全局异常处理

我们在登录时候有可能会出现各种的问题,但是若是在我们的业务函数中处理这些异常时候,代码就会显得很是臃肿,所以我们定义一个异常全局处理函数GlobalExceptionHandler,进行异常的全局处理,在业务逻辑中出现了各种的异常,我们只需要继续抛出,就会由异常处理器进行处理。

@ControllerAdvice
@ResponseBody

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);
		}
	}
}

例如我们在login过程中,可能会有密码错误,用户名不存在这些错误,一般也都是在service层中进行处理,但是这里我们只是将异常进行抛出,不进行具体的处置。减少了代码的冗余度。
在这里插入图片描述

分布式Session

对于分布式session来说也是应用比较广泛也是非常重要的一门技术。对于秒杀项目来说,并不可能将所有的请求都打在同一台服务器上,对于访问量和请求量比较大的情况下性能也会没办法达到,就需要我们多台服务器搭建集群进行数据的处理。此时我们假设一个情况,对于一个用户的请求,第一次打到了第一台服务器上,服务器记录session信息,对于第二次请求打到第二太服务器上,难道用户还用重新登录吗?这当然是不现实的,所以此时需要做的就是对于集群内的服务器进行一个session的同步,对于原生的session同步方法就是对于集群内的所有session进行一个同步处理,但是这样的效率太低下。对于一个集群里面的数据,同步起来也是相当消耗时间。
所以现在常用的方法就是: 用户在登录成功以后给用户生成一个类似于session id的东西,叫做token来标识这个用户,写到cookie中传递给客户端。随后的客户端在访问的过程中,都在cookie中上传这个token,服务端就会根据这个token来取到用户对应的session信息。和容器实现原生的session类似

设置token

  1. 首先设置UUID信息,为每一个用户都生成唯一的UUID
public class UUIDUtil {
	public static String uuid() {
		return UUID.randomUUID().toString().replace("-", "");
	}
}
  1. 此时我们有了token以后,设计将token加入到cookie中.
public static final String COOKI_NAME_TOKEN = "token";
public static final int TOKEN_EXPIRE = 3600*24 * 2;
	private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
		redisService.set(MiaoshaUserKey.token, token, user);
		// 加入第三方缓存中,在下一次的加载中从redis中通过key(token)取到。
		Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
		cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
		// 有效期。
		cookie.setPath("/");
		// 完成以后 将cookie写到response中。
		response.addCookie(cookie);
	}

以上就是设置token信息到cookie中,现在我们模拟在用户登录时候取出对应的信息。

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

初值的判定。

以上我们就实现了根据服务端将一个token写到cookie中,然后客户端在随后的访问中携带这个cookie,这个时候服务端就可以根据cookie中的token找到这个用户。
这个时候就需要进一步的优化处理,我们对于商品页来说,会有很多的方法,都需要进行用户权限的判定,所以这个时候,就利用到了Sping Mvc的addArgumentResolvers方法,对于Controller的方法中会有很多的参数例如request,modle等,但是我们并没有显示赋值,都是通过这个方法的回掉处理,这个时候我们就利用这个特性来对我们的用户对象做一个处理:判断当前对象类型是不是我们的已经进行过验证的类型:

UserArguementResolver
继承HandlerMethodArgumentResolver 以后会要就实现以下两个函数,具体完成即可。


@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_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;
		return userService.getByToken(response, token);
	}

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

后记

这里也只是讲出部分内容,后期我将源码进行详细注释放到github上,有需要的小伙伴可以自行下载呀。

发布了104 篇原创文章 · 获赞 92 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_44015043/article/details/105658287