SpringBoot2 整合 Shiro 基於URL的權限校驗

概念模型

用戶與角色之間是一對一關聯,角色與資源之間是多對多關聯(關聯關係用中間表來維護)

Resource裏面存儲着系統的url路徑

模型字段都很簡單,SQL及Model類就不再貼出來了.


1.引入依賴

<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
</dependency>

添加這一個依賴就夠用了,它會依賴core和web

 


我寫的shiro類有這些

CheckLoginFilter  登錄認證過濾器

CheckPermissionsFilter 權限校驗過濾器

ShiroConfig 配置類(核心)

ShiroLifecycleBeanPostProcessorConfig shiro生命週期管理

ShiroPermissionResolver 生成Permission實例(關鍵)

ShiroWildcardPermission Permission的子孫類,判斷權限通過與否(關鍵)

UserRealm 提供身份認證和授權(核心)

要使用自定義的鑑權方式,需要寫  PermissionResolver  和  WildcardPermission 的子類

注:所有的Service類.就不貼出來了,功能很簡單

1.首先看一下UserRealm

doGetAuthorizationInfo 和 doGetAuthenticationInfo方法必須要求實現

這裏爲了url權限校驗的需要,覆蓋了isPermitted方法(這不是必須的)

package com.web.shiro;

import com.web.constant.Constant;
import com.web.constant.ErrorCode;
import com.web.dto.AuthorizationRoleInfoDTO;
import com.web.entity.Role;
import com.web.entity.User;
import com.web.exception.PolarisException;
import com.web.service.RoleService;
import com.web.service.UserService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;

/**
 * Realm 充當了 Shiro 與應用安全數據間的“橋樑”或者“連接器”
 *
 * @author 
 * @date 2019/07/09 10:43
 */
public class UserRealm extends AuthorizingRealm {
    @Autowired
    @Lazy
    private UserService userService;
    @Autowired
    @Lazy
    private RoleService roleService;
    @Autowired
    @Lazy
    private MemorySessionDAO sessionDAO;

    /**
     * 授權
     *
     * @param principal principal
     * @return 用戶權限信息集合
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        User user = (User) principal.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = (SimpleAuthorizationInfo) SecurityUtils.getSubject()
                .getSession().getAttribute(Constant.AUTHORIZATION_INFO);
        if (info != null) {
            return info;
        }

        info = new SimpleAuthorizationInfo();
        AuthorizationRoleInfoDTO roleInfo = roleService.getAuthorizationRoleInfoDTOByUsername(user.getUsername());
        if (roleInfo == null) {
            return info;
        }

        info.addRole(String.valueOf(roleInfo.getRoleId()));
        // 設置權限碼
        info.setStringPermissions(new HashSet<>(roleInfo.getUrls()));
        SecurityUtils.getSubject().getSession().setAttribute(Constant.AUTHORIZATION_INFO, info);
        return info;
    }

    /**
     * 身份認證
     *
     * @param token token
     * @return 用戶角色信息集合
     * @throws AuthenticationException 認證異常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 當事人,類型取決於調用subject.login之前存儲的數據類型
        String username = (String) token.getPrincipal();
        User user = userService.getByUsername(username);
        if (null == user) {
            throw new PolarisException(ErrorCode.USERNAME_OR_PASSWORD_ERROR);
        }

        if (User.STATUS_DISABLE == user.getStatus()) {
            throw new PolarisException(ErrorCode.USER_IS_DISABLED);
        }

        if (User.STATUS_DELETED == user.getStatus()) {
            throw new PolarisException(ErrorCode.USERNAME_OR_PASSWORD_ERROR);
        }

        String password = user.getPassword();
        user.setPassword(null);
        user.setRole(roleService.getById(user.getRoleId()));
        // 第一個參數隨便放,可以是user,在系統中任意位置可以獲取改對象;
        // 第二個參數必須是密碼
        // 第三個參數當前Realm的名稱,因爲可能存在多個Realm
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        stopPreviousSession(user.getId());
        return info;
    }

    /**
     * 超級管理員自動獲取所有權限
     */
    @Override
    public boolean isPermitted(PrincipalCollection principals, String permission) {
        User user = ((User) principals.getPrimaryPrincipal());
        if (Role.ADMIN_FLAG_SUPER_ADMIN == user.getRole().getAdminFlag()) {
            return true;
        }

        return isPermitted(principals, getPermissionResolver().resolvePermission(permission));
    }

