我正在参加「掘金·启航计划」
简单安全管理框架——Shiro
写在前面
(1)本文摘要
- Shiro基础
- 自定义
数据源Realm
- 认证流程
- 鉴权流程
一、初识Shiro
- 是Appache推出的安全管理框架
- 比起SpringSecurity更加简单易用
- 在Web项目中,一般用来作权限管理
(1)核心功能
-
认证
- 有时候被称为登录验证,只有合法的用户才能登录进入系统
-
授权
- 给对应的用户分配角色、以及权限
- 确定谁有权限访问“什么资源”
-
会话管理
- 管理特定于用户的会话,不局限与Web应用
-
密码学
- 使用加密算法确保数据安全,同时任然易于使用
-
我这里会着重说明授权和认证
(2)核心类型
- 网上很火的一张图
- Shiro核心的几个概念
public static void main(String[] args) {
// 1、安全管理器
DefaultSecurityManager manager = new DefaultSecurityManager();
// 2、设置安全管理器
SecurityUtils.setSecurityManager(manager);
// 3、设置数据源
// ps:数据源 -> 这里是用 .ini 文件模拟一下
IniRealm realm = new IniRealm("classpath:realm.ini");
manager.setRealm(realm);
// 4、模拟构建需要认证的主体
Subject subject = SecurityUtils.getSubject();
String username = "ciusyan";
String password = "222";
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 5、登录认证,不合法的用户,会抛出异常【如下所示】
subject.login(token);
}
复制代码
- 上面用到的
realm.ini
数据源
[users]
root = 111, admin
ciusyan = 222, guest
[roles]
admin = user:create, user:read, user:update, user:delete
guest = user:read
复制代码
shiro
常见的几个异常
public static void main(String[] args) {
try {
// 5、登录
subject.login(token);
} catch (UnknownAccountException e) {
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
System.out.println("密码不正确");
} catch (AuthenticationException e) {
System.out.println("认证失败~");
}
}
复制代码
二、自定义Realm
- 说这个问题之前,我们先来思考一下,为什么要自定义数据源
Realm
呢? Shiro
不是已经实现了好多Realm吗(如下图所示)
(1)解答一
- 上面的那个例子,我们使用了
.ini
文件,存放用户信息(用户名、密码、角色、权限) - 可我们在真实的开发中,大概率是不会将用户信息放在
ini
文件里的 - 这不用我多说,你应该也知道。会将用户信息,放在数据库
DB
中存储 - 那这时候又有疑惑了啊,我上面放的图,
Shiro
默认不也实现了JDBC
吗 - 这不就又回到了我们的问题,为什么要自定义
Realm
呢?
(2)解答二
- 我们先来看一下,官方的描述信息
Realm that allows authentication and authorization via JDBC calls. The default queries suggest a potential schema for retrieving the user's password for authentication, and querying for a user's roles and permissions. The default queries can be overridden by setting the query properties of the realm.
If the default implementation of authentication and authorization cannot handle your schema, this class can be subclassed and the appropriate methods overridden. (usually doGetAuthenticationInfo(AuthenticationToken), getRoleNamesForUser(Connection, String), and/or getPermissions(Connection, String, Collection)
Shiro中JdbcRealm的sql
- 我们从上面的描述中,主要可以得出以下信息
- 默认的实现,不太灵活。你程序的表名、和字段名,都得按默认的规范来
- 如果不能满足我们的系统,我们可以自定义
Realm
- 这一下,你应该知道,我们为什么要自定义数据源
Realm
了吧
(3)如何实现
-
先实现一个简单的自定义数据源
Realm
-
一样的,先模拟一下数据库
-
用户实体类
public class User {
private String username;
private String password;
}
复制代码
- 模拟数据库查询
public class Dbs {
public static User get(String username) {
if ("ciusyan".equals(username)) {
return new User("ciusyan", "222");
}
return null;
}
}
复制代码
1、Step1
public class CustomRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
复制代码
- 在自定义的
Realm
中,直接继承类AuthorizingRealm
Q:为什么要继承这个类呢?
-
看这个类单词的拼写:授权 + 数据源
-
正如我们上面所说。你都到了授权的步骤了。那你肯定已经登录认证了
-
就好比你去学校读书,你都在找对应的班级了,难到你还没有进入学校吗?
-
况且官方的几个数据源
Realm
的默认实现,最终也是继承自AuthorizingRealm
2、Step2
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upTk = (UsernamePasswordToken) token;
// 根据用户名,在数据库查询用户
String username = (String) upTk.getPrincipal();
User user = Dbs.get(username);
// 判断是否有该用户
if (user == null) return null;
// 不需要验证密码
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
}
复制代码
- 实现
doGetAuthenticationInfo
方法【先认证】 - 当主体
subject
需要认证时,就会调用doGetAuthenticationInfo
方法 @param token
是调用subject.login(token)
时,传入的token- 一般情况下,需要在这里根据用户名查询用户的具体信息【用户名、密码等】
Q:为什么验证用户名和密码?
- 我们这里只是用用户名和密码举例,你可以进行其他操作。
- 你也可以验证其他的东西,比如自定义
token
规则。也就是校验规则。我们之后在谈 - 这里先带大家看看两个
默认Token
中的方法,熟悉两个shiro
里的名词
private String username;
private char[] password;
public Object getPrincipal() { return getUsername(); }
public Object getCredentials() { return getPassword(); }
复制代码
- 用户名:
username
--->Principal
,所以之后我们简称Principal
为用户名 - 密码:
password
--->Credentials
,所以之后我们简称Credentials
为密码 - 因为它将其变成了返回
Object
,方便类型转换
Q:为什么只验证用户名而不验证密码呢?
- 因为在
Shiro
里面,这个验证密码的操作 - 有专门的部分来负责,更为专业。耦合性更低
- 比如我们可以先看看,刚刚我们使用过的
.ini
,它里面是如何实现的
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) { }
if (account.isCredentialsExpired()) { }
}
return account;
}
复制代码
- 我们可以看到,它这里的做法是,根据查询的用户信息
account
,检查一下有没有被锁定,有没有过期。 - 就直接将
account
返回了,我们也没有看到,他这里有验证密码吧 - 那你就有疑惑了,那它是如何验证的呢?
Credentials
的验证
- 先看主要步骤
// step1
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
// setp2
assertCredentialsMatch(token, info);
// step3
if (!cm.doCredentialsMatch(token, info)) { ... }
// step4
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials);
}
复制代码
- 当我们将查询的
account
信息直接返回之后。 shiro
会去调用利用Realm
去调用CredentialsMatcher中的方法
- 根据这个名字,我们就可以知道。这是密码匹配器。用于校验密码的
- 默认的实现。直接
equals(tokenCredentials, accountCredentials)
- 将登录的
token
时的密码与返回account
中的密码相比
认证流程
- 密码认证通过之后,我们的认证流程就走完了,那么,我们一起来总结一下其中的过程
-
从图中,我们可以看到。登录时传入了一个
token
【我们这称为用户信息令牌】 -
这个令牌会一直呆到调用完
doGetAuthenticationInfo
-
而图中出现的
info
信息,是调用完doGetAuthenticationInfo
-
返回了
account
后,才进行传递的 -
这也证实了我们上面所说的,来到这个安全系统
-
都是经过管理员
securityManager
之后的,subject.login()
也是一样 -
用文字描述一下这个流程图的关键步骤
/*
认证流程
1、Subject.login(token)
2、SecurityManager -> Authenticator -> Realm【AuthorizingRealm】
3、info = AuthorizingRealm.doGetAuthenticationInfo(token)。根据封装的token令牌,去查询对应的用户信息【如去数据库查询】
4、CredentialsMatcher.doCredentialsMatch(token, info):判断token与info中的Credentials是否正确
*/
复制代码
3、Step3
- 来到这里,你的认证肯定是已经通过了。既然认证通过了
- 那我们想要获取该用户的权限信息、角色信息。又该如何获取权限信息呢?
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 拿到刚刚已经认证通过的用户名
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 【去查询角色信息】添加角色信息
List<String> roles = Dbs.listRoles(username);
if (roles != null) {
info.addRoles(roles);
}
// 【去查询权限信息】添加权限信息
List<String> permissions = Dbs.listPermissions(username);
if (permissions != null) {
info.addStringPermissions(permissions);
}
return info;
}
复制代码
- 模拟去数据库查询用户的【角色信息、权限信息】
// 角色
public static List<String> listRoles(String username) {
if ("ciusyan".equals(username)) {
return List.of("admin", "normal");
}
return null;
}
// 权限
public static List<String> listPermissions(String username) {
if ("ciusyan".equals(username)) {
return List.of("user:create", "user:read", "user:update");
}
return null;
}
复制代码
- 当主体(subject)想要去鉴权的时候,他就会来到授权的方法
doGetAuthorizationInfo
- 例如
System.out.println("【权限】user:create -> " + subject.isPermitted("user:create"));
System.out.println("【权限】user:read -> " + subject.isPermitted("user:read"));
System.out.println("【权限】user:delete -> " + subject.isPermitted("user:delete"));
System.out.println("【角色】admin -> " + subject.hasRole("admin"));
System.out.println("【角色】normal -> " + subject.hasRole("normal"));
System.out.println("【角色】teacher -> " + subject.hasRole("teacher"));
复制代码
- 如上图所示,当主体
subject
去调用hasRole、isPermitted
等方法时 Shiro
就会去调用授权方法,检验用户的权限- 可以看到,
ciusyan
这个用户只有admin、normal
这两个角色 - 有
user:create、user:read、user:update
三种权限 - 到这里,相信你应该知道,打印结果为什么是这样了。
鉴权流程
-
注:我这里说的是去验证权限【验证角色同理】
-
当调用鉴权相关的方法时,主体又会去找到管理员
securityManager
-
管理员又会去找授权器
Authorizer
-
然后授权器为了拿到权限信息,会去调用
doGetAuthorizationInfo
方法 -
这时候就来到了我们自定义
Realm
的授权方法,在这里看看,我们给他授予了什么权限 -
获取完所有权限信息之后,会去遍历刚刚获取到的权限
-
与传进来需要去验证的权限比对
-
用文字描述一下这个流程图的关键步骤
/*
鉴权流程【验证角色、权限的流程】
1、Subject.isPermitted(permission)、Subject.hasRole(role)
2、SecurityManager -> Authorizer -> Realm【AuthorizingRealm】
3、info = AuthorizingRealm.doGetAuthorizationInfo(principals 的集合)。根据principal,去查询对应的角色、权限信息【如去数据库查询】
4、根据返回的info信息,判断权限、角色是否正确
*/
复制代码
写在后面
(1)读后思考
- 为什么需要自定义
Realm
? Shiro
中还有什么部分可以自定义?为什么需要自定义这些部分?- 认证和鉴权的大致流程?
(2)下篇预告
-
Shiro进阶指南
-
将
Shiro
集成到Web项目【Spring Boot】 -
为什么要自定义
balabala...