SpringBoot/SpringMVC整合Shiro:實現登錄與註冊(MD5加鹽加密)

本文歡迎轉載,轉載前請聯繫作者,經允許後方可轉載。轉載後請註明出處,謝謝! 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

發佈了68 篇原創文章 · 獲贊 257 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章