身份验证简介
在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:
principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是邮箱 / 手机号。
credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
最常见的 principals 和 credentials 组合就是用户名 / 密码了。
身份认证流程
- 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager的login方法,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
- SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
- Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
- Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
- Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
Realm
什么是Realm
Shiro从Realm获取安全数据(如用户、角色、权限),SecurityManager要验证用户身份,它需要从Realm获取相应的用户进行比较以确定用户身份是否合法,也需要从Realm获取角色/权限,可以将Realm看成DataSource,即安全数据源。Realm可以配置单个或多个。
单Realm配置
自定义Realm实现
package com.shiro.realm;
public class MyRealm1 implements Realm{
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal(); //获取用户名
String password = new String((char[]) token.getCredentials()); //获取密码
//这里是模拟数据,实际开发中需要从数据库中查询
if(!"zhang".equals(username)){
throw new UnknownAccountException();
}
if(!"123".equals(password)){
throw new IncorrectCredentialsException();
}
//如果身份验证成功,返回一个AuthenticationInfo实现
return new SimpleAuthenticationInfo(username,password,getName());
}
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken; //仅支持UsernamePasswordToken类型的Token
}
public String getName() {
//设置Realm名称
return "myRealm1";
}
}
ini配置文件指定自定义Realm:
[main]
#声明自定义的Realm
myRealm1=com.shiro.realm.MyRealm1
#指定SecurityManager的realms实现
securityManager.realms=$myRealm1
多Realm配置
ini配置文件配置
[main]
#声明自定义的Realm
myRealm1=com.shiro.realm.MyRealm1
myRealm2=com.shiro.realm.MyRealm2
#指定SecurityManager的realms实现
securityManager.realms=$myRealm1,$myRealm2
securityManager会按照realms指定的顺序进行身份认证,如果删除“securityManager.realms=$myRealm1,$myRealm2”,那么SecurityManager会按照Realm声明的顺序执行,当显示的指定realms时,未被指定的realm不会被执行。
shiro默认提供的Realm
以后一般继承AuthorizingRealm即可,它继承了AuthenticatingRealm(身份验证),间接继承了CachingRealm(带有缓存实现)。其中主要默认实现如下:
- org.apache.shiro.realm.text.IniRealm:[users] 部分指定用户名 / 密码及其角色;[roles]部分指定角色即权限信息;
- org.apache.shiro.realm.text.PropertiesRealm:user.username=password,role1,role2 指定用户名 /密码及其角色;role.role1=permission1,permission2 指定角色及权限信息;
- org.apache.shiro.realm.jdbc.JdbcRealm:通过 sql 查询相应的信息,如 “select password from users where username = ?” 获取用户密码,“select password, password_salt from users where username = ?” 获取用户密码及盐;“select role_name from user_roles where username = ?” 获取用户角色;“select permission from roles_permissions where role_name = ?”获取角色对应的权限信息;也可以调用相应的 api 进行自定义 sql;
JDBC Realm 使用
在第一节中的maven配置中添加如下依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>0.2.23</version>
</dependency>
在数据库shiro下建三张表:users、users_roles、roles_permissions;
drop database if exists shiro;
create database shiro;
use shiro;
create table users (
id bigint auto_increment,
username varchar(100),
password varchar(100),
password_salt varchar(100),
constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_users_username on users(username);
create table user_roles(
id bigint auto_increment,
username varchar(100),
role_name varchar(100),
constraint pk_user_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_user_roles on user_roles(username, role_name);
create table roles_permissions(
id bigint auto_increment,
role_name varchar(100),
permission varchar(100),
constraint pk_roles_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_roles_permissions on roles_permissions(role_name, permission);
insert into users(username,password)values('zhang','123');
ini配置
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiro
dataSource.username=root
dataSource.password=
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm
Authenticator及AuthenticationStrategy
Authenticator 的职责是验证用户帐号,是 Shiro API 中身份验证核心的入口点:
public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException
如果验证成功,将返回 AuthenticationInfo 验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的 AuthenticationException 实现。
SecurityManager接口继承了Authenticator, Authorizer, SessionManager
上述介绍了多Realm的使用,但遗留下了一个问题:多Realm验证怎样才算验证成功?一个Reaml验证成功即可,还是全部验证成功方可。shiro提供了AuthenticationStrategy(验证策略),Authenticator的另外一个ModularRealmAuthenticator实现的doAuthenticate方法会进行判断单Realm或多Realm验证,当多Realm验证时,验证规则通过AuthenticationStrategy接口指定,默认提供的实现:
- FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm身份验证成功的认证信息,其他的忽略;
- AtLeastOneSuccessfulStrategy:只要有一个 Realm 验证成功即可,和FirstSuccessfulStrategy 不同,返回所有 Realm 身份验证成功的认证信息;
- AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm身份验证成功的认证信息,如果有一个失败就失败了。
ModularRealmAuthenticator 默认使用AtLeastOneSuccessfulStrategy 策略。
ini配置如下:
[main]
#指定securityManager的authenticator实现
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#指定securityManager.authenticator的authenticationStrategy
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
myRealm1=com.shiro.realm.MyRealm1
myRealm2=com.shiro.realm.MyRealm2
myRealm3=com.shiro.realm.MyRealm3
securityManager.realms=$myRealm1,$myRealm3
验证策略可以自定义实现,只要继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy,重写其中的方法:
//在所有Realm验证之前调用
AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException;
//在每个Realm之前调用
AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;
//在每个Realm之后调用
AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException;
//在所有Realm之后调用
AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;