1. 程式人生 > >SpringBoot整合shiro(一)基礎配置

SpringBoot整合shiro(一)基礎配置

公司專案採用的spring-boot框架。在做使用者許可權功能的時候準備採用shiro許可權框架。前面也考慮過spring家族的spring security安全框架。但是經過網上查詢對比最終選擇了shiro。因為shiro含有基本的安全控制功能,並且配置更為簡單,使用也更加簡潔。
首先引入shiro依賴jar包

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId
>
<version>1.4.0</version> </dependency> <!--shiro快取外掛--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version
>
</dependency>

本次shiro外掛快取功能實現採用的是ehcahe。前面也嘗試過使用Redis。但是在配置資料來源那塊報錯,無法解決資料來源的問題。所以直接改用了ehcahe。後面如果解決了資料來源問題。再發整合Redis的教程。
首先我在系統建立使用者許可權關係
這裡我們需要三張表:
SysUser: 用來儲存使用者的密碼,使用者名稱等等資訊。
SysRole: 角色表,存放所有的角色資訊
SysAuth:許可權表,定義了一些操作訪問許可權資訊。
還有兩張關聯表(這裡我們用JPA自動生成。):
SysUserRole: SysUser和SysRole的關聯表。
SysRoleAuth:SysRole和SysAuth的關聯表。
這裡貼三張表的欄位設計

public class SysUser {
    private Integer userId; 
    private String userAccount;//使用者賬號
    private String userPassword;//使用者密碼
}
public class SysRole {
    private Integer sysRoleId;
    private Byte sysRoleAva; //角色是否生效
    private String sysRoleDes;//角色描述
    private String sysRoleName;//角色名稱
}
public class SysAuth {
    private Integer sysAuthId;
    private String sysAuthCode; //許可權編號
    private String sysAuthName; //許可權名稱
    private String sysAuthUrl; //許可權請求的url 例如: user/login
    private String sysAuthPermission; //許可權的的名稱例如 user:login
    private Byte sysAuthAva; //許可權是否有效
    private Byte sysAuthType; //許可權型別。選單還是按鈕
    private String sysAuthDes; //許可權描述
}

1.配置Realms
Realm是一個Dao,通過它來驗證使用者身份和許可權。這裡Shiro不做許可權的管理工作,需要我們自己管理使用者許可權,只需要從我們的資料來源中把使用者和使用者的角色許可權資訊取出來交給Shiro即可。Shiro就會自動的進行許可權判斷。在專案包下建一個ShiroRealm類,繼承AuthorizingRealm抽象類。

