MD5加密登录
如果不做任何处理:那么明文密码就会在网络上进行传输,假如说恶意用户取得这个数据包,那么就可以得到这个密码,这不安全。
为什么做两次MD5?
- 用户端:PASS=MD5 (明文+固定Salt)
- 服务端:PASS=MD5(用户输入+随机Salt)
第一次 (在前端加密,客户端):密码加密是(明文密码+固定盐值)生成md5用于传输,目的由于http是明文传输,当输入密码若直接发送服务端验证,此时被截取将直接获取到明文密码,获取用户信息。加盐值是为了混淆密码,原则就是明文密码不能在网络上传输。
第二次:服务端接收到已经计算过依次MD5的密码后,我们并不是直接存至数据库里面,而是生成一个随机的salt,跟用户输入的密码一起拼装,再做一次MD5,然后再把最终密码存在数据库里面。
第二次的目的:
防止数据库被入侵,被人通过彩虹表反查出密码。所以服务端接受到后,也不是直接写入到数据库,而是生成一个随机盐(salt),再进行一次MD5后存入数据库。
在pom文件里面引入两个MD5的依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>
编写MD5Util工具类用于生成MD5密码:
public class MD5Util {
public static String md5(String src) {
return DigestUtils.md5Hex(src);
}
//客户端固定的salt,跟用户的密码做一个拼装
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(md5(str));
return md5(str); //char类型计算会自动转换为int类型
}
//二次MD5
public static String formPassToDBPass(String formPass,String salt) {//随机的salt
String str=""+salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
return md5(str);
}
//数据库md5,使用数据库随机salt
public static String inputPassToDbPass(String input,String saltDB) {
String formPass=inputPassToFormPass(input);
System.out.println(formPass);
String dbPass=formPassToDBPass(formPass,saltDB);
return dbPass;
}
public static void main(String[] args) {
String pass = "123456";
String salt = "1a2b3c4d";
System.out.println(inputPassToDBPass(pass,salt));
}
}
数据库里面存的是做了两次MD5的用户密码与其对应的salt值
现在我们登录的时候,要去取得数据库里面对应用户的密码和salt值,然后后台接收了前端做了一次MD5的密码formPass,然后将这个formPass去和数据库里面的salt一起再做一次MD5,然后检测是否与数据库里面存的那个密码一致。
问题引用:
第二次MD5所用的随机saltDB为什么要保存在数据库里?
当数据库被侵入,做MD5之后的密码和saltDB一起被盗的话,用户密码不就泄露了吗?
但是如果不保存这个随机的saltDB,下次用户登录的时候不就没法和数据库保存的密码进行校验了吗?
问题思考:
实际上做MD5也不是绝对安全的,但是我们可以使得破解的难度指数级增长。md5是不可逆的,不能反向解密的,网上所谓的“解密”都是把“加密”结果存储到数据库再比对的只能暴力破解,即有一个字典,从字典中读取一条记录,将密码用加salt盐值做MD5来对比数据库里面的值是否相等。
因为好事者收集常用的密码,然后对他们执行 MD5,然后做成一个数据量非常庞大的数据字典,然后对泄露的数据库中的密码进行对比,如果你的原始密码很不幸的被包含在这个数据字典中,那么花不了多长时间就能把你的原始密码匹配出来,这个数据字典很容易收集,假设有600w个密码。坏人们可以利用他们数据字典中的密码,加上我们泄露数据库中的 Salt,然后散列,然后再匹配。
但是由于我们的 Salt 是随机产生的,每条数据都要加上 Salt 后再散列,假如我们的用户数据表中有 30w 条数据,数据字典中有 600w 条数据,坏人们如果想要完全覆盖的话,他们就必须加上 Salt 后再散列的数据字典数据量就应该是 300000* 6000000 = 1800000000000,所以说干坏事的成本太高了吧。但是如果只是想破解某个用户的密码的话,只需为这 600w 条数据加上 Salt,然后散列匹配。可见 Salt 虽然大大提高了安全系数,但也并非绝对安全。
实际项目中,Salt 不一定要加在最前面或最后面,也可以插在中间,也可以分开插入,也可以倒序,程序设计时可以灵活调整,都可以使破解的难度指数级增长。
步骤:
1.创建秒杀User的domain类
@lombok
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;
}
2.新建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); //绑定
}
3.新建MiaoshaUserService:
在service层,协调redis和mysql的存取
@Service
public class MiaoshaUserService {
public static final String COOKIE1_NAME_TOKEN="token";
@Autowired
MiaoshaUserDao miaoshaUserDao;
@Autowired
RedisService redisService;
/**
* 根据id取得对象,先去缓存中取
* @param id
* @return
*/
public MiaoshaUser getById(long id) {
//1.取缓存 ---先根据id来取得缓存
MiaoshaUser user=redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class);
//能再缓存中拿到
if(user!=null) {
return user;
}
//2.缓存中拿不到,那么就去取数据库
user=miaoshaUserDao.getById(id);
//3.设置缓存
if(user!=null) {
redisService.set(MiaoshaUserKey.getById, ""+id, user);
}
return user;
}
}
4.新建LoginController
判断用户密码是否匹配
@RequestMapping("/login")
@Controller
public class LoginController{
@Autowired
UserService userService;
@Autowired
RedisService redisService;
@Autowired
MiaoshaUserService miaoshaUserService;
//slf4j
private static Logger log=(Logger) LoggerFactory.getLogger(Logger.class);
@RequestMapping("/to_login")
public String toLogin() {
return "login";// 返回页面login
}
@RequestMapping("/do_login") // 作为异步操作
@ResponseBody
public CodeMsg doLogin(LoginVo loginVo) {// 0代表成功
// log.info(loginVo.toString());
if (loginVo == null) {
return CodeMsg.SERVER_ERROR;
}
// 验证
String formPass = loginVo.getPassword();
String mobile = loginVo.getMobile();
// 验证用户
MiaoshaUser user = miaoshaUserService.getById(Long.parseLong(mobile));
if (user == null) {
return CodeMsg.MOBILE_NOTEXIST;
}
// 验证密码
String dbPass = user.getPwd();
String dbSalt = user.getSalt();
System.out.println("dbPass:" + dbPass + " dbSalt:" + dbSalt);
// 验证密码,计算二次MD5出来的pass是否与数据库一致
String tmppass = MD5Util.formPassToDBPass(formPass, dbSalt);
System.out.println("formPass:" + formPass);
System.out.println("tmppass:" + tmppass);
if (!tmppass.equals(dbPass)) {
return CodeMsg.PASSWORD_ERROR;
}
return CodeMsg.SUCCESS;
}
}
5.前端login.html:引入bootstrap, 相关的js和css
var pass=$("#password").val();
//pass='111111';
//固定salt
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);
}
});
JSR303校验
系统在登录的时候做了一个参数校验,也就是说每一个方法的开头都要去做一个校验,那么有没有更简洁的方法呢?那就是使用JSR 303 校验。
JSR 303 用于对Java Bean 中的字段的值进行验证,使得验证逻辑从业务代码中脱离出来。是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
1.引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.用法:
在需要验证的参数前面打上标签注解@Valid,那么此注解就会自动对该Bean 进行参数校验。具体校验规则在该Bean内部实现。本项目是对登陆时候,利用到了参数校验。
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;
}
}
- 好处:可以直接使用@NotNull、@Length(min=32)等注解进行验证,避免重复的校验代码,只需在传入的参数上打上注解就可以进行参数校验,避免代码冗余。