1.数据库设计
建立如下字段
2.明文密码两次MD5处理
原因:如果不对密码做任何处理直接明文传输,很容易被黑客截获密码,这样很不安全,于是通过一次MD5加密,这样即使黑客截获密码也看不出真正的密码是什么。但是现在有彩虹表这个反查MD5的技术,为了安全起见可以再MD5一次,并且在两次MD5加密的时候在原数值后加上Salt尾缀,增加破解难度,使黑客无法获取到用户真正的密码。大体步骤如下
(1)用户端:PASS = MD5 (明文+固定Salt)
(2)服务端:PASS = MD5(用户输入+随机Salt)
明文密码做MD5传给服务端,服务端加入随机Salt与用户输入的密码拼接起来,然后再一次MD5,把MD5和Salt同时写入数据库当中。第一次MD5是为了防止用户的密码在网络上明文传输,服务端的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>
创建util包(存放MD5相关的类)
创建MD5util类,拼接Salt做MD5加密,第一次MD5后到服务端拼接随机Salt并将Salt和第一次MD5后的用户密码存入数据库。
import org.apache.commons.codec.digest.DigestUtils;
public class MD5Util {
public static String md5(String src) {
return DigestUtils.md5Hex(src);
}
private static final String salt = "1a2b3c4d";
//拼接Salt并MD5加密,第一个Salt如果随机,服务端无法知道是什么
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);
}
//第二次MD5,生成随机Salt
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;
}
public static void main(String[] args) {
System.out.println(inputPassToFormPass("123456"));//d3b1294a61a07da9b49b6e22b2cbd7f9
// System.out.println(formPassToDBPass(inputPassToFormPass("123456"), "1a2b3c4d"));
// System.out.println(inputPassToDbPass("123456", "1a2b3c4d"));//b7797cce01b4b131b433b6acf4add449
}
}
3.JSR303参数校验和全局异常处理器
(1)在登录的controller中每一次登录都要做参数校验,
加入JSR303校验的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
使用方法,第一步在传递loginVo的参数前加@Valid注解
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
log.info(loginVo.toString());
//登录
userService.login(response, loginVo);
return Result.success(true);
}
第二步在LoginVo类的每一个需要校验的参数前加@NotNull不允许为空,在手机号前还需加入@IsMobile自定义校验器,在密码参数前还需加入第二个注解@Length(min=32)限制密码长度。
LoginController类如下
@Controller
@RequestMapping("/login")
public class LoginController {
private static Logger log = LoggerFactory.getLogger(LoginController.class);
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
@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());
//登录
userService.login(response, loginVo);
return Result.success(true);
}
}
建立自定义校验器的包validator,在包中建立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 = 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);
}
}
}
}
全局异常拦截器,新建exception包,建立GlobalException类和GlobalExceptionHandler类。
GlobalExceptionHandler类
@ControllerAdvice//类似xml的实现
@ResponseBody
//相当于controller
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());//获取并返回CodeMsg错误信息
}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));//将msg传入绑定参数中,使错误信息带出去
}else {
return Result.error(CodeMsg.SERVER_ERROR);//CodeMag为自定义的错误信息,返回带参数的错误码
}
}
}
GlobalException类(全局异常类)在MiaoshaUserService类中判断出现异常后,抛出此异常(if(loginVo == null){ throw new GlobalException(CodeMsg.Server_error); })抛完之后由异常处理器处理。
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;
}
}
4.分布式Session
秒杀功能,实际运行的时候有多台服务器,此时会有用户的session处理的问题,第一个请求在第一台服务器上,第二个请求在第二个服务器上,用户的session信息就丢失了。原生的session会同步session,但是同步消耗了过多资源。登录成功后给用户生成一个类似于sessionID的东西(takon)绑定用户的session,写到cookie中传递到客户端,然后客户端在之后的访问中上传这个token,服务端拿到token之后,根据token来取到用户的session信息。token是UUID生成的通用唯一识别码复制的。