    @Override
    public boolean isPermitted(PrincipalCollection principals, Permission permission) {
        AuthorizationInfo info = getAuthorizationInfo(principals);
        Collection<Permission> perms = getPermissions(info);
        if (CollectionUtils.isEmpty(perms)) {
            return false;
        }

        for (Permission perm : perms) {
            if (perm.implies(permission)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 踢掉上一個登錄的同名用戶
     *
     * @param id 主鍵
     */

    private void stopPreviousSession(Integer id) {
        Collection<Session> sessions = sessionDAO.getActiveSessions();
        Session currSession = SecurityUtils.getSubject().getSession();
        Serializable sId = currSession.getId();
        for (Session session : sessions) {
            SimplePrincipalCollection collection = (SimplePrincipalCollection) session
                    .getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (collection == null) {
                continue;
            }

            User u = (User) collection.getPrimaryPrincipal();
            if (id.equals(u.getId())) {
                if (sId.equals(session.getId())) {
                    continue;
                }

                session.stop();
                break;
            }
        }
    }
}

 

2. ShiroWildcardPermission 類

      implies方法就是用來校驗權限通過與否的.寫的比較簡單粗淺,考慮的情況很簡單....

     假若用戶擁有 /user/page 路徑的訪問權限,那麼 /user/page/** 下的所有路徑都會判定通過,

     而 /user 路徑判定不通過,/role 判定也不通過

package com.web.shiro;

import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.permission.WildcardPermission;

import java.util.List;
import java.util.Set;

/**
 * @author 
 * @date 2019/07/25 15:10
 */
public class ShiroWildcardPermission extends WildcardPermission {
    ShiroWildcardPermission(String wildcardString) {
        super(wildcardString, DEFAULT_CASE_SENSITIVE);
    }

    @Override
    public boolean implies(Permission p) {
        if (!(p instanceof ShiroWildcardPermission)) {
           return false;
        }

        List<Set<String>> targetParts = ((ShiroWildcardPermission) p).getParts();
        String targetUrl = targetParts.get(0).iterator().next();
        String url = getParts().get(0).iterator().next();
        if (targetUrl.startsWith(url)) {
            if (targetUrl.equals(url)) {
                return true;
            }

            return targetUrl.startsWith(url.concat("/"));
        }

        return false;
    }
}

 

3. ShiroPermissionResolver 類

    很簡單,就是生成 ShiroWildcardPermission 類對象

package com.web.shiro;

import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.permission.PermissionResolver;

/**
 * @author 
 * @date 2019/07/25 15:05
 */
public class ShiroPermissionResolver implements PermissionResolver {
    @Override
    public Permission resolvePermission(String permissionString) {
        return new ShiroWildcardPermission(permissionString);
    }
}

 

4.CheckLoginFilter 

onAccessDenied 方法   當用戶沒有登錄的處理策略,這裏是直接返回json

ShiroUtil類放到最後面貼出

package com.web.shiro;

import com.web.base.RestResponse;
import com.web.constant.ErrorCode;
import com.web.utils.ShiroUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * 登錄認證過濾器
 *
 * @author 
 */
public class CheckLoginFilter extends FormAuthenticationFilter {

    /**
     * 表示訪問拒絕時是否自己處理,如果返回true表示自己不處理且繼續攔截器鏈執行,返回false表示自己已經處理了(比如重定向到另一個頁面)
     *
     * @param request         對象
     * @param servletResponse 對象
     * @return true-繼續往下執行,false-該filter過濾器已經處理,不繼續執行其他過濾器
     * @throws Exception 異常
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse servletResponse) throws Exception {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        Object user = SecurityUtils.getSubject().getPrincipal();
        if (null != user) {
            return true;
        }

        Map<String, String> data = new HashMap<>(1);
        data.put("toLoginUrl", "/login");
        RestResponse<Map> rsp = new RestResponse<>(ErrorCode.SESSION_TIMEOUT_ERROR);
        rsp.setMsg("你還沒有登錄或者會話過期!!!!!!!!!");
        rsp.setData(data);
        ShiroUtil.errorResponse(response, rsp);
        return false;
    }
}

5.CheckPermissionsFilter

第一個方法判定權限的是否通過,getPathWithinApplication(request) 取得當前請求的url路徑

方法會進入到UserRealm的 doGetAuthorizationInfo 方法進行授權,之後進入isPermitted方法進行初步處理,最終會進入ShiroWildcardPermission的 implies 進行鑑權.

第二個方法表示判定失敗時的處理措施

package com.web.shiro;

import com.web.base.RestResponse;
import com.web.constant.ErrorCode;
import com.web.utils.ShiroUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

/**
 * 檢驗權限過濾器
 *
 * @author 
 * @date 2019/07/24 14:34
 */
public class CheckPermissionsFilter extends PermissionsAuthorizationFilter {

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return SecurityUtils.getSubject().isPermitted(getPathWithinApplication(request));
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse r) {
        if (null == SecurityUtils.getSubject().getPrincipals()) {
            RestResponse rest = new RestResponse(ErrorCode.SESSION_TIMEOUT_ERROR);
            ShiroUtil.errorResponse((HttpServletResponse) r, rest);
            return false;
        }

        RestResponse rest = new RestResponse(ErrorCode.PERMIT_INSUFFICIENT);
        rest.setMsg("你沒有訪問權限,請聯繫管理員");
        ShiroUtil.errorResponse((HttpServletResponse) r, rest);
        return false;
    }
}

 

6.ShiroLifecycleBeanPostProcessorConfig 類

  裏面只有一個bean

import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 
 * @date 2019/07/12 15:33
 */
@Configuration
public class ShiroLifecycleBeanPostProcessorConfig {
    /**
     * Shiro生命週期處理器
     *
     * @return LifecycleBeanPostProcessor
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

}

 

7.ShiroConfig 類

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro 配置類
 *
 * @author 
 * @date 2019/07/09 10:13
 */
@Configuration
@AutoConfigureAfter(ShiroLifecycleBeanPostProcessorConfig.class)
public class ShiroConfig {
    @Value("${shiro.session.global-session-timeout}")
    String sessionTimeOut;

    @Bean("userRealm")
    public UserRealm userRealm() {
        UserRealm realm = new UserRealm();
        realm.setPermissionResolver(new ShiroPermissionResolver());
        return realm;
    }

    /**
     * 配置保存sessionId的cookie
     * 注意:這裏的cookie 不是上面的記住我 cookie 記住我需要一個cookie session管理 也需要自己的cookie
     * 默認爲: JSESSIONID 問題: 與SERVLET容器名衝突,重新定義爲sid
     */
    @Bean("sessionIdCookie")
    public SimpleCookie sessionIdCookie() {
        //這個參數是cookie的名稱
        SimpleCookie simpleCookie = new SimpleCookie("sid");
        //setcookie的httponly屬性如果設爲true的話,會增加對xss防護的安全係數。它有以下特點:

        //setcookie()的第七個參數
        //設爲true後,只能通過http訪問,javascript無法訪問
        //防止xss讀取cookie
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/");
        //maxAge=-1表示瀏覽器關閉時失效此Cookie
        simpleCookie.setMaxAge(1800);
        return simpleCookie;
    }

    @Bean(name = "sessionDAO")
    public MemorySessionDAO getMemorySessionDAO() {
        return new MemorySessionDAO();
    }

    @Bean("sessionManager")
    public DefaultWebSessionManager defaultWebSessionManager(
                                                        @Qualifier("sessionIdCookie") SimpleCookie simpleCookie,
                                                        @Qualifier("sessionDAO") MemorySessionDAO sessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 全局會話超時時間,毫秒,目前是二十分鐘
        sessionManager.setGlobalSessionTimeout(Integer.parseInt(sessionTimeOut));
        sessionManager.setSessionIdCookie(simpleCookie);
        sessionManager.setSessionDAO(sessionDAO);
        // 刪除無效的session
        sessionManager.setDeleteInvalidSessions(true);
        // 是否開啓定時調度器進行檢測過期session 默認爲true
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //設置session失效的掃描時間, 清理用戶直接關閉瀏覽器造成的孤立會話 默認爲 1個小時
        //設置該屬性 就不需要設置 ExecutorServiceSessionValidationScheduler 底層也是默認自動調用ExecutorServiceSessionValidationScheduler
        // 毫秒 十分鐘
        sessionManager.setSessionValidationInterval(600000);
        //去掉URL中的JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(UserRealm userRealm, DefaultWebSessionManager sessionManager) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setSessionManager(sessionManager);
        manager.setRealm(userRealm);
        return manager;
    }


    /**
     * 開啓shiro aop註解支持.
     * 使用代理方式;所以需要開啓代碼支持;
     * 可以在controller中的方法前加上註解
     * 如 @RequiresPermissions("userInfo:add")
     *
     * @param securityManager 管理器
     * @return AuthorizationAttributeSourceAdvisor
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        bean.setLoginUrl("/toLogin");
        bean.setUnauthorizedUrl("/403");
        // 定義過濾器
        Map<String, Filter> filters = bean.getFilters();
        filters.put("authc", new CheckLoginFilter());
        filters.put("perms", new CheckPermissionsFilter());
        bean.setFilters(filters);
        // 配置訪問權限
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/*.ico", "anon");
        filterChainDefinitionMap.put("/**/*.js", "anon");
        filterChainDefinitionMap.put("/**/*.css", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/user/login", "anon");
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/interface/**", "anon");
        filterChainDefinitionMap.put("/**", "authc,perms");
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return bean;
    }
}

 

其中 shiroFilter 就配置了訪問策略

可以插入如下代碼,

(因爲可以我只是根據請求url判定,就沒有寫,當然即使加上了,在我這個代碼裏面也沒有效果,

這和CheckPermissionsFilter 類的 isAccessAllowed,UserRealm類的 doGetAuthorizationInfo 授權方法,以及

ShiroWildcardPermission類的鑑權方法implies有關)
filterChainDefinitionMap.put("/user/**","perms[123]"); // /user 下的所有路徑必須具備123權限纔可以訪問
filterChainDefinitionMap.put("/role/**","role[admin]"); // /role下的路徑必須具備admin角色纔可以訪問

 

8.ShiroUtil 類

  就是輸出json提示信息

import com.fasterxml.jackson.databind.ObjectMapper;
import com.polaris.web.base.RestResponse;
import com.polaris.web.constant.ErrorCode;
import com.polaris.web.exception.PolarisException;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author 
 * @date 2019/07/24 15:15
 */
public class ShiroUtil {
    public static void errorResponse(HttpServletResponse response, RestResponse rest) {
        response.setStatus(200);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.write(new ObjectMapper().writeValueAsString(rest));
            writer.flush();
        } catch (IOException e) {
            throw new PolarisException(ErrorCode.ServerError);
        }
    }
}

 

好,至此結束!

可以通過斷點了解執行流程,這樣可以方便理解

水平有限,錯誤之處敬請指出...謝謝!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章