Shiro实现身份验证
身份验证,即在应用中谁能证明他是他本人,一般提供如他们的身份ID、用户名、密码等来证明。
在Shiro中,用户需要提供principals(身份)和credentials(证明)给Shiro,从而应用能验证用户身份:
- principals: 身份,即主体的标识属性,可以是任何东西,如用户名、邮箱,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名、密码等。
- credentials: 证明、凭证,即只有主体知道的安全值,如密码、数字证书等。
入门案例(登录退出)
shiro.ini
使用ini
配置文件来模拟数据库中的数据,实际是应该从数据库中读取安全数据。
1 2 | [users] tycoding=123 |
AuthenticationTest.java 测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class AuthenticationTest { @Test public void loginLogoutTest(){ //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //2、得到SecurityManager实例,并绑定给SecurityUtils SecurityManager securityManager = (SecurityManager) factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); //3、得到Subject及创建用户名、密码身份的Token Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("tycoding","123"); try{ //4、登录,即身份验证 subject.login(token); }catch (AuthenticationException e){ //5、身份验证失败 e.printStackTrace(); } //判断用户是否已登录 //用户登录成功将返回true,否则返回false并抛出异常 System.out.println(subject.isAuthenticated()); //6、退出 subject.logout(); } } |
解释:
- 首先需要创建SecurityManager工厂,这里使用
ini
配置文件来初始化SecurityManager工厂,以后使用Spring时,可通过将SecurityManager注入到Spring容器中,就不再用加载ini
配置文件的方式。 - 通过工厂类获取到SecurityManager实例并绑定到SecurityUtils,这是一个全局设置,设置一次即可。
- 通过SecurityUtils得到Subject,其会自动绑定到当前线程;然后获取身份验证的Token,如用户名、密码等。
- 调用subject.login(token)方法登录,其会自动委托给SecurityManager.login()方法进行登录。
- 如果登录失败请捕获AuthenticationException或其子类,常见的异常有:DisabledAccountException(禁用的账号)、LockedAccountException(锁定的账号)、UnknownAccountException(错误的账号)、ExcessiveAttemptsException(登录失败次数过多)等。
- 最后调用subject.logout()退出,其会自动委托给SecurityManager.logout()退出。
身份认证流程
解释:
- 首先调用Subject.login(token)进行登录,其会自动委托给SecurityManager,调用之前必须通过SecurityUtils.setSecurityManager()设置。
- SecurityManager负责真正的身份验证逻辑;它会自动委托给Authenticator进行身份验证。
- Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现。
- Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证。
- Authenticator会把相应的token传给Realm,从Realm获取身份验证信息,若果没有返回、抛出异常表示身份验证失败。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
Realm
Realm: 域,Shiro从Realm中获取安全数据(如用户、角色、权限);SecurityManager要验证用户身份,就需要从Realm中获取相应的用户进行比较以确定用户身份是否合法,
在org.apache.shiro.realm.Realm
接口中有如下方法:
1 2 3 4 5 6 | String getName(); //返回一个唯一的Realm名字 boolean supports(AuthenticationToken token); //判断此Realm是否支持此token //根据Token获取认证信息 AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException; |
自定义Realm实现
MyRealm.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class MyRealm extends AuthorizingRealm { @Override public String getName() { return "myRealm"; } //用于授权 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } //用于身份验证 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("进入自定义realm..."); //得到用户名 Object principal = token.getPrincipal(); //得到密码,这个密码应该是从数据库中读取到的,而不是我们主动调用getCredentials()方法得到密码,这里仅为了测试 //得到密码 Object credentials = token.getCredentials(); // Object credentials = "12"; try{ //如果身份验证失败就返回null,并抛出异常 //如果身份验证成功就返回正确的信息(AuthenticationInfo的实现类) return new SimpleAuthenticationInfo(principal,credentials,getName()); }catch (AuthenticationException e){ e.printStackTrace(); return null; } } } |
注释:
为了更清晰的展示自定义Realm参与了身份验证,特意打印了一行sys
。
其中密码应该是从数据库中读取的,这里从我们模拟的ini
配置文件中读取安全信息(比如调用了service
层获取的密码)。而token.getCredentials()
是获取用户输入的密码。
注意:
注意最后的返回值类型是SimpleAuthenticationInfo(principal,credentials,realmName)
,它是AuthenticationInfo
接口的一个实现类。它会返回校验结果:
- 校验成功:返回包含安全数据的
SimpleAuthenticationInfo
实例对象。 - 校验失败:返回null。
在实际中,我们应该从数据库中读取用户安全信息,我们也并不需要手动调用getCredentials()
方法,因为在返回SimpleAuthenticationInfo
时,Shiro会自动调用getCredentials()
方法获取用户输入的密码,并与在数据库中读取到的密码进行比较,并将结果返回。
shiro-realm.ini
1 2 3 4 5 6 7 8 | [main] #声明一个realm myRealm=com.shiro.MyRealm #指定securityManager的Realm实现,使用$name来引入之前的realm定义 securityManager.realms=$myRealm [users] tycoding=123 |
注释:
这里我们使用ini
来模拟数据库数据,并在其中指定自动realm
实现,在使用Spring后,我们会将自定义realm实现注入到Spring配置文件中。
多Realm配置
1 2 3 4 5 6 7 8 9 10 | [main] #声明一个realm myRealm=com.shiro.MyRealm myRealm2=com.shiro.MyRealm2 #指定securityManager的Realm实现,使用$name来引入之前的realm定义 securityManager.realms=$myRealm,$myRealm2 [users] tycoding=123 |