1 Shiro 架构 (Shiro外部来看)
• 从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作:
2 身份验证
• 身份验证
:一般需要提供如身份 ID 等一些标识信息来表明登录者的身份,如提供 email,用户名/密码
来证明。
• 在 shiro 中,用户需要提供 principals (身份)
和 credentials(证明)
给 shiro,从而应用能验证用户身份:
• principals
:身份,即主体的标识属性
,可以是任何属性,如用户名、邮箱等,唯一即可
。一个主体可以有多个 principals,但只有一个Primary principals,一般是用户名/邮箱/手机号。
• credentials
:证明/凭证
,即只有主体知道的安全值,如密码/数字证书等。
• 最常见的 principals 和 credentials 组合就是用户名/密码
了
3 身份验证基本流程
• 1、收集
用户身份/凭证,即如用户名/密码
• 2、调用 Subject.login 进行登录
,如果失败将得到相应的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功
• 3、创建自定义的 Realm 类,继承org.apache.shiro.realm.AuthorizingRealm 类,实现doGetAuthenticationInfo() 方法
3.1 详细登录认证流程
- 获取当前的
Subject
, 调用SecurityUtils.getSubject()
; - 测试当前的用户是否已经被认证. 即是否已经登录. 调用
Subject de isAuthenticated()
; - 若没有被认证, 则把
用户名
和密码
封装为UsernamePasswordToken 对象
;
- 1 创建一个表单页面
- 2 把请求提交到 SpringMVC 的 Handler
- 3 获取用户名和密码
- 执行登录: 调用 Subject 的 login(AuthenticationToken) 方法;
- 自定义 Realm 的方法, 从数据库中获取对应的记录, 返回给Shiro.
- 1 实际上需要继承
org.apache.shche.shiro.realm.AuthenticationRealm
类 - 2 实现 doGetAuthenticationInfo(AuthenticationToken) 方法
- 由 Shiro 完成对密码的比对
4 身份验证示例
5 AuthenticationException
• 如果身份验证失败请捕获 AuthenticationException 或其子类
• 最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;
6 认证流程
7 身份认证流程
• 1、首先调用 Subject.login(token) 进行登录,其会自动委托给SecurityManager
• 2、SecurityManager 负责真正的身份验证逻辑;它会委托给Authenticator 进行身份验证;
• 3、Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
• 4、Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用AuthenticationStrategy 进行多 Realm 身份验证;
• 5、Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
8 Realm
• Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),即 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作
• Realm接口如下:
• 一般继承 AuthorizingRealm(授权)即可
;其继承了AuthenticatingRealm(即身份验证),而且也间接继承了CachingRealm(带有缓存实现)。
• Realm 的继承关系:
9 Authenticator
• Authenticator 的职责是验证用户帐号,是 Shiro API 中身份验证核心的入口点:如果验证成功,将返回AuthenticationInfo 验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的 AuthenticationException 异常
• SecurityManager 接口继承了 Authenticator,另外还有一个ModularRealmAuthenticator实现,其委托给多个Realm 进行验证,验证规则通过 AuthenticationStrategy 接口指定
10 AuthenticationStrategy
• AuthenticationStrategy 接口的默认实现:
• FirstSuccessfulStrategy
:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;
• AtLeastOneSuccessfulStrategy
:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信息;
• AllSuccessfulStrategy
:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。
• ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy策略
11 代码
11.1) 实现认证流程
1. login.jsp
2. UserAction 增加登录方法(login)
@PostMapping(value = "login")
public String login(String username, String password){
// 1. 获取当前的 `Subject`, 调用 `SecurityUtils.getSubject()`;
Subject subject = SecurityUtils.getSubject();
//2. 测试当前的用户是否已经被认证. 即是否已经登录. 调用`Subject de isAuthenticated()`;
if (!subject.isAuthenticated()){
//3. 若没有被认证, 则把 `用户名` 和 `密码` 封装为 `UsernamePasswordToken 对象` ;
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// rememberme
// token.setRememberMe(true);
//4. 执行登录: 调用 Subject 的 login(AuthenticationToken) 方法;
System.out.println("token:"+token.hashCode());
try {
subject.login(token);
} catch (AuthenticationException e) {
System.out.println("登录失败: " + e.getMessage());
}
//5. 自定义 Realm 的方法, 从数据库中获取对应的记录, 返回给Shiro.
// - 1 实际上需要继承 `org.apache.shche.shiro.realm.AuthenticationRealm` 类
// - 2 实现 doGetAuthenticationInfo(AuthenticationToken) 方法
//6. 由 Shiro 完成对密码的比对
}
return "redirect:/list";
}
3. ShiroRealm 修改realm
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("doGetAuthenticationInfo:"+authenticationToken.hashCode());
return null;
}
}
4. 测试
11.2) 实现认证(Realm)
1. 修改application-shiro.xml
/login = anon
/logout = logout
# everything else requires authentication:
/** = authc
2. ShiroRealm(修改Realm)
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//2. 从 UsernamePasswordToken 中获取 Username
String username = token.getUsername();
//3. 调用数据库的方法, 从数据库中查询 Username的用户信息
System.out.println("用户名:"+username);
//4. 若用户不存在, 可以抛出UnknownAccountException 异常
if ("unknown".equals(username)) {
throw new UnknownAccountException("用户不存在");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常
if ("master".equals(username)) {
throw new AuthenticationException("用户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenicationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//1) principal: 认证的实体信息, 可以是username, 也可以是数据表对应的用户的实体类对象
Object principal = username;
//2) credentials: 密码
Object credentials = "123456";
//3) realName: 当前 realm 对象的name, 调用父类的 getname() 方法即可
String realName = getName();
SimpleAuthenticationInfo info
= new SimpleAuthenticationInfo(principal, credentials, realName);
return info;
}
3. 增加退出 修改list.jsp
Logout
11.3) MD5加密
密码的比对:
通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行的密码的比对!
1. 查看<123456>加密后的结果
public static void main(String[] args) {
//加密方式
String hashAlgorithmName = "MD5";
//要加密的密码
Object credentials = "123456";
//盐值
Object salt = null;
//加密次数
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
2. 修改realm的配置文件(application-shiro.xml)
11.4) MD5盐值加密
- 为什么使用 MD5 盐值加密:
为了防止不同用户使用同一密码加密后密码一样- 如何做到:
1). 在 doGetAuthenticationInfo 方法返回值创建 SimpleAuthenticationInfo 对象的时候, 需要使用
SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName) 构造器
2). 使用 ByteSource.Util.bytes() 来计算盐值.
3). 盐值需要唯一: 一般使用随机字符串或 user id
4). 使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 来计算盐值加密后的密码的值.
1. 修改main方法
String hashAlgorithmName = "MD5";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("admin");
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
2. 修改doget
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//2. 从 UsernamePasswordToken 中获取 Username
String username = token.getUsername();
//3. 调用数据库的方法, 从数据库中查询 Username的用户信息
System.out.println("用户名:"+username);
//4. 若用户不存在, 可以抛出UnknownAccountException 异常
if ("unknown".equals(username)) {
throw new UnknownAccountException("用户不存在");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常
if ("master".equals(username)) {
throw new AuthenticationException("用户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenicationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//1) principal: 认证的实体信息, 可以是username, 也可以是数据表对应的用户的实体类对象
Object principal = username;
//-----------------------------修改位置(以下都是)-------------------------
//2) credentials: 密码
// Object credentials = "fc1709d0a95a6be30bc5926fdb7f22f4";
Object credentials = null;
if ("user".equals(username)) {
credentials = "098d2c478e9c11555ce2823231e02ec1";
} else if ("admin".equals(username)) {
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
}
//3) realName: 当前 realm 对象的name, 调用父类的 getname() 方法即可
String realName = getName();
//4) 盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info
// = new SimpleAuthenticationInfo(principal, credentials, realName);
= new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realName);
return info;
}
11.5) 多 realm 验证
1. 创建Realm(SecondRealm)
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("SecondRealm:"+authenticationToken.hashCode());
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//2. 从 UsernamePasswordToken 中获取 Username
String username = token.getUsername();
//3. 调用数据库的方法, 从数据库中查询 Username的用户信息
System.out.println("用户名:"+username);
//4. 若用户不存在, 可以抛出UnknownAccountException 异常
if ("unknown".equals(username)) {
throw new UnknownAccountException("用户不存在");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常
if ("master".equals(username)) {
throw new AuthenticationException("用户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenicationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//1) principal: 认证的实体信息, 可以是username, 也可以是数据表对应的用户的实体类对象
Object principal = username;
//2) credentials: 密码
// Object credentials = "fc1709d0a95a6be30bc5926fdb7f22f4";
Object credentials = null;
if ("user".equals(username)) {
credentials = "073d4c3ae812935f23cb3f2a71943f49e082a718";
} else if ("admin".equals(username)) {
credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06";
}
//3) realName: 当前 realm 对象的name, 调用父类的 getname() 方法即可
String realName = getName();
//4) 盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info
// = new SimpleAuthenticationInfo(principal, credentials, realName);
= new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realName);
return info;
}
public static void main(String[] args) {
String hashAlgorithmName = "SHA1";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("user");
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
2. application-shiro.xml