Shiro是Apache基金會下的一個開源安全框架,提供了身份驗證、受權、密碼學和會話管理等功能,Shiro框架不只直觀易用,並且也能提供健壯的安全性,另一點值得說的是Shiro的前身是一個始於2004的開源項目JSecurity,該項目於2008年加入Apache,並於2010年成爲Apache的頂級項目。
OK,以上是關於Shiro的一點簡單介紹,實際上,我在以前有一篇關於權限控制的博客在Spring Boot中使用Spring Security實現權限控制,Shiro的功能沒有Spring Security強大,可是對於通常的項目而言也是夠用了,並且用起來要更簡單一些。OK,關於這兩個孰優孰劣,咱們不去爭論,咱們這裏主要來看Shiro的使用。java
在Shiro中,任意一個和程序交互的主體都是一個Subject,Subject包括可是不限於用戶,全部的Subject都綁定到SecurityManager上 ,全部交由Subject處理的操做最終都會委託給SecurityManager。mysql
SecurityManager單從字面上咱們能夠理解爲一個安全管理器,全部的Subject都由SecurityManager來管理,它是Shiro的核心,SecurityManager有點相似於Spring框架中的DispatcherServlet。git
Shiro在運行的過程當中,從Realm中獲取安全數據,好比用戶的權限、角色等,每當SecurityManager要驗證用戶身份的時候,那麼他就從Realm中獲取相應的數據進行比對,這個有點相似於DAO,由它提供數據源。github
經過上面的簡單介紹,咱們能夠大體梳理一下一個Shiro應用的流程:
代碼經過Subject來進行認證和受權等操做,而Subject又將這個操做委託給SecurityManager,咱們將要驗證的數據源注入到Realm中,SecurityManager在Realm中查詢數據進行驗證。sql
OK,經過以上的介紹,相信小夥伴對Shiro已經有了一個基本的認識,接下來咱們來看看幾個基本的操做。數據庫
我這裏使用IntelliJ IDEA做爲開發工具,咱們先來建立一個基本的Maven工程,而後添加以下依賴:apache
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
單元測試框架、日誌框架和一個shiro-core三個東西就足夠了。後端
咱們這裏先經過ini文件建立一個簡單的數據源,注意文件的位置:
OK,shiro.ini文件的內容以下:安全
[users]
zuolengchan=456
注意上面是users,有s,users表示對用戶、密碼、角色等的配置。咱們這裏提供了一個用戶名爲zuolengchan,密碼爲456的用戶。markdown
OK,接下來,咱們經過一個簡單的單元測試來看看怎麼登陸,以下:
@Test
public void test1() {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zuolengchan", "456");
try {
subject.login(token);
System.out.println("登陸成功");
} catch (AuthenticationException e) {
//登陸失敗
System.out.println("登陸失敗");
e.printStackTrace();
}
subject.logout();
}
關於以上測試文件,我說如下幾點:
1.首先咱們須要獲取SecurityManager工廠,獲取的時候經過ini配置文件來初始化。
2.經過SecurityManager工廠獲取到SecurityManager的實例,並將該實例綁定給SecurityUtils,設置給SecurityUtils是一個全局設置,設置一次便可。
3.從SecurityUtils中獲取到一個Subject實例
4.經過UsernamePasswordToken對象建立用戶名密碼身份驗證Token
5.調用Subject中的login方法執行登陸操做,Subject會將這個登陸操做自動委託給SecurityManager去執行。
6.在登陸過程當中,若是沒有拋異常,說明登陸成功,若是拋異常,說明登陸失敗
7.調用logout方法咱們能夠退出登陸,退出登陸的操做也是委託給SecurityManager去執行。
OK,咱們在登陸的過程當中,輸入正確的用戶名和密碼就能成功登陸。
OK,上個案例中,咱們在ini文件中預設了數據源,固然咱們也能夠自定義Realm,前面咱們也說過Realm至關因而咱們的數據源,咱們能夠在Realm中來進行數據匹配,自定義Realm須要咱們實現Realm接口,該接口中有三個方法須要咱們實現:
1.getName()方法用來返回該Realm的惟一名字。
2.supports(AuthenticationToken token)方法用來驗證傳入的Token是否被支持,由於咱們驗證方式不只僅Username和Password這種方式,還有不少其餘方式。
3.getAuthenticationInfo(AuthenticationToken token)這個方法用來執行驗證操做,而且將驗證結果返回。
OK,有了上面的介紹,接下來咱們來實現一個Realm。
public class MyRealm1 implements Realm {
public String getName() {
return "myrealm1";
}
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof UsernamePasswordToken;
}
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
String password = new String(((char[]) token.getCredentials()));
if (!"wang".equals(username)) {
//用戶名錯誤
throw new UnknownAccountException();
}
if (!"123".equals(password)) {
//密碼錯誤
throw new IncorrectCredentialsException();
}
return new SimpleAuthenticationInfo(username, password, getName());
}
}
getName()方法返回了當前Realm的名字,supports方法判斷是不是Username+Password的驗證,getAuthenticationInfo方法中進行驗證,首先獲取到傳入的用戶名和密碼,而後進行比對,若是不存在用戶名就拋出UnknownAccountException異常,若是密碼錯誤則拋出IncorrectCredentialsException異常,最後將結果返回。
這裏的ini文件就比較簡單了,以下:
myRealm1=org.sang.MyRealm1
securityManager.realms=$myRealm1
首先定義myRealm1,值爲咱們自定義Realm的全路徑,而後設置securityManager的realms屬性便可,OK,作完這些咱們就能夠測試啦。
@Test
public void test2() {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "456");
try {
subject.login(token);
System.out.println("登陸成功");
} catch (AuthenticationException e) {
//登陸失敗
System.out.println("登陸失敗");
e.printStackTrace();
}
subject.logout();
}
若是咱們在登陸的時候輸入正確的用戶名和密碼就可以成功登陸,若是有任意一個輸錯,則會拋出相應的異常。
OK,以上是咱們自定義一個Realm,事實上咱們能夠自定義多個,以下:
public class MyRealm2 implements Realm {
public String getName() {
return "myrealm2";
}
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof UsernamePasswordToken;
}
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 (!"456".equals(password)) {
//密碼錯誤
throw new IncorrectCredentialsException();
}
return new SimpleAuthenticationInfo(username, password, getName());
}
}
自定義多個Realm以後須要咱們在ini文件中配置多個,以下:
myRealm1=org.sang.MyRealm1
myRealm2=org.sang.MyRealm2
securityManager.realms=$myRealm1,$myRealm2
這裏若是咱們不寫最後 一行也是能夠的,Shiro會自動將前面定義的myRealm1,myRealm2設置給SecurityManager的realms屬性。
OK,這裏的測試方式和上文一致,再也不贅述。
上面的數據源都是咱們提早定義好,寫死的,固然咱們也能夠將數據定義在數據庫中,好比,我建立以下三張表,並預設一條數據:
drop database if exists shiro1;
create database shiro1 default character set=utf8 collate utf8_bin;
use shiro1;
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;
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;
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;
create unique index idx_roles_permissions on roles_permissions(role_name,permission);
insert into users(username,password) values('wang','111');
而後在Maven中添加數據庫驅動和鏈接池:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.29</version>
</dependency>
在ini文件中配置JdbcRealm,以下:
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/shiro1?useUnicode=true&characterEncoding=UTF-8
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
securityManager.realm=$jdbcRealm
jdbcRealm定義了咱們要用的JdbcRealm,在最後將之設置給SecurityManager的realm屬性,jdbcRealm中還有dataSource,就是咱們自定義的數據源,其餘的都是數據庫鏈接的東西,我就再也不贅述。
@Test
public void test3() {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-jdbc-realm.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("wang", "111");
try {
subject.login(token);
System.out.println("登陸成功");
} catch (AuthenticationException e) {
//登陸失敗
System.out.println("登陸失敗");
e.printStackTrace();
}
subject.logout();
}
測試方式仍是和前文一致。再也不囉嗦。
Shiro中有三種不一樣的驗證策略,以下:
1.FirstSuccessfulStrategy:表示只要有一個Realm驗證成功便可,而後返回第一個Realm身份驗證成功的認證信息,其餘的忽略。
2.AtLeastOneSuccessfulStrategy:表示只要有一個Realm驗證成功便可,可是它會將全部的成功驗證成功的信息返回。
3.AllSuccessfulStrategy:表示全部的Realm驗證成功纔算成功,且返回全部Realm身份驗證成功的認證信息。
Shiro默認使用了第二種策略。
OK,假設我如今有三個Realm,分別以下:
public class MyRealm3 implements Realm {
public String getName() {
return "myrealm3";
}
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof UsernamePasswordToken;
}
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();
}
return new SimpleAuthenticationInfo(username, password, getName());
}
}
public class MyRealm4 implements Realm {
public String getName() {
return "myrealm4";
}
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof UsernamePasswordToken;
}
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
String password = new String(((char[]) token.getCredentials()));
if (!"wang".equals(username)) {
//用戶名錯誤
throw new UnknownAccountException();
}
if (!"123".equals(password)) {
//密碼錯誤
throw new IncorrectCredentialsException();
}
return new SimpleAuthenticationInfo(username, password, getName());
}
}
public class MyRealm5 implements Realm {
public String getName() {
return "myrealm5";
}
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof UsernamePasswordToken;
}
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();
}
return new SimpleAuthenticationInfo("zhang@163.com", password, getName());
}
}
而後在ini文件中配置驗證策略:
#指定securityManager的authenticator實現
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#驗證策略
allSuccessfulStrategy= org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
myRealm3=org.sang.MyRealm3
myRealm4=org.sang.MyRealm4
myRealm5=org.sang.MyRealm5
securityManager.realms=$myRealm3,$myRealm5,$myRealm4
這個表示我一會驗證的時候,驗證的條件只要有一個知足便可驗證成功,而且在驗證成功後系統會返回全部的驗證信息給我。
固然,我也能夠給allSuccessfulStrategy設置另外兩種值,以下:
allSuccessfulStrategy= org.apache.shiro.authc.pam.AllSuccessfulStrategy
allSuccessfulStrategy= org.apache.shiro.authc.pam.FirstSuccessfulStrategy
OK,咱們來看看測試代碼:
@Test
public void test4() {
login("classpath:shiro-authenticator-all-success.ini");
Subject subject = SecurityUtils.getSubject();
PrincipalCollection principals = subject.getPrincipals();
Iterator iterator = principals.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
private void login(String configFile) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory(configFile);
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) {
System.out.println("登陸失敗");
e.printStackTrace();
}
}
登陸成功以後將全部的認證信息打印出來。
OK,以上就是Shiro的一個簡單應用。
本文案例地址:
https://github.com/lenve/Shiro
更多JavaEE資料請移步這裏:
https://github.com/lenve/JavaEETest
參考資料:
張開濤大神的《跟我學Shiro》,原文鏈接http://jinnianshilongnian.iteye.com/blog/2018398
關注公衆號【江南一點雨】,專一於 Spring Boot+微服務以及先後端分離等全棧技術,按期視頻教程分享,關注後回覆 Java ,領取鬆哥爲你精心準備的 Java 乾貨!