Spring Boot [集成-Shiro]

導讀:

在閱讀這篇文章以前假設你已經對Apache Shiro(後面統一用Shiro做爲代指)有了必定的瞭解,若是你還對Shiro不熟悉的話在這篇文章的結尾附有相關的學習資料,關於Shiro是用來作什麼的這裏有個不錯的介紹,在後面的文章中就不在對其進行描述了。後面的文章將圍繞着 Spring Boot 集成Shiro 來進行展開。html

快速上手:

1.引入pom依賴

<!-- 使用Shiro認證 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.2.5</version>
</dependency>

2.實現相關的 用戶,角色,權限等代碼的編寫:

因爲篇幅緣由這裏不進行展開 提供一個參考前端

3.實現Realm:

AbstractUserRealm繼承AuthorizingRealm,並重寫doGetAuthorizationInfo(用於獲取認證成功後的角色、權限等信息) 和 doGetAuthenticationInfo(驗證當前登陸的Subject)方法:web

public abstract class AbstractUserRealm extends AuthorizingRealm {

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

    @Autowired
    private UserRepository userRepository;
    //獲取用戶組的權限信息
    public abstract UserRolesAndPermissions doGetGroupAuthorizationInfo(User userInfo);
    //獲取用戶角色的權限信息
    public abstract UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo);

    /**
     * 獲取受權信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String currentLoginName = (String) principals.getPrimaryPrincipal();
        Set<String> userRoles = new HashSet<>();
        Set<String> userPermissions = new HashSet<>();
        //從數據庫中獲取當前登陸用戶的詳細信息
        User userInfo = userRepository.findByLoginName(currentLoginName);
        if (null != userInfo) {
            UserRolesAndPermissions groupContainer = doGetGroupAuthorizationInfo(userInfo);
            UserRolesAndPermissions roleContainer = doGetGroupAuthorizationInfo(userInfo);
            userRoles.addAll(groupContainer.getUserRoles());
            userRoles.addAll(roleContainer.getUserRoles());
            userPermissions.addAll(groupContainer.getUserPermissions());
            userPermissions.addAll(roleContainer.getUserPermissions());
        } else {
            throw new AuthorizationException();
        }
        //爲當前用戶設置角色和權限
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRoles(userRoles);
        authorizationInfo.addStringPermissions(userPermissions);
        logger.info("###【獲取角色成功】[SessionId] => {}", SecurityUtils.getSubject().getSession().getId());
        return authorizationInfo;
    }

    /**
     * 登陸認證
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authenticationToken) throws AuthenticationException {
        //UsernamePasswordToken對象用來存放提交的登陸信息
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //查出是否有此用戶
        User user = userRepository.findByLoginName(token.getUsername());
        if (user != null) {
            // 若存在,將此用戶存放到登陸認證info中,無需本身作密碼對比,Shiro會爲咱們進行密碼對比校驗
            return new SimpleAuthenticationInfo(user.getLoginName(), user.getPassword(), getName());
        }
        return null;
    }

    protected class UserRolesAndPermissions {
        Set<String> userRoles;
        Set<String> userPermissions;

        public UserRolesAndPermissions(Set<String> userRoles, Set<String> userPermissions) {
            this.userRoles = userRoles;
            this.userPermissions = userPermissions;
        }

        public Set<String> getUserRoles() {
            return userRoles;
        }

        public Set<String> getUserPermissions() {
            return userPermissions;
        }
    }
@Component
public class UserRealm extends AbstractUserRealm {

    @Override
    public UserRolesAndPermissions doGetGroupAuthorizationInfo(User userInfo) {
        Set<String> userRoles = new HashSet<>();
        Set<String> userPermissions = new HashSet<>();
        //TODO 獲取當前用戶下擁有的全部角色列表,及權限
        return new UserRolesAndPermissions(userRoles, userPermissions);
    }

    @Override
    public UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo) {
        Set<String> userRoles = new HashSet<>();
        Set<String> userPermissions = new HashSet<>();
        //TODO 獲取當前用戶下擁有的全部角色列表,及權限
        return new UserRolesAndPermissions(userRoles, userPermissions);
    }
}

4.建立Shiro配置類:

這是最重要的一步等價於常規的Spring web應用的配置文件,將相關的配置託管給Spring 管理。spring

@Configuration
public class ShiroConfiguration {

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

    /**
     * Shiro的Web過濾器Factory 命名:shiroFilter<br /> * * @param securityManager * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        logger.info("注入Shiro的Web過濾器-->shiroFilter", ShiroFilterFactoryBean.class);
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //Shiro的核心安全接口,這個屬性是必須的
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //要求登陸時的連接(可根據項目的URL進行替換),非必須的屬性,默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //登陸成功後要跳轉的鏈接,邏輯也能夠自定義,例如返回上次請求的頁面
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //用戶訪問未對其受權的資源時,所顯示的鏈接
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        /*定義shiro過濾器,例如實現自定義的FormAuthenticationFilter,須要繼承FormAuthenticationFilter **本例中暫不自定義實現,在下一節實現驗證碼的例子中體現 */

        /*定義shiro過濾鏈 Map結構 * Map中key(xml中是指value值)的第一個'/'表明的路徑是相對於HttpServletRequest.getContextPath()的值來的 * anon:它對應的過濾器裏面是空的,什麼都沒作,這裏.do和.jsp後面的*表示參數,比方說login.jsp?main這種 * authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內置的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置退出過濾器,其中的具體的退出代碼Shiro已經替咱們實現了
        filterChainDefinitionMap.put("/logout", "logout");

        // <!-- 過濾鏈定義,從上向下順序執行,通常將 /**放在最爲下邊 -->:這是一個坑呢,一不當心代碼就很差使了;
        // <!-- authc:全部url都必須認證經過才能夠訪問; anon:全部url都均可以匿名訪問-->
        filterChainDefinitionMap.put("/login", "anon");//anon 能夠理解爲不攔截
        filterChainDefinitionMap.put("/reg", "anon");
        filterChainDefinitionMap.put("/plugins/**", "anon");
        filterChainDefinitionMap.put("/pages/**", "anon");
        filterChainDefinitionMap.put("/api/**", "anon");
        filterChainDefinitionMap.put("/dists/img/*", "anon");
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    @Bean
    public EhCacheManager ehCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        return cacheManager;
    }

    /**
     * 不指定名字的話,自動建立一個方法名第一個字母小寫的bean * @Bean(name = "securityManager") * @return
     */
    @Bean
    public SecurityManager securityManager(UserRealm userRealm) {
        logger.info("注入Shiro的Web過濾器-->securityManager", ShiroFilterFactoryBean.class);
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        //注入緩存管理器;
        securityManager.setCacheManager(ehCacheManager());//這個若是執行屢次,也是一樣的一個對象;
        return securityManager;
    }

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

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

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