import com.lingjiugis.ocr.domain.SysAuth;
import com.lingjiugis.ocr.domain.SysRole;
import com.lingjiugis.ocr.domain.SysUser;
import com.lingjiugis.ocr.service.SysAuthService;
import com.lingjiugis.ocr.service.SysRoleService;
import com.lingjiugis.ocr.service.UserService;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import java.util.List;
public class ShiroRealm extends AuthorizingRealm {
    private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
    //這裡嘗試過使用@Autowired 但是發現會報錯。這個是spring的註解。如果有知道原因的可以留言。謝謝
    @Resource
    private UserService userService;
    @Resource
    private SysRoleService sysRoleService;
    @Resource
    private SysAuthService authService;
    /**
     * 配置許可權 注入許可權
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
        System.out.println("--------許可權配置-------");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUser user = (SysUser) principals.getPrimaryPrincipal();
        try {
            //注入角色(查詢所有的角色注入控制器)
            List<SysRole> list = sysRoleService.selectRoleByUser(user.getUserId());
            for (SysRole role: list){
                authorizationInfo.addRole(role.getSysRoleName());
            }
            //注入角色所有許可權(查詢使用者所有的許可權注入控制器)
            List<SysAuth> sysAuths = authService.queryByUserId(user.getUserId());
            for(SysAuth sysAuth:sysAuths){
                authorizationInfo.addStringPermission(sysAuth.getSysAuthPermission());
            }
        }catch (Exception e){
            e.printStackTrace();
            logger.error(ExceptionUtils.getFullStackTrace(e));
        }
        return authorizationInfo;
    }

    /**
     * 使用者驗證
     * @param token 賬戶資料
     * @return
     * @throws AuthenticationException 根據賬戶資料查詢賬戶。根據賬戶狀態丟擲對應的異常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //獲取使用者的輸入的賬號
        String username = (String) token.getPrincipal();
        //這裡需注意。看別人的教程有人是這樣寫的String password = (String) token.getCredentials();
        //專案執行的時候報錯,發現密碼不正確。後來進原始碼檢視發現將密碼注入後。Shiro會進行轉義將字串轉換成字元陣列。
        //原始碼:this(username, password != null ? password.toCharArray() : null, false, null);
        //不曉得是否是因為版本的原因,建議使用的時候下載原始碼進行檢視
        String password = new String((char[]) token.getCredentials());
        //通過username從資料庫中查詢 User物件,如果找到,沒找到.
        //實際專案中,這裡可以根據實際情況做快取,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
        SysUser user = userService.selectByAccount(username);
        if(null == user){
            throw new UnknownAccountException();
        }else {
            if(password.equals(user.getUserPassword())){
                if(0 == user.getUserState()){
                    throw new LockedAccountException();
                }else if (2 == user.getUserState()){
                    throw new DisabledAccountException();
                }else{
                    SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo(user,user.getUserPassword().toCharArray(),getName());
                    return authorizationInfo;
                }
            } else {
                throw new IncorrectCredentialsException();
            }
        }
    }
}

2.接下來配置Shiro的關鍵部分

這裡要配置的是ShiroConfig類,Apache Shiro 核心通過 Filter 來實現,就好像SpringMvc 通過DispachServlet 來主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過URL規則來進行過濾和許可權校驗,所以我們需要定義一系列關於URL的規則和訪問許可權。

import com.lingjiugis.ocr.config.GlobalExceptionResolver;
import com.lingjiugis.ocr.filter.ShiroSessionManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("--------------------shiro filter-------------------");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        //注意過濾器配置順序 不能顛倒
        //配置退出 過濾器,其中的具體的退出程式碼Shiro已經替我們實現了,登出後跳轉配置的loginUrl
        // 配置不會被攔截的連結 順序判斷
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        //攔截其他所以介面
        filterChainDefinitionMap.put("/**", "authc");
        //配置shiro預設登入介面地址,前後端分離中登入介面跳轉應由前端路由控制,後臺僅返回json資料
        shiroFilterFactoryBean.setLoginUrl("/user/unlogin");
        // 登入成功後要跳轉的連結 自行處理。不用shiro進行跳轉
        // shiroFilterFactoryBean.setSuccessUrl("user/index");
        //未授權介面;
         shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * shiro 使用者資料注入
     * @return
     */
    @Bean
    public ShiroRealm shiroRealm(){
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }

    /**
     * 配置管理層。即安全控制層
     * @return
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        return  securityManager;
    }
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    /**
     * 開啟shiro aop註解支援 使用代理方式所以需要開啟程式碼支援
     *  一定要寫入上面advisorAutoProxyCreator()自動代理。不然AOP註解不會生效
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

3.修改我們的Controller中的登入請求

// 這裡如果不寫method引數的話,預設支援所有請求,如果想縮小請求範圍,還是要新增method來支援get, post等等某個請求。
    @RequestMapping("/login")
    public String login(HttpServletRequest request, Map<String, Object> map) throws Exception {
        BaseResponse<String> baseResponse = new BaseResponse<>();
        Subject subject = SecurityUtils.getSubject();
        //資料庫的密碼我進行了Md5加密。如果沒有進行加密的無需這個
        user.setUserPassword(MD5Util.getPwd(user.getUserPassword()));
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUserAccount(),user.getUserPassword());
        try {
            subject.login(token);
            //System.out.println(getSession().getId());
            baseResponse.success(getSession().getId());
        } catch (UnknownAccountException e){
            baseResponse.setMsg("使用者名稱不存在");
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            baseResponse.setMsg("密碼錯誤");
        } catch (LockedAccountException e){
            baseResponse.setCode(CodeField.ACCOUNT_NOT_ACTIVAT);
            baseResponse.setMsg(CodeField.ACCOUNT_NOT_ACTIVAT_MSG);
        }catch (DisabledAccountException e){
            baseResponse.setCode(CodeField.ACCOUNT_BAN);
            baseResponse.setMsg(CodeField.ACCOUNT_BAN_MSG);
        } catch (Exception e){
            e.printStackTrace();
            logger.error(ExceptionUtils.getFullStackTrace(e));
        }
        return baseResponse;
    }

這裡@RequestMapping之所以沒加method是因為如果使用者沒登入,Shiro會呼叫get方法請求/login,而後面我們在login頁面會用post請求傳送form表單,所以這裡就沒設定method(預設支援所有請求)。
配置完成了就可以執行起來了。