认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
认证的关键对象
- Subject:主体,主体可以是访问系统的用户、应用程序等。需要进行认证的都称为主体。
- Principal:身份信息,是主体(Subject)进行身份认证的标识,标识必须具有唯一性。如用户名、手机号、邮箱地址等。一个主体可以有多个身份,但是必须有一个主身份(Primary Principal) 。
- Credential:凭证信息,是只有主体自己知道的安全信息,如密码、证书等。
Shiro认证流程
用户认证时携带身份和凭证信息(一般是用户名和密码),Shiro会将我们提供的信息打包成一个Token令牌,然后将Token给Shiro中的Security Manager去进行校验。校验具体流程是:Security Manager调用Authenticator,Authenticator调用Pluggable Realms获取原始数据,然后将原始数据和用户提供的信息相匹配,如果一致,则认证成功。
Shiro认证Demo
-
在
pom.xml
中引入依赖<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency>
-
在
resources
目录中创建shiro.ini
文件[users] john=123 peter=12345
ini是Shiro中的配置文件,在没有连接数据库时可以将系统中相关的权限数据写在ini文件中。
-
Demo
public static void main(String[] args) { // 创建安全管理器对象 DefaultSecurityManager是对SecurityManager接口的一个具体实现 DefaultSecurityManager securityManager = new DefaultSecurityManager(); // 给安全管理器设置Realm 获取身份和凭证数据 securityManager.setRealm(new IniRealm("classpath:shiro.ini")); // Shiro提供的全局安全工具类SecurityUtils 安全工具设置安全管理器完成认证 SecurityUtils.setSecurityManager(securityManager); // 关键对象 Subject 当前登录的主体对象 Subject subject = SecurityUtils.getSubject(); // 创建令牌 身份信息为username 凭证信息为password UsernamePasswordToken token = new UsernamePasswordToken("john", "123"); try { // 用户认证 认证通过不会抛出异常 subject.login(token); // 获取认证状态 true为认证成功 false为认证失败 System.out.println("认证状态:" + subject.isAuthenticated()); } catch (UnknownAccountException e) { // 认证失败异常 用户名不存在 e.printStackTrace(); } catch (IncorrectCredentialsException e) { // 认证失败异常 密码错误 e.printStackTrace(); } }
Shiro认证源码
上边的程序Demo使用的是Shiro自带的lniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义Realm。
Shiro提供的系统Realm如下:
其中涉及到认证的Realm如下:
SimpleAccountRealm
类就是Shiro默认完成认证和授权的底层操作,其中 doGetAuthenticationInfo
方法处理认证 ,doGetAuthorizationInfo
方法处理授权。
public class SimpleAccountRealm extends AuthorizingRealm {
...
// 认证处理
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
// 授权处理
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
}
由源码可得,
SimpleAccountRealm
类中定义的doGetAuthenticationInfo
方法只处理身份信息(用户名)的认证,密码的认证由Shiro自动处理。
Shiro自定义认证
由源码可知,如果我们要将读取数据的模式从ini配置文件改变到从数据库中读取数据,那么我们必须自己定义一个类去继承 AuthorizingRealm
,并重写 doGetAuthenticationInfo
方法。
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 修改认证处理方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取身份信息
String username = (String) token.getPrincipal();
// 从数据库中读取数据进行身份验证和密码验证
if ("john".equals(username)) {
// 参数1:用户名 参数2:密码 参数3:当前Realm名称
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, "123", this.getName());
// 认证成功返回SimpleAuthenticationInfo
return simpleAuthenticationInfo;
}
return null;
}
}
再修改注入的Realm
public static void main(String[] args) {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 注入自定义Realm
UserRealm userRealm = new UserRealm();
securityManager.setRealm(userRealm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("john", "123");
try {
subject.login(token);
System.out.println("认证状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
}
}
MD5算法
1. 特点
-
不可逆,加密过的密文不能反推出原文。(只能根据明文——>密文)
那些MD5解密工具的原理使用的都是暴力穷举的算法来解密。
-
如果加密相同的内容,无论加密多少次,最后生成的密文都一致。
2. 作用
- 给明文加密
- 签名(校验和),利用MD5第二个特点,使用MD5给文件加密,如果文件内容不变,那么加密结果不会发生变化。因此可以通过比对当前文件加密结果和原加密结果来判断当前文件的内容是否发生改变。
3. 生成结果
MD5加密的对象无论有多大,加密的结果一定是一个16进制32位的字符串。
Salt盐
因为MD5加密相同的内容结果不变,因此MD5加密的信息可能会被暴力穷举而破解,因此我们需要在MD5时添加一层安全处理。
使用的Salt的场景:
假设在注册时设置的密码为 "123",正常情况下是直接使用MD5将 "123" 加密。如果添加了Salt之后,假设盐为 "abc",先将盐保存到数据库,再在密码的末尾添加 "abc" 构成 "123abc",然后使用MD5将 "123abc" 加密。等用户使用 "123" 登录时,再将盐取出来拼接到密码末尾再做MD5加密,获取的结果和数据库中保存的密码密文进行匹配,就可以实现身份认证了。
使用了MD5 + Salt对密码进行加密后,会让密码相对更安全,因为Salt的加密机制不在于Salt的内容,而在于Salt和密码的拼接规则。
Shiro加密API
Shiro中对于密码加密并不仅仅使用MD5,而是使用MD5 + Hash散列的方式对密码进行加密。
Hash散列次数如果不设置,默认为1次,相当于没有做Hash散列,还是原生MD5进行加密。如果设置Hash散列次数,次数越多,密码加密的安全度越高。(一般散列1024或者2048次)
Shiro中Salt和密码的默认拼接规则是Salt拼接在密码的开头。
使用如下API可以在用户注册时完成对用户密码的加密。但是注意,注册使用的加密方式必须和认证时的加密方式一致,如果不一致,最终导致匹配时两边加密的结果不一样。
public static void main(String[] args){
// 参数是密码 MD5对密码进行加密
Md5Hash md5 = new Md5Hash("123");
// 获取加密后的密码
String passwordMD5 = md5.toHex();
System.out.println(passwordMD5);
// 参数1是密码 参数2是盐 Shiro默认将盐拼接到密码开头进行处理
Md5Hash md5Salt = new Md5Hash("123", "abc");
String passwordMD5Salt = md5Salt.toHex();
System.out.println(passwordMD5Salt);
// 参数1是密码 参数2是盐 参数3是散列次数
Md5Hash md5HashSalt = new Md5Hash("123", "abc", 1024);
String passwordMD5SaltHash = md5HashSalt.toHex();
System.out.println(passwordMD5SaltHash);
}
Shiro加密认证流程
Shiro普通认证使用的是默认凭证匹配器,默认匹配器不会对输入的密码进行加密。
实现Shiro加密认证需要使用指定的凭证匹配器,从而可以对输入的密码进行MD5 + Salt + Hash散列的加密。
public static void main(String[] args) {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 注入自定义Realm
UserMD5Realm userMD5Realm = new UserMD5Realm();
// 给Realm设置Hash凭证匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置Hash散列次数
hashedCredentialsMatcher.setHashIterations(1024);
// 装载凭证匹配器
userMD5Realm.setCredentialsMatcher(hashedCredentialsMatcher);
securityManager.setRealm(userMD5Realm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
// 自动给该密码加密,Salt从UserMD5Realm中获取
UsernamePasswordToken token = new UsernamePasswordToken("john", "123");
try {
subject.login(token);
System.out.println("认证状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
}
}
自定义Realm,模拟从数据库中取出加密后的密码(即上一步中第三种方式生成的 md5HashSalt)和加密后的前台输入密码进行匹配
public class UserMD5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
// 从数据库中取出加密后的密码进行匹配 同时从数据库中返回Salt给HashedCredentialsMatcher加密使用(Salt的内容是由开发人员设置,并在注册时存入数据库)
if ("john".equals(username)) {
// 第三个参数为数据库中返回的Salt
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, "894b3913a4a13b25dc6186d11835c209", ByteSource.Util.bytes("abc"), this.getName());
// 返回身份认证信息对象
return simpleAuthenticationInfo;
}
return null;
}
}
原文地址:https://juejin.cn/post/7011811166229889037