# Shiro 安全模塊
[TOC]
1. 準備 SpringMVC 開發環境;
2. 引入 Shiro 依賴;
3. 配置 ShiroConfig 類;
4. 配置自定義 Realm;
5. 登陸驗證;
6. 注冊驗證;
## 準備 SpringMVC 開發環境
準備數據庫用戶表,存放用戶信息。
```sql
-- ----------------------------
-- Table structure for user_t
-- ----------------------------
DROP TABLE IF EXISTS `user_t`;
CREATE TABLE `user_t` (
`id` varchar(32) NOT NULL,
`username` varchar(64) NOT NULL,
`password` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS=1;
```
編寫 DAO 接口,用于根據 Username 查詢信息以及插入數據。
userMapper.java
```java
@Repository
public interface UserMapper {
/**
* 根據用戶名查詢用戶信息
* @param username 用戶名
* @return 將數據封裝到Map類型中
*/
public Map<String, Object> queryInfoByUsername(String username);
/**
* 插入一條數據
* @param data Map中包含id,username,password
*/
public void insertData(Map<String, String> data);
}
```
userMapper.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myz.shirodemo.dao.UserMapper">
<select id="queryInfoByUsername" parameterType="java.lang.String" resultType="java.util.Map">
SELECT id, username, password FROM user_t WHERE username = #{username,jdbcType=VARCHAR}
</select>
<insert id="insertData" parameterType="java.util.Map">
INSERT INTO user_t ( id, username,password )
VALUES ( #{id, jdbcType=VARCHAR}, #{username, jdbcType=VARCHAR},#{password, jdbcType=VARCHAR});
</insert>
</mapper>
```
## 引入 Shiro 依賴
```xml
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
<!-- shiro END-->
```
## 配置 ShiroConfig 類
```java
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 攔截器。匹配原則是最上面的最優先匹配
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
// 配置不會被攔截的鏈接
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/doLogin", "anon");
filterChainDefinitionMap.put("/doRegister", "anon");
filterChainDefinitionMap.put("/register", "anon");
// 配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
filterChainDefinitionMap.put("/doLogout", "logout");
// 剩余請求需要身份認證
filterChainDefinitionMap.put("/**", "authc");
// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
shiroFilterFactoryBean.setLoginUrl("/login");
// 未授權界面;
// shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean(name = "myShiroRealm")
public ShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){
ShiroRealm myShiroRealm = new ShiroRealm();
myShiroRealm.setCredentialsMatcher(matcher);
return myShiroRealm;
}
@Bean
public SecurityManager securityManager(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm(matcher));
return securityManager;
}
/**
* 密碼匹配憑證管理器
*
* @return
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 采用MD5方式加密
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 設置加密次數
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
}
```
1. `shirFilter`( `SecurityManagersecurityManager` ) 方法,是設置 `shiro` 的過濾規則。用于控制哪些請求需要身份認證后才能繼續執行,哪些不需要認證等。
身份驗證相關:
- authc:基于表單的攔截器;如“/**=authc”,如果沒有登錄會跳到相應的登錄頁面登錄;主要屬性:usernameParam:表單提交的用戶名參數名( username); passwordParam:表單提交的密碼參數名(password); rememberMeParam:表單提交的密碼參數名(rememberMe); loginUrl:登錄頁面地址(/login.jsp);successUrl:登錄成功后的默認重定向地址;failureKeyAttribute:登錄失敗后錯誤信息存儲 key(shiroLoginFailure);
- authcBasic:Basic HTTP 身份驗證攔截器,主要屬性: applicationName:彈出登錄框顯示的信息(application);
- logout:退出攔截器,主要屬性:redirectUrl:退出成功后重定向的地址(/); 示例“/logout=logout”
- user:用戶攔截器,用戶已經身份驗證 / 記住我登錄的都可;示例“/**=user”
- anon:匿名攔截器,即不需要登錄即可訪問;一般用于靜態資源過濾;示例“/static/**=anon”
授權相關的:
- roles:角色授權攔截器,驗證用戶是否擁有所有角色;主要屬性: loginUrl:登錄頁面地址(/login.jsp);unauthorizedUrl:未授權后重定向的地址;示例“/admin/**=roles[admin]”
- perms:權限授權攔截器,驗證用戶是否擁有所有權限;屬性和 roles 一樣;示例“/user/**=perms[“user:create”]”
- port:端口攔截器,主要屬性:port(80):可以通過的端口;示例“/test= port[80]”,如果用戶訪問該頁面是非 80,將自動將請求端口改為 80 并重定向到該 80 端口,其他路徑 / 參數等都一樣
- rest:rest 風格攔截器,自動根據請求方法構建權限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)構建權限字符串;示例“/users=rest[user]”,會自動拼出“user:read,user:create,user:update,user:delete”權限字符串進行權限匹配(所有都得匹配,isPermittedAll);
- ssl:SSL 攔截器,只有請求協議是 https 才能通過;否則自動跳轉會 https 端口(443);其他和 port 攔截器一樣;
- noSessionCreation:不創建會話攔截器,調用 subject.getSession(false) 不會有什么問題,但是如果 subject.getSession(true) 將拋出 DisabledSessionException 異常;
2. `myShiroRealm`(`HashedCredentialsMatchermatcher`) 用于配置自定義的 `Realm` 。在 `Shiro` 中,所有有關身份認證及授權管理數據源的獲取與管理,都在 `Realm` 中進行。
3. `hashedCredentialsMatcher()` 用于生成加密規則。這里采用 `MD5` 加密 `1024` 次的方式對密碼進行加密處理。
4. `securityManager( `HashedCredentialsMatchermatcher` )` 將加密規則屬性設置到自定義的 `ShiroRealm` 中,并將這個 `Realm` 加載到 `SecurityManager` 中。
## 配置自定義 Realm
```java
public class ShiroRealm extends AuthenticatingRealm {
@Autowired
private BaseService baseService;
private SimpleAuthenticationInfo info = null;
/**
* 1.doGetAuthenticationInfo,獲取認證消息,如果數據庫中沒有數,返回null,如果得到了正確的用戶名和密碼,
* 返回指定類型的對象
*
* 2.AuthenticationInfo 可以使用SimpleAuthenticationInfo實現類,封裝正確的用戶名和密碼。
*
* 3.token參數 就是我們需要認證的token
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 將token裝換成UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
// 獲取用戶名即可
String username = upToken.getUsername();
// 查詢數據庫,是否查詢到用戶名和密碼的用戶
Map<String, Object> userInfo = baseService.queryInfoByUsername(username);
if(userInfo != null) {
// 如果查詢到了,封裝查詢結果,返回給我們的調用
Object principal = userInfo.get("username");
Object credentials = userInfo.get("password");
// 獲取鹽值,即用戶名
ByteSource salt = ByteSource.Util.bytes(username);
String realmName = this.getName();
// 將賬戶名,密碼,鹽值,realmName實例化到SimpleAuthenticationInfo中交給Shiro來管理
info = new SimpleAuthenticationInfo(principal, credentials, salt,realmName);
}else {
// 如果沒有查詢到,拋出一個異常
throw new AuthenticationException();
}
return info;
}
}
```
1. 這里我只做了身份認證。新建一個 `ShiroRealm` 類繼承 `AuthenticatingRealm` 類,實現 `doGetAuthenticationInfo( AuthenticationTokenauthenticationToken` ) 方法。
2. 這個方法主要就是用于獲取數據庫中的賬戶信息,以便用于和用戶登錄時從前臺傳過來的賬戶密碼進行對比。
3. 根據用戶名到用戶表中查詢賬戶名密碼,并設置好鹽值。這里的鹽值要和 `ShiroConfig` 中的鹽值規則一樣。將賬戶名,密碼,鹽值, `realmName` 實例化到 `SimpleAuthenticationInfo` 中交給 `Shiro` 來管理。
4. 如果賬戶不存在,則拋出 `AuthenticationException` 異常。
5. 這樣,每次用戶進行 `login` 操作時,就會調用 `doGetAuthenticationInfo` 方法。 `Shiro` 就自動幫我們校驗了賬戶密碼是否匹配。
## 登陸驗證
```java
@Controller
public class MyController {
@Autowired
private BaseService baseService;
private final Logger logger = LoggerFactory.getLogger(MyController.class);
@RequestMapping("/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password) {
// 創建Subject實例
Subject currentUser = SecurityUtils.getSubject();
// 將用戶名及密碼封裝到UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
currentUser.login(token);
// 判斷當前用戶是否登錄
if (currentUser.isAuthenticated() == true) {
return "/index.html";
}
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登錄失敗");
}
return "/loginPage.html";
}
@RequestMapping("/doRegister")
public String doRegister(@RequestParam("username") String username,
@RequestParam("password") String password) {
boolean result = baseService.registerData(username,password);
if(result){
return "/login";
}
return "/register";
}
@RequestMapping(value = "/login")
public String login() {
logger.info("login() 方法被調用");
return "loginPage.html";
}
@RequestMapping(value = "/register")
public String register() {
logger.info("register() 方法被調用");
return "registerPage.html";
}
@RequestMapping(value = "/hello")
public String hello() {
logger.info("hello() 方法被調用");
return "helloPage.html";
}
}
```
1. 在 `doLogin` 方法中,實現登錄認證過程。
2. 首先獲取當前 `Subject` 實例
3. 將用戶名和密碼封裝到 `UsernamePasswordToken` 中
4. 用當前 `Subject` 實例執行 `login` 方法,傳入參數為剛剛封裝的 `token` 。執行 `login` 方法后, `shiro` 框架最終就會調用剛剛自定義 `ShiroRealm` 中的 `doGetAuthenticationInfo` 方法。
5. 用 `isAuthenticated()` 方法判斷用戶是否已經登錄,如果是則跳轉到登錄后的頁面(這里我跳轉到的是 `index.html`)。如果登錄失敗,則走報異常,最后還是跳轉到登錄界面。
6. 這里我只 `catch` 了 `AuthenticationException` 異常。然而在 `AuthenticationException` 下有多個子異常,用于各種登錄失敗的場景,比如賬戶名不存在,密碼不對,登錄次數過多等等。大家針對不同的情況做不同的處理。但有一點建議,就是對于前臺用戶來說,不要暴露過多的錯誤信息,只是報一個登錄失敗即可,提高安全性。
## 注冊驗證
在 `service` 中對 `DAO` 進行封裝,實現信息查詢以及信息注冊。
```
public interface BaseService {
/**
* 根據用戶名查詢用戶信息
* @param username 用戶名
* @return 將數據封裝到Map類型中
*/
public Map<String, Object> queryInfoByUsername(String username);
/**
* 注冊功能
* @param username 用戶名
* @param password 密碼
* @return
*/
public boolean registerData(String username, String password);
}
```
## MD5 加密
```java
@Service
public class BaseServiceImpl implements BaseService {
@Autowired
private UserMapper userMapper;
@Override
public Map<String, Object> queryInfoByUsername(String username) {
return userMapper.queryInfoByUsername(username);
}
@Override
public boolean registerData(String username, String password) {
// 生成uuid
String id = UUIDUtil.getOneUUID();
// 將用戶名作為鹽值
ByteSource salt = ByteSource.Util.bytes(username);
/*
* MD5加密:
* 使用SimpleHash類對原始密碼進行加密。
* 第一個參數代表使用MD5方式加密
* 第二個參數為原始密碼
* 第三個參數為鹽值,即用戶名
* 第四個參數為加密次數
* 最后用toHex()方法將加密后的密碼轉成String
* */
String newPs = new SimpleHash("MD5", password, salt, 1024).toHex();
Map<String, String> dataMap = new HashMap<>();
dataMap.put("id", id);
dataMap.put("username", username);
dataMap.put("password", newPs);
// 看數據庫中是否存在該賬戶
Map<String, Object> userInfo = queryInfoByUsername(username);
if(userInfo == null) {
userMapper.insertData(dataMap);
return true;
}
return false;
}
}
```
1. 注冊時注意,由于之前配置了鹽值規則及加密規則,所以這里要對用戶輸入的密碼也做相同的處理之后再存入數據庫中。
2. 使用 `SimpleHash` 類完成密碼的加密。最后用 `toHex()` 將加密后的密碼轉成 `String` 。
## Shiro 緩存配置
```xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shirocache">
<diskStore path="java.io.tmpdir"/>
<!-- 登錄記錄緩存 鎖定10分鐘 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authorizationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authenticationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro_cache"
maxElementsInMemory="2000"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
maxElementsOnDisk="0"
overflowToDisk="true"
memoryStoreEvictionPolicy="FIFO"
statistics="true">
</cache>
</ehcache>
```