5.實現登陸/退出等操做:

@Controller
public class SecurityController {

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

    @Autowired
    private UserService userService;

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

    @PostMapping("/login")
    public String login(@Valid User user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
            return "login";
        }
        String loginName = user.getLoginName();
        logger.info("準備登錄用戶 => {}", loginName);
        UsernamePasswordToken token = new UsernamePasswordToken(loginName,user.getPassword());
        //獲取當前的Subject
        Subject currentUser = SecurityUtils.getSubject();
        try {
            //在調用了login方法後,SecurityManager會收到AuthenticationToken,並將其發送給已配置的Realm執行必須的認證檢查
            //每一個Realm都能在必要時對提交的AuthenticationTokens做出反應
            //因此這一步在調用login(token)方法時,它會走到MyRealm.doGetAuthenticationInfo()方法中,具體驗證方式詳見此方法
            logger.info("對用戶[" + loginName + "]進行登陸驗證..驗證開始");
            currentUser.login(token);
            logger.info("對用戶[" + loginName + "]進行登陸驗證..驗證經過");
        } catch (UnknownAccountException uae) {
            logger.info("對用戶[" + loginName + "]進行登陸驗證..驗證未經過,未知帳戶");
            redirectAttributes.addFlashAttribute("message", "未知帳戶");
        } catch (IncorrectCredentialsException ice) {
            logger.info("對用戶[" + loginName + "]進行登陸驗證..驗證未經過,錯誤的憑證");
            redirectAttributes.addFlashAttribute("message", "密碼不正確");
        } catch (LockedAccountException lae) {
            logger.info("對用戶[" + loginName + "]進行登陸驗證..驗證未經過,帳戶已鎖定");
            redirectAttributes.addFlashAttribute("message", "帳戶已鎖定");
        } catch (ExcessiveAttemptsException eae) {
            logger.info("對用戶[" + loginName + "]進行登陸驗證..驗證未經過,錯誤次數過多");
            redirectAttributes.addFlashAttribute("message", "用戶名或密碼錯誤次數過多");
        } catch (AuthenticationException ae) {
            //經過處理Shiro的運行時AuthenticationException就能夠控制用戶登陸失敗或密碼錯誤時的情景
            logger.info("對用戶[" + loginName + "]進行登陸驗證..驗證未經過,堆棧軌跡以下");
            ae.printStackTrace();
            redirectAttributes.addFlashAttribute("message", "用戶名或密碼不正確");
        }
        //驗證是否登陸成功
        if (currentUser.isAuthenticated()) {
            logger.info("用戶[" + loginName + "]登陸認證經過(這裏能夠進行一些認證經過後的一些系統參數初始化操做)");
            return "redirect:/index";
        } else {
            token.clear();
            return "redirect:/login";
        }
    }

    @GetMapping("/logout")
    public String logout(RedirectAttributes redirectAttributes) {
        //使用權限管理工具進行用戶的退出,跳出登陸,給出提示信息
        SecurityUtils.getSubject().logout();
        redirectAttributes.addFlashAttribute("message", "您已安全退出");
        return "redirect:/login";
    }


    @GetMapping("/reg")
    @ResponseBody
    public Result<String> reg(@Valid User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return Result.error("用戶信息填寫不完整");
        }
        userService.save(user);
        return Result.ok();
    }
}

6.前端頁面編寫:

一個簡單 form表單提交的demo數據庫

<form action="login" method="POST">
            <div class="form-group has-feedback">
                <input name="loginName" type="text" class="form-control" placeholder="用戶名"/>
                <span class="glyphicon glyphicon-user form-control-feedback"></span>
            </div>
            <div class="form-group has-feedback">
                <input name="password" type="password" class="form-control"/>
                <span class="glyphicon glyphicon-lock form-control-feedback"></span>
            </div>
            <div class="row">
                <!-- /.col -->
                <div class="col-xs-12">
                    <button type="submit" class="btn btn-primary btn-block btn-flat">登 錄</button>
                </div>
                <!-- /.col -->
            </div>
        </form>

擴展:

權限註解:apache

@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。

標籤
代碼驗證:
(暫時忽略)留待補充api

結語:

Shiro 做爲一款安全框架爲咱們提供了經常使用的功能,已經足夠應對絕大多數的業務須要,在下一篇文章中將介紹一款更增強大的安全框架 Spring Security。緩存

參考資料:

Spring Boot系列(十五) 安全框架Apache Shiro(一)基本功能
Spring Boot Shiro 權限管理安全

學習資料:

Apache Shiro 使用手冊
《跟開濤學Shiro》 - 博客版
跟開濤學 Shiro - wiki版
官方文檔app

相關文章
相關標籤/搜索