存储尽量不要用明文密码,且不可逆的加密存储
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的 数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个 salt(盐),比如 加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一 些 md5 解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相 对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐); 这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。
我们知道用户登录时,在realm的doGetAuthenticationInfo方法中,我们将根据账户查出来的密码放在了SimpleAuthenticationInfo对象返回,shiro会帮我们比对用户输入的密码和从数据库查出来的密码,如果不正确就会抛异常IncorrectCredentialsException,我们在登录方法外catch异常显示在页面,比对正确就没有异常。
简单来说,加密就是:创建用户的时候我们将 用户传进来的明文密码 用算法工具类加密,存入数据库。
而解密就是:shiro提供了CredentialsMatcher 将我们采用的加密算法注入,将CredentialsMatcher注入realm,shiro通过这个CredentialsMatcher将用户输入的密码也加密,进行密码比对。 所以接下来短信免密登录、限制密码重试次数我们都可以通过继承重写CredentialsMatcher的方法实现
1.用户注册时 生成密码,加密存储。
采用md5,盐用随机数,2代表做两次哈希运算
public void registeStu(StuRegister stuRegister) {
String salt=CodeGenerateUtils.getRandNum(4);
DlStudentInfo stu=new DlStudentInfo();
stu.setDlStudentPassword(new SimpleHash("md5", stuRegister.getPassword(), salt, 2).toString());
stu.setDlStudentSalt(salt);
studentInfoMapper.insert(stu);
}
2。realm doGetAuthenticationInfo ,返回SimpleAuthenticationInfo,加入盐(从用户表查到的对应的盐),和从库中查出的密码
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authcToken) throws AuthenticationException {
userInfo是根据账户查出的用户对象
SimpleAuthenticationInfo authcInfo=new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), this.getName());
if(userInfo.getSalt()!=null) {
authcInfo.setCredentialsSalt(ByteSource.Util.bytes(userInfo.getSalt()));
}
return authcInfo;
}
3.shiro配置在realm中注入密码凭证器
/**
* 身份认证realm; (这个需要自己写,账号密码校验;权限等)
* @return
*/
@Bean
public DLingShiroRealm DLingShiroRealm() {
DLingShiroRealm myShiroRealm = new DLingShiroRealm();
myShiroRealm.setCredentialsMatcher(credentialsMatcher());
myShiroRealm.setUserService(userService);
return myShiroRealm;
}
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashSalted(true);
credentialsMatcher.setHashIterations(2);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
通过 credentialsMatcher.hashAlgorithmName=md5 指定散列算法为 md5,需要和生成密 码时的一样;
credentialsMatcher.hashIterations=2,散列迭代次数,需要和生成密码时的意义;
credentialsMatcher.storedCredentialsHexEncoded=true 表示是否存储散列后的密码为 16 进 制,需要和生成密码时的一样,默认是 base64;
此处最需要注意的就是 HashedCredentialsMatcher 的算法需要和生成密码时的算法一样。另 外 HashedCredentialsMatcher 会 自 动 根 据 AuthenticationInfo 的 类 型 是 否 是 SaltedAuthenticationInfo 来获取 credentialsSalt 盐
如果采用短信登录的话,主要是校验验证码,不需要校验密码。但需求是密码登录和短信登录都有,该怎么做?
发送短信看我这篇:https://blog.csdn.net/u014203449/article/details/80683014
HashedCredentialsMatcher 中完成密码校验的方法是doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info),参数是令牌和认证信息。这个令牌token 就是在登录时subject.login(token)的这个令牌。
所在我们可以继承UsernamePasswordToken 令牌 ,多加一个loginType属性,用于区分登录方式。
在doCredentialsMatch方法中,如果登录方式是免密登录,就直接放过返回true密码校对成功(前提自己校对验证码),如果是密码登录,则直接返回父类UsernamePasswordToken的doCredentialsMatch方法执行
自定义NoPasswordToken
public class NoPasswordToken extends UsernamePasswordToken {
private static final long serialVersionUID = -2564928913725078138L;
private String loginType;
/**
* 账号密码登录
* @param type
* @param username
* @param password
*/
public NoPasswordToken(String username, String password) {
super(username,password);
this.loginType=BaseConstants.LoginType.PASSWORD.getCode();
}
/**
* 免密登录
*/
public NoPasswordToken(String username) {
super(username,"");
this.loginType=BaseConstants.LoginType.NOPASSWD.getCode();
}
}
免密登录和密码登录写两个接口,密码登录:
public Message userLogin(UserStuLogin user){
if(user==null){
return Message.ok("登录失败");
}
String account=user.getAccount();
String password=user.getPassword();
NoPasswordToken token = new NoPasswordToken(account,password);
Subject currentUser = SecurityUtils.getSubject();
try {
currentUser.login(token);
} catch(IncorrectCredentialsException e){
return Message.ok("密码错误");
} catch (AuthenticationException e) {
return Message.ok("登录失败"+e.getMessage());
}
免密登录:
public void userMsgLogin(UserMsgLogin userMsgLogin) {
NoPasswordToken token=new NoPasswordToken(userMsgLogin.getAccount());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (AuthenticationException e) {
throw new DataValidateFiledException("登录失败");
}
}
shiro配置密码凭证器
@Bean
public DLingShiroRealm DLingShiroRealm() {
DLingShiroRealm myShiroRealm = new DLingShiroRealm();
myShiroRealm.setCredentialsMatcher(credentialsMatcher());
myShiroRealm.setUserService(userService);
return myShiroRealm;
}
/**
* 密码凭证器,免密,避免暴力破解
* @return
*/
@Bean
public RetryLimitHashedCredentialsMatcher credentialsMatcher() {
RetryLimitHashedCredentialsMatcher credentialsMatcher=new RetryLimitHashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashSalted(true);
credentialsMatcher.setHashIterations(2);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
credentialsMatcher.setRedissonClient(redissonClient);
return credentialsMatcher;
}
自定义RetryLimitHashedCredentialsMatcher:
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
NoPasswordToken noPasswordToken=(NoPasswordToken) token;
//如果是免密,就不需要核对密码了
if(noPasswordToken.getLoginType().equals(BaseConstants.LoginType.NOPASSWD.getCode())) {
return true;
}
return super.doCredentialsMatch(token, info);
}
用redis记录一个账号尝试密码的次数和时间,如果到5次 就锁定30分钟
在自定义的RetryLimitHashedCredentialsMatcher,调用HashedCredentialsMatcher 密码校验方法前,从redis中取出此账号上次登录的时间和重试的次数,如果次数超过5次并且距离上次登录的时间在30分钟内,就抛异常不允许登录,时间超过30分钟清空记录。
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
//@Autowired
RedissonClient redissonClient;
public RetryLimitHashedCredentialsMatcher() {
}
public RedissonClient getRedissonClient() {
return redissonClient;
}
public void setRedissonClient(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
NoPasswordToken noPasswordToken=(NoPasswordToken) token;
//如果是免密,就不需要核对密码了
if(noPasswordToken.getLoginType().equals(BaseConstants.LoginType.NOPASSWD.getCode())) {
return true;
}
String username = (String)token.getPrincipal();
//retry count + 1
RMap map = redissonClient.getMap("retrycredentials");
UserRetrycredentials userRetrycredentials = map.get(username);
Date now = new Date();
if(userRetrycredentials==null) {
userRetrycredentials = new UserRetrycredentials();
userRetrycredentials.setRetryCount(0);
userRetrycredentials.setTime(now);
map.put(username,userRetrycredentials);
}else if(userRetrycredentials.getRetryCount()>5
&&now.getTime()-userRetrycredentials.getTime().getTime()<1800000) {
throw new ExcessiveAttemptsException("密码填写错误5次,账号已被锁定,请30分钟后再登录");
}else if(now.getTime()-userRetrycredentials.getTime().getTime()>1800000) {
userRetrycredentials.setRetryCount(0);
userRetrycredentials.setTime(now);
}
boolean matches = super.doCredentialsMatch(token, info);
if(matches) {
//clear retry count
map.remove("retrycredentials");
}else {
userRetrycredentials.setRetryCount(userRetrycredentials.getRetryCount()+1);
userRetrycredentials.setTime(now);
map.put(username, userRetrycredentials);
}
return matches;
}
}