身份验证
身份验证,即在应用中谁能证明他就是本人,一般提供如他们的身份ID一些标识信息来表明他就是本人,如提供用户名/密码来证明。
在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从而应用能验证用户身份:
prinipals:身份,即主体的标识属性,可以是任何东西,如用户名,邮箱等,维一即可。一个主体可以有多个principals,但是只有一个primary principals,一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最长久的principals和credentials组合就是用户名/密码了。
另外两个相关的概念是之前提到的Subject以及Realm,分别是主体及验证主体的数据源。
环境准备:
新建Maven工程->添加Maven依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>20171119</groupId> <artifactId>Shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.2.RELEASE</version> <relativePath></relativePath> </parent> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.9</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>1.7.6</version> </dependency> </dependencies> </project>
登录/退出
1.首先准备一些用户身份/凭据(shiro.ini)
[users]
zhang=123
wang=123
此处使用ini配置文件,通过[users]指定了两个主体:zhang/123,wang/123.
2.测试用例
LoginLogoutTest.java
package com.fpc.test; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.junit.Test; import junit.framework.Assert; public class LoginLogoutTest { @Test public void testHelloworld() { //1.获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //2.得到SecurityManager实例后并且绑定给SecurityUtils org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); //3.得到Subject以及创建用户名/密码身份验证Token(即用户身份/凭证) Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wang","123"); try { //4.登录,即验证身份 subject.login(token); System.out.println("登录成功"); } catch(AuthenticationException e) { //抛出异常则,表明验证失败 System.out.println("登录失败"); } Assert.assertEquals(true,subject.isAuthenticated()); subject.logout(); } }
2.1、首先通过new IniSecurityManagerFactory并指定一个ini配置文件来创建一个SecurityManager工厂;
2.2、接着获取SecurityManager并绑定到SecurityUtils,这是一个全局设置,设置一次即可;
2.3、通过SecurityUtils得到Subject,其会自动绑定到当前线程;如果在web环境在请求结束时需要解除绑定;然后获取身份验证的Token,如用户名/密码;
2.4、调用subject.login方法进行登录,其会自动委托给SecurityManager.login方法进行登录;
2.5、如果身份验证失败请捕获AuthenticationException或其子类,常见的如: DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;
2.6、最后可以调用subject.logout退出,其会自动委托给SecurityManager.logout方法退出。
验证步骤:
1、收集用户身份/凭证,即如用户名/密码;
2、调用Subject.login进行登录,如果失败将得到相应的AuthenticationException异常,根据异常提示用户错误信息;否则登录成功;
3、最后调用Subject.logout进行退出操作。
如上测试的几个问题:
1、用户名/密码硬编码在ini配置文件,以后需要改成如数据库存储,且密码需要加密存储;
2、用户身份Token可能不仅仅是用户名/密码,也可能还有其他的,如登录时允许用户名/邮箱/手机号同时登录。
Realm
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。如我们之前的ini配置方式将使用org.apache.shiro.realm.text.IniRealm。
Realm接口的一些方法:
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException { // TODO Auto-generated method stub return null; } public String getName() { // TODO Auto-generated method stub return null; } public boolean supports(AuthenticationToken arg0) { // TODO Auto-generated method stub return false; }
1.单Realm配置
自定义Realm实现:CusRealm.java
package com.fpc.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.realm.Realm; public class CusRealm implements Realm { @Override public AuthenticationInfo getAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException { // TODO Auto-generated method stub String username = (String)arg0.getPrincipal();//得到用户名 String password = new String((char[])arg0.getCredentials());//得到密码 if( !username.equals("zhang")) { throw new UnknownAccountException();//用户名错误 } if (!password.equals("123")) { throw new IncorrectCredentialsException();//密码错误 } //如果不抛出异常,则身份验证成功,返回一个AuthenticationInfo实现 return new SimpleAuthenticationInfo(username,password,getName()); } @Override public String getName() { // TODO Auto-generated method stub return "CusRealm"; } @Override public boolean supports(AuthenticationToken arg0) { // TODO Auto-generated method stub //仅支持UsernamePasswordToken类型的Token return arg0 instanceof UsernamePasswordToken; } }
ini配置文件指定自定义Realm实现(shiro-realm.ini),通过$name来引入之前的realm定义
[main] CusRealm=com.fpc.realm.CusRealm securityManager.realms=$CusRealm
测试用例参考上面的LoginLogoutTest.java:把之前的shiro.ini替换成shiro-realm.ini即可。
@Test public void testCustomRealm() { Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini"); org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wang","123"); try { subject.login(token); System.out.println("登录成功"); } catch (AuthenticationException e) { // e.printStackTrace(); System.out.println("登录失败"); } }
2.多Realm配置
1.ini配置文件(shiro-multi-realm.ini)
[main] CusRealm1=com.fpc.realm.CusRealm1 CusRealm2=com.fpc.realm.CusRealm2 securityManager.realms=$CusRealm1,$CusRealm2
securityManager会按照realms指定的顺序进行身份验证。此处我们使用显示指定顺序的方式指定了Realm的顺序,如果删除“securityManager.realms=$CusRealm1,$CusRealm2”,那么securityManager会按照realm声明的顺序进行使用(即无需设置realms属性,其会自动发现),当我们显示指定的realm后,其他没有指定的realm也不会被忽略,如果“securityManager.realms=$CusRealm1”,那么CusRealm2也会被自动设置进去。
当我们设置shiro-multi-realm.ini为下面这样时:
[main] CusRealm1=com.fpc.realm.CusRealm1 CusRealm1=com.fpc.realm.CusRealm2 securityManager.realms=$CusRealm1
我们的CusRealm1和CusRealm2中的内容如下:
CusRealm1.java:
package com.fpc.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.realm.Realm; public class CusRealm1 implements Realm { @Override public AuthenticationInfo getAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException { // TODO Auto-generated method stub String username = (String)arg0.getPrincipal();//得到用户名 String password = new String((char[])arg0.getCredentials());//得到密码 if( !username.equals("wang")) { throw new UnknownAccountException();//用户名错误 } if (!password.equals("123")) { throw new IncorrectCredentialsException();//密码错误 } //如果不抛出异常,则身份验证成功,返回一个AuthenticationInfo实现 return new SimpleAuthenticationInfo(username,password,getName()); } @Override public String getName() { // TODO Auto-generated method stub return "CusRealm"; } @Override public boolean supports(AuthenticationToken arg0) { // TODO Auto-generated method stub //仅支持UsernamePasswordToken类型的Token return arg0 instanceof UsernamePasswordToken; } }
CusRealm2.java:
package com.fpc.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.Realm; public class CusRealm2 implements Realm{ @Override public String getName() { // TODO Auto-generated method stub return "CusRealm2"; } @Override public boolean supports(AuthenticationToken token) { // TODO Auto-generated method stub return token instanceof UsernamePasswordToken; } @Override public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // TODO Auto-generated method stub String username = (String) token.getPrincipal(); String password = new String((char[])token.getCredentials()); if (!username.equals("zhang")) { throw new UnknownAccountException(); } if ( !password.equals("123")) { throw new IncorrectCredentialsException(); } return new SimpleAuthenticationInfo(username,password,getName()); } }
由上面的代码可知,能通过CusRealm1验证的只有wang/123,能通过CusRealm2验证的只有zhang/123。再看看我们的测试类中的代码:
@Override public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // TODO Auto-generated method stub String username = (String) token.getPrincipal(); String password = new String((char[])token.getCredentials()); if (!username.equals("zhang")) { throw new UnknownAccountException(); } if ( !password.equals("123")) { throw new IncorrectCredentialsException(); } return new SimpleAuthenticationInfo(username,password,getName()); }
我们测试的用户token是zhang/123,那么我们运行看一下到的是否会登录成功?
也就是说,只要再ini文件中声明的Realm,即使没有指定,其也不会被忽略。
那么我们再尝试将CusRealm2在ini文件中的声明删除?,ini文件如下:
[main] CusRealm1=com.fpc.realm.CusRealm1 securityManager.realms=$CusRealm1
再次运行LoginLogoutTest.java
这也证明了,Realm能否被识别,只跟它在ini文件中有没有被声明有关系,跟它有没有被指定没关系。
JDBC Realm使用
1.使用mysql数据库和druid连接池;要使用mysql,需要在Maven项目的pom.xml中添加mysql连接所需的依赖:
<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>
2.到数据库shiro下建三张表:user(用户名/密码),user_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) ); 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) ); 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');
3.ini配置(shiro-jdbc-realm.ini)
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://10.0.20.252:3306/shiro
dataSource.username=root
dataSource.password=Free-Wi11
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm
1.变量名=全限定类名会自动创建一个类实例
2.变量名.属性=值 自动调用相应的setter方法进行赋值
3.$变量名 引用之前的一个对象实例
4.编写测试方法:
@Test public void testJdbcRealm() { Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-jdbc-realm.ini"); org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhang","123"); try { subject.login(token); System.out.println("登录成功"); } catch (AuthenticationException e) { // e.printStackTrace(); System.out.println("登录失败"); } Assert.assertEquals(true,subject.isAuthenticated()); subject.logout(); }
运行结果:
如果更改数据库中的密码再运行呢?