springboot-shiro

## name發表於2021-01-01

springboot-shiro

簡介

shiro是apache下的一個輕量級開源專案,相對於springSecurity簡單的多。

三大功能模組:

  • Subject:主體,一般指使用者;
  • SecurityManager:安全管理器,管理所有Subject,可以配合內部安全元件;
  • Realms:用於進行許可權資訊驗證,一般需要自己實現。

細分功能:

  • Authentication:身份認證/登入;
  • Authorization:授權;
  • Session Manager:會話管理,即登入後的session;
  • Cryptography:加密,密碼加密登;
  • Web Support:web支援;
  • Caching:快取,使用者資訊、角色、許可權等快取redis等快取中;
  • Concurrency:多執行緒併發驗證;
  • Testing:測試支援;
  • Run As:允許一個使用者假裝另一個使用者(如果允許);
  • Remember Me:記住密碼;

開始

新增依賴

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

密碼比較器

這個Bean定義了密碼的加密方式、加密鹽值和加密次數;當然也可以自己寫個類繼承HashedCredentialsMatcher類,從而進一步自定義密碼匹配(例如:新增密碼錯誤次數限制);

@Bean
public CredentialsMatcher credentialsMatcher() {
    // 如果要用redis,可以將RedisCacheManager作為構造引數
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    //加密方式
    credentialsMatcher.setHashAlgorithmName(md5);
    //加密迭代次數
    credentialsMatcher.setHashIterations(md5Time);
    //true加密用的hex編碼,false用的base64編碼
    credentialsMatcher.setStoredCredentialsHexEncoded(true);
    return credentialsMatcher;
}

認證和授權

這裡其實就是配置一個認證域,

  • 認證:可以獲取使用者輸入的使用者名稱和密碼、token型別,可以設定密碼匹配規則,最後需要返回一個AuthenticationInfo交給shiro處理(這裡是可以進行很大程度上的自定義認證操作,例如:免密登入,豐富使用者資訊到session等);
  • 授權:主要是給使用者設定角色和許可權,需要注意的是,授權方法是在第一次訪問授權的地址時才會執行;
@Bean
public AuthorizingRealm pwdAuthorizingRealm(CredentialsMatcher credentialsMatcher) {
    return new AuthorizingRealm(credentialsMatcher) {
        // 認證
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            // 使用者輸入的使用者名稱和密碼
            String userName = token.getPrincipal().toString();
            // String userPwd = new String((char[]) token.getCredentials());
            // todo: 根據使用者名稱從資料庫獲取密碼,這裡固定為:5678
            String password = "25d55ad283aa400af464c76d713c07ad";
            if (userName == null) {
                return null;
            }
            // 自定義密碼加密鹽值,可以不要,預設沒有加密。也可以在這裡自定義密碼匹配。
            ByteSource credentialsSalt = ByteSource.Util.bytes(md5Salt);
            return new SimpleAuthenticationInfo(userName, password, credentialsSalt, getName());
        }

        // 授權
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            // todo: 可以根據登入名稱查詢角色、許可權資訊、同時還可以將角色資訊快取起來
            String username = (String) principals.getPrimaryPrincipal();
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            // 設定角色
            Set<String> roles = new HashSet<>();
            roles.add(username);
            roles.add("role_xxx");
            info.setRoles(roles);
            // 直接設定許可權
            Set<String> stringSet = new HashSet<>();
            stringSet.add(username);
            info.setStringPermissions(stringSet);
            return info;
        }
    };
}

redis

開始

使用redis管理快取和會話(可以不要);

  • 參考springboot在專案中引入redis;

  • 新增依賴

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.2.3</version>
</dependency>

Bean

配置好這些Bean並不代表就成功整合redis了,還要在後面的會話管理器、許可權管理器中注入這些Bean;

@Bean
public RedisManager redisManager(RedisProperties redisProperties) {
    RedisManager redisManager = new RedisManager();
    redisManager.setHost(redisProperties.getHost() + ":" + redisProperties.getPort());
    redisManager.setPassword(redisProperties.getPassword());
    redisManager.setTimeout(1800);
    return redisManager;
}

@Bean
public RedisSessionDAO redisSessionDAO(RedisManager redisManager) {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager);
    return redisSessionDAO;
}

@Bean
public RedisCacheManager redisCacheManager(RedisManager redisManager) {
    RedisCacheManager cacheManager = new RedisCacheManager();
    cacheManager.setRedisManager(redisManager);
    return cacheManager;
}

會話管理器

可以設定一些會話管理器的引數,例如是否使用redis;

@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    // 這兩行用於整合redis
    // sessionManager.setSessionDAO(sessionDAO);
    // sessionManager.setCacheManager(cacheManager);
    return sessionManager;
}

許可權管理器

可以設定一些會話管理器的引數,例如是否使用redis快取,配置一個或者多個Realm域

@Bean
public DefaultWebSecurityManager securityManager(AuthorizingRealm pwdAuthorizingRealm, DefaultWebSessionManager sessionManager) {
    DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
    // 這行用於整合redis
    // defaultSecurityManager.setCacheManager(cacheManager);
    defaultSecurityManager.setSessionManager(sessionManager);
    defaultSecurityManager.setRealm(pwdAuthorizingRealm);
    return defaultSecurityManager;
}

