本文歡迎轉載,轉載前請聯繫作者,經允許後方可轉載。轉載後請註明出處,謝謝! http://blog.csdn.net/colton_null 作者:喝酒不騎馬 Colton_Null from CSDN
終於終於,用正確姿勢搭建了一個Spring框架下整合Shiro的登錄註冊的DEMO。
因爲還有其他事情要忙,所以有關這個shiro的demo前前後後鼓搗了一週多。爲啥多花這麼多時間?
按照平常,我在研究新框架的時候,都是看看框架介紹,找找樣例幾天就能搞出來了。但這次最氣的是,目前能夠講清楚Spring + Shiro整合方法認真寫的博客幾乎沒有。照着幾個博客跟着配置,越配越生氣的。給的配置文件不全;夾雜着好多沒用的配置文件;沒有註釋;代碼不全;還有的用法直接就是錯的。耽誤了太多的時間。
所以,就自己整理下正確實現Spirng + Shiro整合的姿勢。分享給大家,希望別再走彎路。文末有demo源碼,大家可以自行下載。
本文主要基於SpringBoot + Maven + Mybatis實現對Shiro的整合。對於Mabatis的配置在這裏就不多做介紹了。而對於SpringMVC,也是SSM框架,只不過在對Shiro的配置方式上有所不同,原理都是一樣的。我也會把SpringMVC下Shiro的配置方法貼出來。
在這裏推薦兩個個人覺得受益頗多的教程鏈接:
《跟我學shiro》
騰訊課程視頻《Shiro安全框架》
一、準備好Maven + SpringBoot + MyBatis開發環境
1.準備一個user_t表,用戶存放賬戶信息
-- ----------------------------
-- 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;
2.編寫兩個DAO接口
分別用於根據username查詢信息以及插入一條數據。
UserMapper.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 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依賴
pom中有關shiro的依賴如下
<!-- 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類
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(SecurityManager securityManager)方法,是設置shiro的過濾規則。用於控制哪些請求需要身份認證後才能繼續執行,哪些不需要認證等。
從http://blog.csdn.net/u010092167/article/details/52372811這個博主的博文中找到攔截器彙總表。
身份驗證相關:
- 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(HashedCredentialsMatcher matcher)用於配置自定義的Realm。在Shiro中,所有有關身份認證及授權管理數據源的獲取與管理,都在Realm中進行。
3.hashedCredentialsMatcher()用於生成加密規則。這裏採用MD5加密1024次的方式對密碼進行加密處理。
4.securityManager(HashedCredentialsMatcher matcher)將加密規則屬性設置到自定義的ShiroRealm中,並將這個Realm加載到SecurityManager中。
四、配置自定義Realm
ShiroRealm.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(AuthenticationToken authenticationToken)方法。
2.這個方法主要就是用於獲取數據庫中的賬戶信息,以便用於和用戶登錄時從前臺傳過來的賬戶密碼進行對比。
3.根據用戶名到用戶表中查詢賬戶名密碼,並設置好鹽值。這裏的鹽值要和ShiroConfig中的鹽值規則一樣。將賬戶名,密碼,鹽值,realmName實例化到SimpleAuthenticationInfo中交給Shiro來管理。
4.如果賬戶不存在,則拋出AuthenticationException異常。
5.這樣,每次用戶進行login操作時,就會調用doGetAuthenticationInfo方法。Shiro就自動幫我們校驗了賬戶密碼是否匹配。
五、實現登錄
這裏先貼上Controller的全部代碼
MyController.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下有多個子異常,用於各種登錄失敗的場景,比如賬戶名不存在,密碼不對,登錄次數過多等等。大家針對不同的情況做不同的處理。但有一點建議,就是對於前臺用戶來說,不要暴露過多的錯誤信息,只是報一個登錄失敗即可,提高安全性。
六、實現註冊
在Controller的doRegister方法中,調用service層的registerData方法,完成註冊功能。
在service中對DAO進行封裝,實現信息查詢以及信息註冊。
接口BaseService.java
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);
}
BaseService接口的實現類
BaseServiceImpl.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。
七、大功告成
到此,有關SpringBoot與Shiro的整合就基本完成了。有關靜態頁的代碼這裏就不貼了,在下面的github源碼中有,大家可以自行下載。
最終效果就是,在http://localhost/register界面中,完成賬戶的註冊。在http://localhost/login界面中進行登錄操作。如果登錄成功,則跳轉到index頁。如果登錄失敗或者沒有登錄,則訪問index時會自動跳轉到login界面。
八、有關SpringMVC環境下的配置
在SpringMVC下,有關Shiro的配置就要在Spring配置文件中來完成。
配置原理和SpringBoot中的一樣。
spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="successUrl" value="/views/success.jsp"/>
<property name="unauthorizedUrl" value="/views/test.jsp"/>
<!-- 配置攔截策略 -->
<property name="filterChainDefinitions">
<value>
/login = anon
/doLogin = anon
/doLogout = logout
/** = authc
</value>
</property>
</bean>
<!-- 自定義Realm -->
<bean id="myRealm" class="com.myz.bean.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="1024"/>
</bean>
</property>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 緩存管理器 -->
<property name="cacheManager" ref="cacheManager" />
<property name="realm" ref="myRealm" />
</bean>
<!-- 配置ehcache -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
</bean>
<!-- 用來管理Spring容器中的Shiro常見的對象 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!-- 網絡方面 -->
<bean id="secureRemoteInvocationExecutor" class="org.apache.shiro.spring.remoting.SecureRemoteInvocationExecutor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
shiro緩存配置
ehcache-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>
九、源碼
有關SpringBoot + Maven + Mybatis + Shiro的登錄註冊demo源碼:ShiroDemo