Spring Boot 整合 Shiro實現認證及授權管理

雲天發表於2019-07-19

Spring Boot Shiro

本示例要內容

  • 基於RBAC,授權、認證
  • 加密、解密
  • 統一異常處理
  • redis session支援

介紹

Apache Shiro 是一個功能強大且易於使用的Java安全框架,可執行身份驗證,授權,加密和會話管理。藉助Shiro易於理解的API,您可以快速輕鬆地保護任何應用程式(從最小的移動應用程式到最大的Web和企業應用程式)。

開始使用

新增依賴

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

RBAC

RBAC 是基於角色的訪問控制(Role-Based Access Control )在 RBAC 中,許可權與角色相關聯,使用者通過成為適當角色的成員而得到這些角色的許可權。這樣管理都是層級相互依賴的,許可權賦予給角色,而把角色又賦予使用者,這樣的許可權設計很清楚,管理起來很方便。

建立實體類

  • SysPermission.java
  • SysRole.java
  • UserInfo.java

採用 Jpa 技術會自動生成5張基礎表格,分別是:

  • user_info(使用者資訊表)
  • sys_role(角色表)
  • sys_permission(許可權表)
  • sys_user_role(使用者角色表)
  • sys_role_permission(角色許可權表)

初始化資料

INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理員', '7430bfdcc59212b32d78aacd42c7fe33', 'md5!@#', 0);
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'使用者管理',0,'0/','userInfo:view','menu','userInfo/userList');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (2,0,'使用者新增',1,'0/1','userInfo:add','button','userInfo/userAdd');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (3,0,'使用者刪除',1,'0/1','userInfo:del','button','userInfo/userDel');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (1,0,'管理員','admin');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (2,0,'VIP會員','vip');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (3,1,'test','test');
INSERT INTO `sys_role_permission` VALUES ('1', '1');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3,2);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);

Shiro配置

首先要配置的是 ShiroConfig 類,Apache Shiro 核心通過 Filter 來實現。使用 Filter是可以通過 URL 規則來進行過濾和許可權校驗,所以我們需要定義一系列關於 URL 的規則和訪問許可權。

@Configuration
@Slf4j
public class ShiroConfig {

    @Autowired
    private IgnoreAuthUrlProperties ignoreAuthUrlProperties;

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        log.info("Shiro過濾器開始處理");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 配置登入頁
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登入成功後跳轉頁面
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授權介面
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        //攔截器
        Map<String, String> filterMap = new LinkedHashMap<>();

        //anon:所有url都都可以匿名訪問
        Set<String> urlSet = new HashSet<>(ignoreAuthUrlProperties.getIgnoreAuthUrl());
        urlSet.stream().forEach(temp -> filterMap.put(temp, "anon"));

        //使用者未登入不進行跳轉,返回錯誤資訊
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        filters.put("authc", new MyFormAuthenticationFilter());

        //配置退出 過濾器
        filterMap.put("/logout", "logout");

        //authc:所有url都必須認證通過才可以訪問
        filterMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 憑證匹配器
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //雜湊演算法:這裡使用MD5演算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    @Bean
    public AuthRealm authRealm() {
        AuthRealm authRealm = new AuthRealm();
        authRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return authRealm;
    }

    /**
     * 安全管理器
     *
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm());
        return securityManager;
    }

    /**
     * 啟用shiro註解
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 異常處理
     *
     * @return
     */
    @Bean(name = "simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();

        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError");
        mappings.setProperty("UnauthorizedException", "403");
        r.setExceptionMappings(mappings);

        r.setDefaultErrorView("error");
        r.setExceptionAttribute("ex");
        return r;
    }
}

Shiro 內建的兩個主要 Filter介紹

  • anon:所有 url 都都可以匿名訪問
  • authc: 需要認證才能進行訪問

認證和授權


@Slf4j
public class AuthRealm extends AuthorizingRealm {

    @Resource
    private UserInfoService userInfoService;

    /**
     * 授權
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("呼叫授權方法");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        UserInfo userInfo = (UserInfo) principals.getPrimaryPrincipal();
        for (SysRole role : userInfo.getRoleList()) {
            authorizationInfo.addRole(role.getRole());
            for (SysPermission p : role.getPermissions()) {
                authorizationInfo.addStringPermission(p.getPermission());
            }
        }
        return authorizationInfo;
    }

    /**
     * 認證(主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確)
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("呼叫認證方法");
        //獲取使用者的輸入的賬號.
        String username = (String) token.getPrincipal();
        if (username == null) {
            throw new AuthenticationException("賬號名為空,登入失敗!");
        }

        log.info("credentials:" + token.getCredentials());
        UserInfo userInfo = userInfoService.findByUsername(username);
        if (userInfo == null) {
            throw new AuthenticationException("不存在的賬號,登入失敗!");
        }

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userInfo,                                               //使用者
                userInfo.getPassword(),                                 //密碼
                ByteSource.Util.bytes(userInfo.getCredentialsSalt()),   //加鹽後的密碼
                getName()                                               //指定當前 Realm 的類名
        );
        return authenticationInfo;
    }
}

登入


    /**
     * 登入
     *
     * @param username
     * @param password
     * @param map      如果出錯,回傳給前端的map
     * @return
     */
    @RequestMapping("/login")
    public String login(String username, String password, Map<String, Object> map) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        String msg = "";
        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            msg = "賬號不存在!";
        } catch (DisabledAccountException e) {
            msg = "賬號未啟用!";
        } catch (IncorrectCredentialsException e) {
            msg = "密碼錯誤!";
        } catch (Throwable e) {
            msg = "未知錯誤!";
        }

        //判斷登入是否出現錯誤
        if (msg.length() > 0) {
            map.put("msg", msg);
            return "/login";
        } else {
            return "redirect:index";
        }
    }
    

方法增加許可權驗證

    /**
     * 使用者新增
     *
     * @return
     */
    @RequestMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String userInfoAdd() {
        return "userInfoAdd";
    }

這樣配置完,執行程式。只有使用者擁有userAdd許可權才允許訪問userAdd介面,否則會提示“未授權”訪問

資料

相關文章