自定義filterMap

這其實就是個Map,可以在這個map中自定義shiro許可權過濾器,其中Map的key為要自定義的許可權名(如:roles、perms、authc等),Map的value為自定義的過濾方法,下面的示例程式碼實現了將角色過濾的判斷邏輯有原來預設的and改為or(如果不需要自定義,可以不要這個Map);

public Map<String, Filter> filterMap(){
    Map<String, Filter> filterMap = new HashMap<>();
    // 自定義角色過濾器,將and改成or,同時role無許可權返回json
    filterMap.put("roles", new RolesAuthorizationFilter() {
        @Override
        public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            String[] roles = (String[]) mappedValue;
            if (roles == null || roles.length == 0) {
                return true;
            }
            Subject subject = getSubject(request, response);
            for (String role : roles) {
                if (subject.hasRole(role)) return true;
            }
            return false;
        }

        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
            Subject subject = getSubject(request, response);
            HttpServletResponse res = (HttpServletResponse) response;
            res.setContentType("application/json;charset=utf-8");
            if (subject.getPrincipal() == null) {
                saveRequestAndRedirectToLogin(request, response);
                /*res.setStatus(HttpStatus.UNAUTHORIZED.value());
                    res.getWriter().write("請先登入");*/
            } else {
                res.setStatus(HttpStatus.FORBIDDEN.value());
                res.getWriter().write("您沒有訪問許可權");
            }
            return false;
        }
    });
    // 自定義登入攔截,未登入的訪問直接返回json,而不是跳轉
    /*filterMap.put("authc", new UserFilter(){
            @Override
            protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
                HttpServletResponse res = (HttpServletResponse) response;
                res.setStatus(HttpStatus.UNAUTHORIZED.value());
                res.setContentType("application/json;charset=utf-8");
                res.getWriter().write("請先登入");
            }
        });*/
    return filterMap;
}

資源許可權

這個其實就是LinkedHashMap,需要注意的是這是個有順序的Map,先插入的許可權資料優先判斷,另外這個Map的鍵是訪問url規則,值為具體許可權,取值有:

  • authc:所有url都必須認證通過才可以訪問;
  • anon:所有url都都可以匿名訪問
  • user:如果使用rememberMe的功能可以直接訪問
  • perms: 該資源必須得到資源許可權可以訪問
  • roles: 該資源必須得到角色許可權才能訪問
public Map<String, String> findFilterChainDefinitionMap(){
    // todo: 後面這個可以直接從資料庫裡面獲取
    // 注意這個map
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    //按順序依次判斷
    filterChainDefinitionMap.put("/favicon.ico", "anon");
    filterChainDefinitionMap.put("/login/login", "anon");
    filterChainDefinitionMap.put("/login/login/pwd", "anon");
    filterChainDefinitionMap.put("/login/logout", "logout");
    filterChainDefinitionMap.put("/admin", "roles[admin]");
    filterChainDefinitionMap.put("/user", "roles[user]");
    // 注意許可權設定順序
    filterChainDefinitionMap.put("/a/1", "roles[admin]");
    filterChainDefinitionMap.put("/a/**", "roles[user, admin]");
    // 這種預設是and,即同時擁有admin和user才能訪問
    filterChainDefinitionMap.put("/info", "roles[admin,user]");
    filterChainDefinitionMap.put("/perms", "perms[admin]");
    
    filterChainDefinitionMap.put("/**", "authc");
    return filterChainDefinitionMap;
}

ShiroFilterFactoryBean

Shiro的核心配置類

@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 上面自定義的filterMap
    shiroFilterFactoryBean.setFilters(shiroService.filterMap());
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    shiroFilterFactoryBean.setLoginUrl("/login/login");
    shiroFilterFactoryBean.setUnauthorizedUrl("/deny");
    // 上面編寫資源許可權Map
    Map<String, String> filterChainDefinitionMap = shiroService.findFilterChainDefinitionMap();
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
}

登入

@PostMapping("/login")
public String login(String username, String password, Model model, HttpServletRequest req) {
    // 在認證提交前準備 token(令牌)
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    // 執行認證登陸
    // 可以獲取登入前的訪問request物件
    String url = WebUtils.getSavedRequest(req).getRequestURI();
    System.out.println(url);
    // 從SecurityUtils裡邊建立一個 subject
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.login(token);
    } catch (AuthenticationException e) {
        // 包括未知賬戶、密碼錯誤、使用者名稱或密碼錯誤次數過多、賬戶已鎖定、使用者名稱或密碼不正確等異常
        model.addAttribute("msg", e.getMessage());
    }
    if (subject.isAuthenticated()) {
        Session session = subject.getSession();
        // session.setTimeout(30 * 1000);
        session.setAttribute("user", "這裡有登入資訊");
        model.addAttribute("msg", "登入成功");
        return "index";
    } else {
        token.clear();
    }
    return "login";
}