學習Spring Boot:(十三)配置 Shiro 權限認證


經過前面學習 Apache Shiro ,現在結合 Spring Boot 使用在項目里,進行相關配置。

正文

添加依賴

pom.xml 文件中添加 shiro-spring 的依賴:

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>

RBAC

RBAC 1 是基於角色的訪問控制,權限與角色關聯,給用戶配置相關角色,來獲取權限信息。

Shiro 配置

新建一個新的 Shiro 配置類 ShiroConfig:

package com.wuwii.common.config;

import com.wuwii.module.sys.autho2.OAuth2Realm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
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.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/** * Apache Shiro 核心通過 Filter 來實現,就好像SpringMvc 通過DispachServlet 來主控制一樣。 * 既然是使用 Filter 一般也就能猜到,是通過URL規則來進行過濾和權限校驗, * 所以我們需要定義一系列關於URL的規則和訪問權限。 * * @author KronChan * @version 1.0 * @since <pre>2018/2/9 10:35</pre> */
@Configuration
public class ShiroConfig {

    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdCookieEnabled(true);
        return sessionManager;
    }

    /** * 注冊安全管理,必須設置 SecurityManager * * @param oAuth2Realm 認證 * @param sessionManager 緩存 * @return */
    @Bean
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 可以添加多個認證,執行順序是有影響的
        securityManager.setRealm(oAuth2Realm);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //自定義一個oauth2攔截器,不設置就是使用默認的攔截器
        /*Map<String, Filter> filters = new HashMap<>(); filters.put("oauth2", new OAuth2Filter()); shiroFilter.setFilters(filters);*/
        //攔截器
        //<!-- 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊 -->
        //<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->
        Map<String, String> filterMap = new LinkedHashMap<>();
        //配置退出過濾器,其中的具體的退出代碼Shiro已經替我們實現了
        filterMap.put("/sys/logout", "logout");
        // 驗證碼
        filterMap.put("/sys/captcha.jpg", "anon");
        // 設置系統模塊下訪問需要權限
        filterMap.put("/sys/login", "anon");
        // 自定義的攔截
        //filterMap.put("/sys/**", "oauth2");
        filterMap.put("/sys/**", "authc");
        // 登陸的 url
        shiroFilter.setLoginUrl("/sys/login");
        // 登陸成功跳轉的 url
        shiroFilter.setSuccessUrl("/");
        // 未授權的 url
        // shiroFilter.setUnauthorizedUrl("/login.html");
        //未授權界面;
        shiroFilter.setUnauthorizedUrl("/403");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

    /** * Shiro生命周期處理器 * @return */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /** * 開啟Shiro的注解, * (如@RequiresRoles,@RequiresPermissions),需借助SpringAOP掃描使用Shiro注解的類, * 並在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能 * * @return */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

    /** * 開啟 shiro aop注解支持. * * @param securityManager * @return */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /** * 憑證匹配器 * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 * 所以我們需要修改下doGetAuthenticationInfo中的代碼; * ) * <b>需要在身份認證中添加 realm.setCredentialsMatcher(hashedCredentialsMatcher())</b> * @return */
    /*@Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5("")); return hashedCredentialsMatcher; }*/
}

Filter Chain定義說明:
1. 一個URL可以配置多個Filter,使用逗號分隔
2. 當設置多個過濾器時,全部驗證通過,才視為通過
3. 部分過濾器可指定參數,如perms,roles

Shiro內置的FilterChain:

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

* anon:所有url都都可以匿名訪問
* authc: 需要認證才能進行訪問
* user:配置記住我或認證通過可以訪問

自定義的攔截器(可選)

如果需要按照自己的需要定義一個 oauth2 的攔截器,則需要 繼承 AuthenticatingFilter 實現幾個方法即可。

/** * oauth2過濾器 */
public class OAuth2Filter extends AuthenticatingFilter {

    /** * logger */
    private static final Logger LOGGER = LoggerFactory.getLogger(OAuth2Filter.class);

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //獲取請求token
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return new OAuth2Token(token);
    }

    /** * shiro權限攔截核心方法 返回true允許訪問resource, * @param request * @param response * @param mappedValue * @return */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return false;
    }

    /** * 當訪問拒絕時是否已經處理了; * 如果返回true表示需要繼續處理; * 如果返回false表示該攔截器實例已經處理完成了,將直接返回即可。 * @param request * @param response * @return * @throws Exception */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //獲取請求token,如果token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            ((HttpServletResponse) response).setStatus(401);
            response.getWriter().print("沒有權限,請聯系管理員授權");
            return false;
        }
        return executeLogin(request, response);
    }

    /** * 鑒定失敗,返回錯誤信息 * @param token * @param e * @param request * @param response * @return */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        try {
            ((HttpServletResponse) response).setStatus(401);
            response.getWriter().print("沒有權限,請聯系管理員授權");
        } catch (IOException e1) {
            LOGGER.error(e1.getMessage(), e1);
        }
        return false;
    }

    /** * 獲取請求的token */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //從header中獲取token
        String token = httpRequest.getHeader("token");
        //如果header中不存在token,則從參數中獲取token
        if (StringUtils.isBlank(token)) {
            return httpRequest.getParameter("token");
        }
        // 還可以實現從 cookie 獲取
        Cookie[] cookies = httpRequest.getCookies();
        if(null==cookies||cookies.length==0){
            return null;
        }
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("token")) {
                token = cookie.getValue();
                continue;
            }
        }
        return token;
    }
}

具體實現可以參考我的上篇文章 《Apache Shiro的攔截器和認證》

