spring-boot+mybatisPlus+shiro的集成demo 我用了5天


spring-boot + mybatis-plus + shiro 的集成demo我用了五天

  關於shiro框架,我還是從飛機哪里聽來的,就連小賤都知道,可我母雞啊。簡單百度了下,結論很好上手,比spring的security要簡單許多...於是我就是開始了我的shiro學習之路 。正巧這幾天在研究spring-boot集成mybatis-plus 於是乎我就把shiro也揉了進去,但是效果並不像我預期想象的那樣。

以下是我這幾天血淚換來的成果-->

基本概念

shiro :隸屬Apache 簡單易用的java安全框架 三大件 :Subject, SecurityManager 和 Realm
Subject:即當前操作用戶
SecurityManager:安全管理器
Realm:用戶數據的概念,域

過程:

1)建表 數據庫一般至少有五張表

1-user:用戶賬號密碼

2-role:角色ID,一個賬戶可以有很多角色

3-permission權限ID,一個角色可以有很多權限

4-user_role關系對照表:記錄每個userID有的角色

5-role_permission關系對照表,記錄每個role有的permission

-- 用戶表
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info` (
`uid` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`pass_word` varchar(255) DEFAULT NULL COMMENT '密碼',
`salt` varchar(255) DEFAULT NULL COMMENT '加密',
`state` tinyint(4) NOT NULL COMMENT '狀態',
`username` varchar(255) DEFAULT NULL COMMENT '用戶名',
`email` varchar(64) DEFAULT NULL COMMENT '郵箱',
`crtime` datetime DEFAULT NULL COMMENT '創建時間',
PRIMARY KEY (`uid`),
UNIQUE KEY(`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 角色表
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`available` bit(1) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`role` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- 用戶角色表
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`uid` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
 PRIMARY KEY(`uid`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 權限表
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` int(11) NOT NULL COMMENT 'PMK',
`available` bit(1) DEFAULT NULL COMMENT '是否激活',
`name` varchar(255) DEFAULT NULL,
`parent_id` bigint(20) DEFAULT NULL,
`parent_ids` varchar(255) DEFAULT NULL,
`permission` varchar(255) DEFAULT NULL COMMENT '權限',
`resource_type` enum('menu','button') DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 角色權限表
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`permission_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
 PRIMARY KEY(`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2)項目搭建

spring-boot mybatis-plus shiro 

1.pom.xml 貼出部分

<!--             mybits-plus            -starter-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatisplus-spring-boot-starter</artifactId>
            <version>1.0.5</version>
        </dependency>
        <!-- MP 核心庫 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>2.1.8</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- 模板引擎 代碼生成 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity</artifactId>
            <version>1.7</version>
        </dependency>
        <!--              mybits-plus                -end-->
        <!--shiro 登錄認證-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>5.1.35</scope>
        </dependency>
        <!--druid 數據庫連接池監控-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.0</version>
        </dependency>
        <!--jasypt 數據庫加解密-->
        <dependency>
            <groupId>com.github.ulisesbocchio</groupId>
            <artifactId>jasypt-spring-boot-starter</artifactId>
            <version>1.8</version>
        </dependency>

2.先集成mybatis-plus 利用自動生成方法生成 新建5張表的POJO 和 Mapper Service文件


3.編寫ShiroConfig配置類

package com.zxt.ms.configs;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

/**
 * @ClassName ShiroConfig
 * @Description ms 夢想家
 * @Author Zhai XiaoTao https://www.cnblogs.com/zhaiyt
 * @Date 2019/1/26 17:27
 * @Version 1.0
 */
@Slf4j
@Configuration
public class ShiroConfig {

    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * shiro自帶過濾器,無需再另外設置filter
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {

        log.info("ShiroConfig.shirFilter() start ...");

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //設置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //配置過濾器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

        //沒登錄的頁面
        shiroFilterFactoryBean.setLoginUrl("/notLogin");
        // 設置無權限時跳轉的 url;
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");

        /*
         * shiro 內置枚舉
         * anon 表示可以匿名使用
         * authc 表示需要認證(登錄)才能使用,沒有參數
         */
        //靜態資源允許訪問
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/layer/**", "anon");

        //游客,開發權限
        filterChainDefinitionMap.put("/guest/**", "anon");
        //用戶,需要角色權限 “user”
        filterChainDefinitionMap.put("/user/**", "roles[user]");
        //管理員,需要角色權限 “admin”
        filterChainDefinitionMap.put("/admin/**", "roles[admin]");
        //開放登陸接口
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginUser", "anon");
        //其余接口一律攔截
        //主要這行代碼必須放在所有權限設置的最后,不然會導致所有 url 都被攔截
        filterChainDefinitionMap.put("/**", "authc");

        //過濾器注入工廠類
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        log.info("ShiroConfig.shirFilter() end ...");
        return shiroFilterFactoryBean;
    }

    /**
     * @return org.apache.shiro.mgt.SecurityManager
     * @Description <安全管理器Bean>
     * @Author Zhaiyt
     * @Date 14:35 2019/1/28
     * @Param
     **/
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //注入realm
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    /**
     * @return com.zxt.ms.configs.ShiroRealm
     * @Description <域 的概念 Shiro 從從Realm獲取安全數據(如用戶、角色、權限),就是說SecurityManager要驗證用戶身份,
     * 那么它需要從Realm獲取相應的用戶進行比較以確定用戶身份是否合法也需要從Realm得到用戶相應的角色/權限進行驗證用戶是否能進行操作;
     * 可以把Realm看成DataSource , 即安全數據源>
     * @Author Zhaiyt
     * @Date 14:38 2019/1/28
     * @Param
     **/
    @Bean
    public ShiroRealm myShiroRealm() {
        ShiroRealm myShiroRealm = new ShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

    /**
     * @return org.springframework.web.servlet.handler.SimpleMappingExceptionResolver
     * @Description <異常處理>
     * @Author Zhaiyt
     * @Date 15:20 2019/1/28
     * @Param
     **/
    @Bean(name = "simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError");//數據庫異常處理
        mappings.setProperty("UnauthorizedException", "403");
        exceptionResolver.setExceptionMappings(mappings);  // None by default
        exceptionResolver.setDefaultErrorView("error");    // No default
        exceptionResolver.setExceptionAttribute("ex");     // Default is "exception"
        return exceptionResolver;
    }
    
    /**
     * 因為我們的密碼是加過密的,所以,如果要Shiro驗證用戶身份的話,需要告訴它我們用的是md5加密的,並且是加密了兩次。
     * @Description <加密>
     * @Author Zhaiyt
     * @Date 16:18 2019/1/29
     * @Param 
     * @return org.apache.shiro.authc.credential.HashedCredentialsMatcher
     **/
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5(""));
        return hashedCredentialsMatcher;
    }

    /**
     * @Description <因為只有開啟了AOP才執行doGetAuthorizationInfo(),也就權限攔截>
     * @Author Zhaiyt
     * @Date 16:18 2019/1/29
     * @Param 
     * @return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
     **/
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

shiro配置類編寫主要需要注意以下幾點:
  1.Shiro 過濾器
  2.SecurityManager 安全管理器
  3.加密方式
  4.域需要注入到SecurityManager中

4.自定義ShiroRealm編寫:

package com.zxt.ms.configs;

import com.zxt.ms.entity.UserInfo;
import com.zxt.ms.service.IUserInfoService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.Set;

/**
 * @ClassName ShiroRealm
 * @Description ms 夢想家
 * @Author Zhai XiaoTao https://www.cnblogs.com/zhaiyt
 * @Date 2019/1/26 16:54
 * @Version 1.0
 */
@Slf4j
@Component
public class ShiroRealm extends AuthorizingRealm {


    @Autowired
    private IUserInfoService userInfoServiceImpl;

    /**
     * @Description <權限驗證>
     * @Author Zhaiyt
     * @Date 14:57 2019/1/28
     * @Param
     * @return org.apache.shiro.authz.AuthorizationInfo
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        //因為非正常退出,即沒有顯式調用 SecurityUtils.getSubject().logout()
        if (!SecurityUtils.getSubject().isAuthenticated()) {
            log.info("非正常退出,清除緩存");
            doClearCache(principalCollection);
            SecurityUtils.getSubject().logout();
            return null;
        }

        UserInfo userInfo = (UserInfo)principalCollection.getPrimaryPrincipal();
        String username = userInfo.getUsername();
        //用戶存在 授權
        if(StringUtils.isNotBlank(username)){
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            Set<String> roles=userInfoServiceImpl.findRoleByUser(userInfo.getUsername());
            Set<String> permissions=userInfoServiceImpl.findPermissionByUser(userInfo.getUsername());
            authorizationInfo.setRoles(roles);
            authorizationInfo.setStringPermissions(permissions);
            return authorizationInfo;
        }
        return null;
    }

    /**
     * @Description <身份驗證>
     * @Author Zhaiyt
     * @Date 14:58 2019/1/28
     * @Param
     * @return org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("shiroRealm.doGetAuthenticationInfo() start ...");
        //獲取用戶的輸入的賬號.
        String username = (String)authenticationToken.getPrincipal();

        //實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鍾內不會重復執行該方法
        if(StringUtils.isNotBlank(username)){

            UserInfo userInfo = userInfoServiceImpl.findByUsername(username);

            if(userInfo == null){
                log.error("用戶不存在");
                throw new UnknownAccountException("用戶名或密碼錯誤!");
            }

            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    userInfo, //用戶名
                    userInfo.getPassword(), //密碼
                    ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
                    getName()  //realm name
            );
            return authenticationInfo;
        }
        throw new UnknownAccountException("用戶名或密碼錯誤!");
    }
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    /**
     * 清除權限緩存
     * 使用方法:在需要清除用戶權限的地方注入 ShiroRealm,
     * 然后調用其clearCache方法。
     */
    public void clearCache() {
        PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
        super.clearCache(principals);
    }

    public static void main(String[] args) {
        String hashAlgorithmName = "MD5";
        String credentials = "123456";
        int hashIterations = 2;
        ByteSource credentialsSalt = ByteSource.Util.bytes("zhaizhai");
        Object obj = new SimpleHash(hashAlgorithmName, credentials, credentialsSalt, hashIterations);
        System.out.println(obj);
    }
}

ShiroRealm 需要集成 AuthorizingRealm 這個里面要實現兩個方法,一個認證 doGetAuthenticationInfo,一個授權 doGetAuthorizationInfo

5.測試controller編寫

package com.zxt.ms.controller;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName TestController
 * @Description ms 夢想家
 * @Author Zhai XiaoTao https://www.cnblogs.com/zhaiyt
 * @Date 2019/1/28 16:16
 * @Version 1.0
 */
@Slf4j
@Controller
public class HomeController {

    @RequiresPermissions(value = "admin")
    @RequestMapping(value = "/index")
    public String test(){
        return "index";
    }

    @GetMapping(value = "/login")
    public String login(){
        return "login";
    }

    @RequestMapping("/loginUser")
    @ResponseBody
    public Map<String,Object> login(HttpServletRequest request, HttpServletResponse response) throws Exception{
        log.info("HomeController.login()");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String rememberMe = request.getParameter("rememberMe");
        Map<String,Object> map = new HashMap<>();
        String ret="";
        Subject currentUser = SecurityUtils.getSubject();
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken(username,
                    password);
            token.setRememberMe(rememberMe=="true");
            map.put("code","FAILD");
            try {
                currentUser.login(token);
                map.put("code","SUCCESS");
                map.put("msg","登陸成功");
            } catch (UnknownAccountException ex) {
                map.put("msg","賬號錯誤");
                log.error(MapUtils.getString(map,"msg"));
            } catch (IncorrectCredentialsException ex) {
                map.put("msg","密碼錯誤");
                log.error(MapUtils.getString(map,"msg"));
            } catch (LockedAccountException ex) {
                map.put("msg","賬號已被鎖定,請與管理員聯系");
                log.error(MapUtils.getString(map,"msg"));
            } catch (AuthenticationException ex) {
                map.put("msg","您沒有授權");
                log.error(MapUtils.getString(map,"msg"));
            }
        }
        return map;
    }
}

 

代碼基本上就上面那些,但是我深知,僅僅只有上面那些東西根本就跑不起來,我不知道我為什么要寫這樣的東西,可能某個小哥在看我寫的博客時候也會罵我吧,寫的什么鬼雞仔。。。 我是沒有辦法把所有東西都整到這來,沒有任何意義,有些坑必須自己去踩,只有踩過了,也許才會記得更清楚。

 

下面我要說坑了:

1.編寫shiroFilter的時候我沒有將靜態數據過濾,導致頁面展示格式錯亂

2.登陸使用的是ajax,而shiro當做是表單的提交,所以一開始的 login controller 寫的是有問題,導致一直報 302

3.認證無法通過,在shiro配置類中我指定了加密方式,為MD5 2次離散 ,在Realm中我指定了需要加 salt 的加密方式,因此密碼的加密方式為  MD5 + salt 我使用main 跑出來加密后的數據,添加至數據庫,可是一直無法認證成功,后發現在SecurityManager注入的ShiroRealm實體沒有set加密方法...

4.認證成功后無法回調授權方法,原因,需要權限校驗的方法上添加 @RequiresPermissions(value = "admin") OK

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM