補習系列(6)- springboot 整合 shiro 一指禪


目標

  1. 了解ApacheShiro是什么,能做什么;
  2. 通過QuickStart 代碼領會 Shiro的關鍵概念;
  3. 能基於SpringBoot 整合Shiro 實現URL安全訪問;
  4. 掌握基於注解的方法,以實現靈活定制。

一、Apache Shiro是什么

Apache Shiro 是一個強大且易用的Java安全框架,用於實現身份認證、鑒權、會話管理及加密功能。
框架提供了非常簡單且易於上手的API,可以支持快速為web應用程序實現安全控制能力。
官網地址
github 地址

Shiro 能做什么

Apache Shiro 的設計初衷是讓安全管理變得易於上手和容易理解,它可以實現:

  • 鑒別用戶身份,是否本系統注冊的A用戶;
  • 管理用戶權限,是否有某個角色,或某些權限;
  • 即使沒有web或EJB容器,也可以使用Session API
  • 可以聚合一個或多個用戶權限數據源並且以用戶視圖的形式統一表現出來
  • 實現單點登錄功能(SSO)
  • 無需登錄便可實現記住我這一功能

有什么特性

官網-Features

主要概念 包括了
Authentication(身份鑒別)、Authorization(權限管理)、Session Management(會話管理)、Cryptography(加密)
這號稱軟件安全的四大基石.. 關於幾個概念,用下面的表格說明:

名稱 解釋
Authentication(身份鑒別) 指鑒別登錄用戶的身份
Authorization(權限認證) 決定用戶是否有權訪問某物
Session Management(會話管理) 支持獨立的會話管理
Cryptography(加密) 利用加密算法保證數據安全

其他特性非核心,但是非常有用

  • web應用支持
    如JavaEE、Spring的整合支持
  • 緩存
    用於提升安全管理的效率
  • 並發
    可支持多線程應用
  • 測試
    可以通過單元測試和集成測試驗證程序的安全性
  • Run As
    允許用戶將某一身份賦予另一用戶(在一些行政管理軟件中常用)
  • Remember Mes
    在Session(會話)期間記住用戶身份,當只有強制要求登錄是才需要用戶登錄

架構說明

看看下面的圖:

圖中涉及了若干個模塊,關於每個模塊的大致作用如下:

Subject
交互實體,對應於當前用戶。

SecurityManager
安全管理器,Shiro最核心的模塊,管理各安全模塊的工作;

Authenticator
身份鑒別組件,執行和反饋用戶的認證(登錄),
該組件從Realm中獲取用戶信息。

Authentication Strategy
如果配置了多個Realm,該怎么協調?這就用到策略

Authorizer
權限認證,顧名思義,就是用於負責用戶訪問控制的模塊。

SessionManager
會話管理器,在Web環境中Shiro一般會沿用Servlet容器的會話。
但脫離了Web環境就會使用獨立的會話管理。

SessionDAO
執行會話持久化的工具

CacheManager
一個緩存管理器,可為 Shiro 的其他組件提供緩存能力。

Cryptography
加密組件,提供了大量簡單易用的安全加密API

到這里,不需要為這么多的模塊而苦惱,在使用Shiro時,只需要牢牢記住下面的實體關系,便不會產生理解上的困難。

簡而言之
應用程序依賴於 Subject 實體來標識當前的用戶,而SecurityManager 則通過Realm接口讀取數據,進而實現 Subject 的關聯管理。

二、快速入門

為了幫助讀者更快速理解Shiro,下面上一段QuickStart的代碼

// 加載 shiro.ini並構造 SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();

// 設置當前的 SecurityManager對象
SecurityUtils.setSecurityManager(securityManager);

// 獲取當前用戶
Subject currentUser = SecurityUtils.getSubject();

// 操作會話
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
    log.info("Retrieved the correct value! [" + value + "]");
}

// 執行登錄
if (!currentUser.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
    token.setRememberMe(true);
    try {
        currentUser.login(token);
    } catch (UnknownAccountException uae) {
        log.info("There is no user with username of " + token.getPrincipal());
    } catch (IncorrectCredentialsException ice) {
        log.info("Password for account " + token.getPrincipal() + " was incorrect!");
    } catch (LockedAccountException lae) {
        log.info("The account for username " + token.getPrincipal() + " is locked. "
                + "Please contact your administrator to unlock it.");
    } catch (AuthenticationException ae) {
        // unexpected condition? error?
    }
}

// 輸出用戶信息
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

// 檢查角色
if (currentUser.hasRole("schwartz")) {
    log.info("May the Schwartz be with you!");
} else {
    log.info("Hello, mere mortal.");
}

// 檢查權限
if (currentUser.isPermitted("lightsaber:weild")) {
    log.info("You may use a lightsaber ring. Use it wisely.");
} else {
    log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

// 結束,執行注銷
currentUser.logout();

System.exit(0);

上面這段代碼來自 shiro-sample/QuickStart.java
關於代碼的解釋.. 老司機認為看下注釋是一定能懂的了。

三、SpringBoot 整合 Shiro

我們嘗試將 Shiro 整合到 SpringBoot 項目,翻了下官網並沒有太多介紹,
猜想這可能與 SpringBoot 框架還比較新有關系,Shiro是個老框架(2010年出的第一個版本)..
但最終老司機還是成功找到了 膠合組件:shiro-spring-boot-starter

接下來,為項目引入依賴:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.4.0</version>
</dependency>

接下來,我們將完成一個 URL訪問安全控制 的示例,通過這個案例
讀者可以了解到如何根據業務定制必要的功能模塊。

系統設計

圖示中,名為lilei 的用戶擁有 normal (普通用戶)的角色,而相應的具備customer.profile的讀寫權限。

以上是基於RBAC(基於角色的權限控制) 的設計,RBAC 目前的應用非常廣泛

在 web應用訪問中,某些頁面是允許任何人訪問的,某些需要登錄用戶,比如個人中心
而某些頁面需要具備一些特權,比如vip資料.. 如下圖所示:

用戶模塊

通常,在設計用戶權限時都會考慮用戶信息、角色信息以及對應的權限

用戶實體

public static class UserInfo {
    private String username;
    private String passwordHash;
    private String salt;

需要注意到 salt是用於密碼存儲的加鹽值(用於防止暴力破解)
passwordHash 是原始密碼經過加鹽哈希計算后的值(16進制形式)

角色實體

public static class RoleInfo {
    private String roleName;
    private List<String> perms;

為了簡化,我們直接將權限用字符串形式表示,一個角色RoleInfo包含了一組權限perm。

用戶管理器

在我們的樣例中,需要實現一個UserManager類,用於做用戶信息、權限信息的管理。

public class ShiroUserManager {

    // 用戶表
    private final Map<String, UserInfo> users = new HashMap<String, UserInfo>();
    // 角色權限表
    private final Map<String, List<RoleInfo>> userRoles = new HashMap<String, List<RoleInfo>>();

    private static final Logger logger = LoggerFactory.getLogger(ShiroUserManager.class);

    // 密鑰匹配類
    private ShiroHashMatcher matcher;

    public ShiroUserManager(ShiroHashMatcher matcher) {
        this.matcher = matcher;
    }

    public ShiroHashMatcher getMatcher() {
        return this.matcher;
    }

    @PostConstruct
    private void init() {

        // 預置信息
        register("lilei", "111111", "123");
        grant("normal", new RoleInfo("customer", "customer.profile.read"));
        grant("normal", new RoleInfo("customer", "customer.profile.write"));
    }

    /**
     * 獲取用戶信息
     * 
     * @param username
     * @return
     */
    public UserInfo getUser(String username) {
        if (StringUtils.isEmpty(username)) {
            return null;
        }
        return users.get(username);
    }

    /**
     * 獲取權限信息
     * 
     * @param username
     * @return
     */
    public List<RoleInfo> getRoles(String username) {
        if (StringUtils.isEmpty(username)) {
            return Collections.emptyList();
        }
        return userRoles.get(username);
    }

    /**
     * 添加用戶
     * 
     * @param username
     * @param password
     * @param salt
     * @return
     */
    public UserInfo register(String username, String password, String salt) {
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password) || StringUtils.isEmpty(salt)) {
            return null;
        }

        // 生成加鹽密碼Hash值
        String passwordHash = matcher.getCredentialHash(password, salt);

        logger.info("user {} register with passHash :{}", username, passwordHash);
        UserInfo user = new UserInfo(username, passwordHash, salt);
        users.put(username, user);

        return user;
    }

    /**
     * 授權操作
     * 
     * @param username
     * @param role
     */
    public void grant(String username, RoleInfo role) {
        if (userRoles.containsKey(username)) {

            userRoles.get(username).add(role);
        } else {
            List<RoleInfo> roleList = new ArrayList<RoleInfo>();
            roleList.add(role);
            userRoles.put(username, roleList);
        }
    }

在上面的實現中,我們僅僅將用戶、角色信息放在內存中管理,並內置了名為lilei的用戶角色。
在真實應用中,用戶權限需要通過持久層(DB)實現

密鑰算法

我們基於Shiro的基礎類HashedCredentialsMatcher進行了擴展。
選用SHA-256哈希算法,設置迭代次數為1024。

public class ShiroHashMatcher extends HashedCredentialsMatcher {

    public ShiroHashMatcher() {
        setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        setHashIterations(1024);
        setStoredCredentialsHexEncoded(true);
    }

    public String getCredentialHash(Object credentials, Object salt) {
        return new SimpleHash(this.getHashAlgorithmName(), credentials, salt, this.getHashIterations()).toHex();
    }

Realm實現

在Shiro 框架中, Realm 是用作用戶權限信息查詢的接口,我們的實現如下:

public class ShiroRealm extends AuthorizingRealm {

    private static final Logger logger = LoggerFactory.getLogger(ShiroRealm.class);

    private ShiroUserManager userManager;

    public ShiroRealm(ShiroUserManager userManager) {
        this.setCredentialsMatcher(userManager.getMatcher());
        this.userManager = userManager;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("check authorization info");

        SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();

        // 獲取當前用戶
        UserInfo userInfo = (UserInfo) principals.getPrimaryPrincipal();

        // 查詢角色信息
        List<RoleInfo> roleInfos = userManager.getRoles(userInfo.getUsername());

        if (roleInfos != null) {
            for (RoleInfo roleInfo : roleInfos) {

                authInfo.addRole(roleInfo.getRoleName());

                if (roleInfo.getPerms() != null) {
                    for (String perm : roleInfo.getPerms()) {
                        authInfo.addStringPermission(perm);
                    }
                }
            }
        }

        return authInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info("check authentication info");

        String username = (String) token.getPrincipal();

        // 獲取用戶信息
        UserInfo user = userManager.getUser(username);

        if (user == null) {
            return null;
        }

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPasswordHash(),
                ByteSource.Util.bytes(user.getSalt()), getName());
        return authenticationInfo;
    }

Bean 注冊

將實現好的 ShiroRealm 注冊為Bean,並初始化 WebSecurityManager

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        return securityManager;
    }


    @Bean
    public ShiroRealm realm() {
        ShiroRealm realm = new ShiroRealm(userManager());
        return realm;
    }

   
    @Bean
    public ShiroUserManager userManager() {
        return new ShiroUserManager(matcher());
    }


    @Bean
    public ShiroHashMatcher matcher() {
        return new ShiroHashMatcher();
    }

定義攔截鏈

攔截器鏈通過 ShiroFilterFactoryBean實現定制,實現如下:

    @Bean
    public ShiroFilterFactoryBean filter(org.apache.shiro.mgt.SecurityManager securityManager) {
        logger.info("config shiro filter");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 定義URL攔截鏈
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 允許匿名用戶訪問首頁
        filterChainDefinitionMap.put("/shiro/index", "anon");
        // 定義注銷路徑
        filterChainDefinitionMap.put("/shiro/logout", "logout");
        // 所有用戶界面都需要身份驗證,否則會跳轉到loginurl,由FormAuthenticationFilter處理
        filterChainDefinitionMap.put("/shiro/user/**", "authc");
        // 為login路徑定義攔截,由FormAuthenticationFilter處理
        filterChainDefinitionMap.put("/shiro/login", "authc");
        // 所有vip路徑要求具備vip角色權限
        filterChainDefinitionMap.put("/shiro/vip/**", "roles[vip]");
        // 指定loginurl 路徑
        shiroFilterFactoryBean.setLoginUrl("/shiro/login");
        // 登錄成功后跳轉路徑
        shiroFilterFactoryBean.setSuccessUrl("/shiro/user/");
        // for un authenticated
        shiroFilterFactoryBean.setUnauthorizedUrl("/shiro/unauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        // 自定義filters,可覆蓋默認的Filter列表,參考 DefaultFilter
        Map<String, Filter> filters = new LinkedHashMap<String, Filter>();

        // 定制logout 過濾,指定注銷后跳轉到登錄頁(默認為根路徑)
        LogoutFilter logoutFilter = new LogoutFilter();
        logoutFilter.setRedirectUrl("/shiro/login");
        filters.put("logout", logoutFilter);

        // 定制authc 過濾,指定登錄表單參數
        FormAuthenticationFilter authFilter = new FormAuthenticationFilter();
        authFilter.setUsernameParam("username");
        authFilter.setPasswordParam("password");
        filters.put("authc", authFilter);

        shiroFilterFactoryBean.setFilters(filters);
        return shiroFilterFactoryBean;
    }

跟着老司機的注釋,上面代碼應該不難理解(盡管有點冗長),filterChainDefinitionMap的定義中,
key對應於url路徑,而value則對應了過濾器的縮寫,Shiro內置的過濾器可參考DefaultFilter枚舉

配置 過濾器 功能
anon AnonymousFilter 可匿名訪問
authc FormAuthenticationFilter form表單登錄攔截
authcBasic BasicHttpAuthenticationFilter basic登錄攔截
logout LogoutFilter 注銷處理
noSessionCreation NoSessionCreationFilter 禁止創建會話
perms PermissionsAuthorizationFilter 指定權限
port PortFilter 指定端口
rest HttpMethodPermissionFilter HttpMethod轉換
roles RolesAuthorizationFilter 指定角色
ssl SslFilter 需要https
user UserFilter 已登錄或Rememberme

深入一點
FormAuthenticationFilter 實現了表單登錄的攔截邏輯:

  1. 如果當前沒有登錄,則跳轉到 loginUrl;
  2. 如果是登錄請求,則執行登錄操作,成功后跳轉到 loginSuccessUrl
  3. 如果登錄失敗,將當前的異常信息寫入請求上下文,由業務處理。

扒一扒源碼,可以看到相應的邏輯實現:

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                return executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }
                //allow them to see the login page ;)
                return true;
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +
                        "Authentication url [" + getLoginUrl() + "]");
            }
            saveRequestAndRedirectToLogin(request, response);
            return false;
        }
    }

isLoginSubmission 方法的判斷中,認為來自 loginUrl 的 POST 請求就是登錄操作。

protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
        return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
    }

在登錄失敗后,寫入上下文信息,這里使用的是異常類的名稱

    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                     ServletRequest request, ServletResponse response) {
        if (log.isDebugEnabled()) {
            log.debug( "Authentication exception", e );
        }
        setFailureAttribute(request, e);
        //login failed, let request continue back to the login page:
        return true;
    }

    protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
        String className = ae.getClass().getName();
        request.setAttribute(getFailureKeyAttribute(), className);
    }

看到這里,你應該能理解為什么在過濾鏈定義中,loginUrl 也需要被攔截了。

filterChainDefinitionMap.put("/shiro/login", "authc");

Controller 類

基於上面的分析后,我們便可以輕松的完成Controller的編寫,如下:

@Controller
@RequestMapping("/shiro")
public class ShiroController {

    /**
     * 登錄界面,展示登錄表單
     * 
     * @return
     */
    @GetMapping("/login")
    public String login() {
        return "shiro/login";
    }

    /**
     * 登錄表單處理
     * 
     * @return
     */
    @PostMapping("/login")
    public String doLogin(HttpServletRequest servletRequest, final RedirectAttributes redirectAttrs) {

        // FormAuthenticationFilter已經做了登錄校驗處理,
        // 若登錄成功會跳轉到loginSuccessUrl,這里只做異常處理
        String errorException = (String) servletRequest
                .getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);

        // 登錄失敗,errorException 非空
        if (!StringUtils.isEmpty(errorException)) {
            // 設置錯誤消息,執行跳轉
            redirectAttrs.addFlashAttribute("loginErrorMsg", "LoginFailed:" + errorException);
            return "redirect:/shiro/login";
        }
        return "OK";
    }

    /**
     * 用戶信息界面
     * 
     * @return
     */
    @GetMapping("/user")
    @ResponseBody
    public String user() {
        Subject subject = SecurityUtils.getSubject();
        UserInfo user = (UserInfo) subject.getPrincipals().getPrimaryPrincipal();
        return "Welcome back, " + user.getUsername();
    }
    
    /**
     * VIP 用戶信息界面
     * 
     * @return
     */
    @GetMapping("/vip")
    @ResponseBody
    public String userVip() {
        Subject subject = SecurityUtils.getSubject();
        UserInfo user = (UserInfo) subject.getPrincipals().getPrimaryPrincipal();
        return "Hi, " + user.getUsername() + ", This is for the vip";
    }
    
    /**
     * 匿名訪問界面
     * 
     * @return
     */
    @GetMapping("/annon/*")
    @ResponseBody
    public String annon() {
        return "this is the content anyone can access";
    }

    /**
     * 無權限界面
     * 
     * @return
     */
    @GetMapping("/unauth")
    @ResponseBody
    public String unauth() {
        return "you are no allow to access";
    }

登錄頁面

登錄頁面為一個簡單的HTML界面,包含一個POST表單,使用username/password作為請求參數。
在登錄失敗時由Controller跳轉回登錄頁,並顯示出錯信息,效果如下:

四、注解的使用

前面的例子演示了 Shiro的經典用法,然而,老司機認為注解會更好用。
Shiro 的注解是基於AOP實現的,在方法上聲明所需要的權限,相比URL攔截要更加靈活。

shiro-spring-boot-starter 為我們自動注入了AOP 代理配置,可直接使用注解。

如果使用了注解,我們可以對url 啟用匿名訪問,這樣訪問控制則通過注解和異常處理來實現。

        // 對於所有shiroan路徑一律不攔截
        filterChainDefinitionMap.put("/shiroan/**", "anon");

權限注解

    /**
     * vip 界面,需要vip角色
     * 
     * @return
     */
    @RequiresRoles("vip")
    @GetMapping("/vip")
    @ResponseBody
    public String vip() {
        return "this is the vip info";
    }

    /**
     * home 界面,需要登錄
     * 
     * @return
     */
    @RequiresAuthentication
    @GetMapping("/home")
    @ResponseBody
    public String home() {
        return "this is the home page";
    }

    /**
     * 資料界面,需要資料權限
     * 
     * @return
     */
    @RequiresPermissions("customer.profile.read")
    @GetMapping("/profile")
    @ResponseBody
    public String profile() {
        return "this is the profile info";
    }

    /**
     * 讀取相冊界面,需要詳情權限
     * 
     * @return
     */
    @RequiresPermissions("customer.album.read")
    @GetMapping("/album")
    @ResponseBody
    public String album() {
        return "this is the album info";
    }

@RequiredRoles、@RequiredPermissions、@RequiredAuthentication 定義了方法執行所需的權限。
除此之外,Shiro還內置了其他注解,如下:

名稱 功能
@RequiresRoles 指定的角色可以訪問
@RequiresPermissions 指定的權限可以訪問
@RequiresAuthentication 登錄用戶可以訪問
@RequiresGuest 僅游客可以訪問
@RequiresUser 已登錄或 "記住我"的用戶

在訪問方法未通過權限檢查時,會拋出AuthorizationException,我們需要定義一個攔截器進行處理

攔截器

    /**
     * 自定義攔截,處理鑒權異常
     * 
     * @author atp
     *
     */
    @ControllerAdvice(assignableTypes = ShiroAnnotateController.class)
    public static class AuthExceptionHandler {

        @ExceptionHandler(value = { AuthorizationException.class })
        public ResponseEntity<String> handle(AuthorizationException e, HandlerMethod m) {

            logger.info("Authorization Failed {} -- {}", e.getClass(), e.getMessage());

            String msg = "not allow to access";
            if (e instanceof UnauthorizedException) {

                // 沒有權限
                msg = "you have no permissions";
            } else if (e instanceof UnauthenticatedException) {

                // 未登錄
                msg = "you must login first";
            }
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(msg);
        }

    }

登錄邏輯

同樣,由於沒有了過濾鏈,我們需要自行實現 login 邏輯,代碼非常簡單:

    /**
     * 模擬登錄接口
     * 
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/login")
    @ResponseBody
    public String login(@RequestParam("username") String username, @RequestParam("password") String password) {

        Subject subject = SecurityUtils.getSubject();
        AuthenticationToken token = new UsernamePasswordToken(username, password.toCharArray());

        try {
            // 執行登錄
            subject.login(token);

        } catch (UnknownAccountException e) {

            // 未知用戶
            logger.warn("the account {}  is not found", username);

            return "account not found";
        } catch (IncorrectCredentialsException e) {

            // 用戶或密碼不正確
            logger.warn("the account or password is not correct");
            return "account or password not correct";

        }
        return "login success";
    }

一些常見的登錄異常如下表,可按業務需要使用:

異常 描述
UnknownAccountException 找不到用戶
IncorrectCredentialsException 用戶名密碼不正確
LockedAccountException 用戶被鎖定
ExcessiveAttemptsException 密碼重試超過次數
ExpiredCredentialsException 密鑰已經過期

登出的代碼:

    @RequestMapping("/logout")
    @ResponseBody
    public String logout() {

        Subject subject = SecurityUtils.getSubject();

        // 執行注銷
        if (subject.isAuthenticated()) {
            subject.logout();
        }
        return "OK";
    }

深入一點
shiro-spring-boot-starter 為我們實現了大量的自動裝配功能,如以下代碼片段:

@SuppressWarnings("SpringFacetCodeInspection")
@Configuration
@ConditionalOnProperty(name = "shiro.annotations.enabled", matchIfMissing = true)
public class ShiroAnnotationProcessorAutoConfiguration extends AbstractShiroAnnotationProcessorConfiguration {

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    @ConditionalOnMissingBean
    @Override
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        return super.defaultAdvisorAutoProxyCreator();
    }

    @Bean
    @ConditionalOnMissingBean
    @Override
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        return super.authorizationAttributeSourceAdvisor(securityManager);
    }
}

其中,DefaultAdvisorAutoProxyCreator 是AOP實現的關鍵類,有興趣可以繼續深入了解

進一步擴展

Shiro 的功能非常靈活,本文中的樣例僅供參考,如果要在生產環境中使用,你需要思考更多方面的東西:

  • 用戶信息、權限的存儲需要數據庫支持;
  • 為了加速權限校驗的性能,你可以使用Cache模塊;
  • 更安全的檢查,比如動態校驗碼,密碼失敗重試次數檢查;
  • 更通用的方案,比如JWT/OAUTH2.0 ,非常適用於微服務架構。

碼雲同步代碼

參考文檔

Shiro-integrating-with-spring
Shiro-integrating-with-springboot
Shiro-1.2.x-refence-waylau
Shirot-SprintBoot優雅整合

小結

Apache Shiro 是一個強大易用的安全框架,其本身也提供了非常多的特性模塊。
本文旨在介紹如何將Shiro與當前流行的SpringBoot 框架結合使用,並提供了極簡單的案例。
筆者在問題求證過程中通過閱讀部分源碼,更深入理解了其框架原理。目前認為,Shiro強大之處
還在於框架保持了簡單易用、靈活擴展的特點,相信這也是許多人青睞它的原因吧。

最后,歡迎繼續關注"美碼師的補習系列-springboot篇" ,如果覺得老司機的文章還不賴,請多多分享轉發-


免責聲明!

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



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