認證實現

Shiro的認證過程最終會交由Realm執行,這時會調用Realm的 getAuthenticationInfo(token) 方法。
該方法主要執行以下操作:
1. 檢查提交的進行認證的令牌信息
2. 根據令牌信息從數據源(通常為數據庫)中獲取用戶信息
3. 對用戶信息進行匹配驗證。
4. 驗證通過將返回一個封裝了用戶信息的AuthenticationInfo實例。
5. 驗證失敗則拋出AuthenticationException異常信息。

而在我們的應用程序中要做的就是自定義一個Realm類,繼承 AuthorizingRealm 抽象類,重載 doGetAuthenticationInfo (),重寫獲取用戶信息的方法。

@Component
public class OAuth2Realm extends AuthorizingRealm {
    @Resource
    private ShiroService shiroService;
    @Resource
    private SysUserService sysUserService;


    /** * 此方法調用 hasRole,hasPermission的時候才會進行回調. * * 權限信息.(授權): * 1、如果用戶正常退出,緩存自動清空; * 2、如果用戶非正常退出,緩存自動清空; * 3、如果我們修改了用戶的權限,而用戶不退出系統,修改的權限無法立即生效。 * (需要手動編程進行實現;放在service進行調用) * 在權限修改后調用realm中的方法,realm已經由spring管理,所以從spring中獲取realm實例, * 調用clearCached方法; * :Authorization 是授權訪問控制,用於對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。 * * * 當沒有使用緩存的時候,不斷刷新頁面的話,這個代碼會不斷執行, * 當其實沒有必要每次都重新設置權限信息,所以我們需要放到緩存中進行管理; * 當放到緩存中時,這樣的話,doGetAuthorizationInfo就只會執行一次了, * 緩存過期之后會再次執行。 * * @param principals * @return */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SysUserEntity user =(SysUserEntity) (principals.getPrimaryPrincipal());;

        // 獲取該用戶權限列表
        Set<String> permsSet = shiroService.getUserPermissions(user.getId());

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permsSet);
        return info;
    }

    /** * 認證回調函數,登錄時調用 * 首先根據傳入的用戶名獲取User信息;然后如果user為空,那么拋出沒找到帳號異常UnknownAccountException; * 如果user找到但鎖定了拋出鎖定異常LockedAccountException;最后生成AuthenticationInfo信息, * 交給間接父類AuthenticatingRealm使用CredentialsMatcher進行判斷密碼是否匹配, */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        SysUserEntity user = sysUserService.queryByUsername(usernamePasswordToken.getUsername());
        //賬號不存在、密碼錯誤
        if (user == null) {
            throw new KCException("賬號或密碼不正確");
        }

        // 交給 shiro 自己去驗證,
        // 明文驗證
        return new SimpleAuthenticationInfo(
                user, // 存入憑證的信息,登陸成功后可以使用 SecurityUtils.getSubject().getPrincipal();在任何地方使用它
                user.getPassword(),
                getName());

        // 加密的方式
        // 交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,如果覺得人家的不好可以自定義實現
        /*return new SimpleAuthenticationInfo( user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), // 加鹽,可以注冊憑證匹配器 HashedCredentialsMatcher 告訴它怎么加密的 getName());*/

    }
}

實現上面兩個方法即可完成身份驗證和權限驗證。

登陸實現

    @PostMapping("/login")
    @ApiOperation("系統登陸")
    public ResponseEntity<String> login(@RequestBody SysUserLoginForm userForm) {
        String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);
        if (!userForm.getCaptcha().equalsIgnoreCase(kaptcha)) {
            throw new KCException("驗證碼不正確!");
        }
        UsernamePasswordToken token = new UsernamePasswordToken(userForm.getUsername(), userForm.getPassword());
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.login(token);

         //賬號鎖定
        if (getUser().getStatus() == SysConstant.SysUserStatus.LOCK) {
            throw new KCException("賬號已被鎖定,請聯系管理員");
        }
        return ResponseEntity.status(HttpStatus.OK).body("登陸成功!");
    }

權限驗證

    @ApiOperation("用於測試,查詢")
    @ApiImplicitParam(name = "string", value = "id", dataType = "string")
    @RequiresPermissions("sys:user:list1")
    @GetMapping()
    public ResponseEntity<List<SysUserEntity>> query(@CustomValid String string) {
        return new ResponseEntity<>(sysUserService.query(new SysUserEntity()), OK);
    }

簡單測試一個例子,sys:user:list1 多加一個 1 肯定會驗證失敗,查看程序會做什么,它會去我們定義的 Realm 中的 doGetAuthorizationInfo(PrincipalCollection principals) 方法中,執行查詢該用戶的所有權限。
驗證失敗后最后程序結果如下:

Caused by: org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public org.springframework.http.ResponseEntity com.wuwii.module.sys.controller.SysUserController.query(java.lang.String)
    at org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor.assertAuthorized(AuthorizingAnnotationMethodInterceptor.java:90)
    ... 77 common frames omitted
權限注解
@RequiresAuthentication  
表示當前Subject已經通過login進行了身份驗證;即Subject. isAuthenticated()返回true@RequiresUser  
表示當前Subject已經身份驗證或者通過記住我登錄的。

@RequiresGuest  
表示當前Subject沒有身份驗證或通過記住我登錄過,即是游客身份。

@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)  
表示當前Subject需要角色admin和user。

@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)  
表示當前Subject需要權限user:a或user:b。  

參考資料


  1. Role-Based Access Control


免責聲明!

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



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