SpringBoot學習:整合shiro(身份認證和權限認證),使用EhCache緩存

1、在pom中引入依賴jar包css

 1 <properties>  
 2     <shiro.version>1.3.2</shiro.version>  
 3 </properties>  
 4 
 5 <!--shiro start-->  
 6     <dependency>  
 7       <groupId>org.apache.shiro</groupId>  
 8       <artifactId>shiro-core</artifactId>  
 9       <version>${shiro.version}</version>  
10     </dependency>  
11     <dependency>  
12       <groupId>org.apache.shiro</groupId>  
13       <artifactId>shiro-web</artifactId>  
14       <version>${shiro.version}</version>  
15     </dependency>  
16     <dependency>  
17       <groupId>org.apache.shiro</groupId>  
18       <artifactId>shiro-ehcache</artifactId>  
19       <version>${shiro.version}</version>  
20     </dependency>  
21     <dependency>  
22       <groupId>org.apache.shiro</groupId>  
23       <artifactId>shiro-spring</artifactId>  
24       <version>${shiro.version}</version>  
25     </dependency>  
26 <!--shiro end-->  

 

2、shiro配置類:java

  ShiroConfiguration:web

  1 package com.example.demo;
  2 
  3 import org.apache.log4j.Logger;
  4 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
  5 import org.springframework.context.annotation.Bean;
  6 import org.springframework.context.annotation.Configuration;
  7 
  8 import java.util.LinkedHashMap;
  9 import java.util.Map;
 10 
 11 /**
 12  * Shiro 配置
 13  * Apache Shiro 核心經過 Filter 來實現,就好像SpringMvc 經過DispachServlet 來主控制同樣。
 14  * 既然是使用 Filter 通常也就能猜到,是經過URL規則來進行過濾和權限校驗,因此咱們須要定義一系列關於URL的規則和訪問權限。
 15  * Created by sun on 2017-4-2.
 16  */
 17 @Configuration
 18 @EnableTransactionManagement
 19 public class ShiroConfiguration{
 20 
 21     private final Logger logger = Logger.getLogger(ShiroConfiguration.class);
 22 
 23     /**
 24      * ShiroFilterFactoryBean 處理攔截資源文件問題。
 25      * 注意:單獨一個ShiroFilterFactoryBean配置是或報錯的,由於在
 26      * 初始化ShiroFilterFactoryBean的時候須要注入:SecurityManager
 27      *
 28      Filter Chain定義說明
 29      一、一個URL能夠配置多個Filter,使用逗號分隔
 30      二、當設置多個過濾器時,所有驗證經過,才視爲經過
 31      三、部分過濾器可指定參數,如perms,roles
 32      *
 33      */
 34     @Bean
 35     public EhCacheManager getEhCacheManager(){
 36         EhCacheManager ehcacheManager = new EhCacheManager();
 37         ehcacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
 38         return ehcacheManager;
 39     }
 40 
 41     //配置shiro倉庫
 42     @Bean(name = "myShiroRealm")
 43     public MyShiroRealm myShiroRealm(EhCacheManager ehCacheManager){
 44         MyShiroRealm realm = new MyShiroRealm();
 45         realm.setCacheManager(ehCacheManager);
 46         return realm;
 47     }
 48 
 49     //把shiro生命週期交給spring boot管理
 50     @Bean(name = "lifecycleBeanPostProcessor")
 51     public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
 52         return new LifecycleBeanPostProcessor();
 53     }
 54 
 55     //DefaultAdvisorAutoProxyCreator實現Spring自動代理
 56     @Bean
 57     public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
 58         DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
 59         creator.setProxyTargetClass(true);
 60         return creator;
 61     }
 62 
 63     //權限認證信息
 64     @Bean(name = "securityManager")
 65     public DefaultWebSecurityManager defaultWebSecurityManager(MyShiroRealm realm){
 66         System.out.println("shiro~~~~~~~~啓動");
 67         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
 68         //設置realm
 69         securityManager.setRealm(realm);
 70         securityManager.setCacheManager(getEhCacheManager());
 71         return securityManager;
 72     }
 73 
 74     @Bean
 75     public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
 76         AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
 77         advisor.setSecurityManager(securityManager);
 78         return advisor;
 79     }
 80 
 81     //shiro核心攔截器
 82     @Bean(name = "shiroFilter")
 83     public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
 84         ShiroFilterFactoryBean factoryBean = new MyShiroFilterFactoryBean();
 85         factoryBean.setSecurityManager(securityManager);
 86         // 若是不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
 87         factoryBean.setLoginUrl("/login");
 88         // 登陸成功後要跳轉的鏈接
 89         factoryBean.setSuccessUrl("/welcome");
 90         factoryBean.setUnauthorizedUrl("/403");
 91         loadShiroFilterChain(factoryBean);
 92         logger.info("shiro攔截器工廠類注入成功");
 93         return factoryBean;
 94     }
 95 
 96     //加載ShiroFilter權限控制規則
 97     private void loadShiroFilterChain(ShiroFilterFactoryBean factoryBean) {
 98         /**下面這些規則配置最好配置到配置文件中*/
 99         Map<String, String> filterChainMap = new LinkedHashMap<String, String>();
100         // authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內置的一個攔截器
101         // anon:它對應的過濾器裏面是空的,什麼都沒作,能夠理解爲不攔截
102         //authc:全部url都必須認證經過才能夠訪問; anon:全部url均可以匿名訪問
103         filterChainMap.put("/permission/userInsert", "anon");
104         filterChainMap.put("/error", "anon");
105         filterChainMap.put("/tUser/insert","anon");
106         filterChainMap.put("/**", "authc");
107 
108         factoryBean.setFilterChainDefinitionMap(filterChainMap);
109     }
110 
111     /*
112         1.LifecycleBeanPostProcessor,這是個DestructionAwareBeanPostProcessor的子類,負責org.apache.shiro.util.Initializable類型bean的生命週期的,初始化和銷燬。主要是AuthorizingRealm類的子類,以及EhCacheManager類。
113         2.HashedCredentialsMatcher,這個類是爲了對密碼進行編碼的,防止密碼在數據庫裏明碼保存,固然在登錄認證的生活,這個類也負責對form裏輸入的密碼進行編碼。
114         3.ShiroRealm,這是個自定義的認證類,繼承自AuthorizingRealm,負責用戶的認證和權限的處理,能夠參考JdbcRealm的實現。
115         4.EhCacheManager,緩存管理,用戶登錄成功後,把用戶信息和權限信息緩存起來,而後每次用戶請求時,放入用戶的session中,若是不設置這個bean,每一個請求都會查詢一次數據庫。
116         5.SecurityManager,權限管理,這個類組合了登錄,登出,權限,session的處理,是個比較重要的類。
117         6.ShiroFilterFactoryBean,是個factorybean,爲了生成ShiroFilter。它主要保持了三項數據,securityManager,filters,filterChainDefinitionManager。
118         7.DefaultAdvisorAutoProxyCreator,Spring的一個bean,由Advisor決定對哪些類的方法進行AOP代理。
119         8.AuthorizationAttributeSourceAdvisor,shiro裏實現的Advisor類,內部使用AopAllianceAnnotationsAuthorizingMethodInterceptor來攔截用如下註解的方法。
120     */
121 }

 

  MyShiroRealm :spring

package com.sun.configuration;  
  
import com.sun.permission.model.Role;  
import com.sun.permission.model.User;  
import com.sun.permission.service.PermissionService;  
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;  
import org.apache.commons.lang3.builder.ToStringStyle;  
import org.apache.log4j.Logger;  
import org.apache.shiro.authc.*;  
import org.apache.shiro.authz.AuthorizationInfo;  
import org.apache.shiro.authz.SimpleAuthorizationInfo;  
import org.apache.shiro.realm.AuthorizingRealm;  
import org.apache.shiro.subject.PrincipalCollection;  
import org.springframework.beans.factory.annotation.Autowired;  
  
import java.util.List;  
  
/**  
 * shiro的認證最終是交給了Realm進行執行  
 * 因此咱們須要本身從新實現一個Realm,此Realm繼承AuthorizingRealm  
 * Created by sun on 2017-4-2.  
 */  
public class MyShiroRealm extends AuthorizingRealm {  
  
    private static final Logger logger = Logger.getLogger(MyShiroRealm.class);  
    @Autowired  
    private PermissionService permissionService;  
    /**  
     * 登陸認證  
     */  
    @Override  
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {  
        //UsernamePasswordToken用於存放提交的登陸信息  
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;  
        logger.info("登陸認證!");  
        logger.info("驗證當前Subject時獲取到token爲:" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));  
        User user = permissionService.findByUserEmail(token.getUsername());  
        if (user != null){  
            logger.info("用戶: " + user.getEmail());  
            if(user.getStatus() == 0){  
                throw new DisabledAccountException();  
            }  
            // 若存在,將此用戶存放到登陸認證info中,無需本身作密碼對比,Shiro會爲咱們進行密碼對比校驗  
            return new SimpleAuthenticationInfo(user.getEmail(), user.getPswd(), getName());  
        }  
        return null;  
    }  
  
    /**  
     * 權限認證(爲當前登陸的Subject授予角色和權限)  
     *  
     * 該方法的調用時機爲需受權資源被訪問時,而且每次訪問需受權資源都會執行該方法中的邏輯,這代表本例中並未啓用AuthorizationCache,  
     * 若是連續訪問同一個URL(好比刷新),該方法不會被重複調用,Shiro有一個時間間隔(也就是cache時間,在ehcache-shiro.xml中配置),  
     * 超過這個時間間隔再刷新頁面,該方法會被執行  
     *  
     * doGetAuthorizationInfo()是權限控制,  
     * 當訪問到頁面的時候,使用了相應的註解或者shiro標籤纔會執行此方法不然不會執行,  
     * 因此若是隻是簡單的身份認證沒有權限的控制的話,那麼這個方法能夠不進行實現,直接返回null便可  
     */  
    @Override  
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {  
        String loginName = (String) super.getAvailablePrincipal(principals);  
        User user = permissionService.findByUserEmail(loginName);  
        logger.info("權限認證!");  
        if (user != null){  
            // 權限信息對象info,用來存放查出的用戶的全部的角色及權限  
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();  
            //用戶的角色集合  
            info.setRoles(permissionService.getRolesName(user.getId()));  
            List<Role> roleList = permissionService.getRoleList(user.getId());  
            for (Role role : roleList){  
                //用戶的角色對應的全部權限  
                logger.info("角色: "+role.getName());  
                info.addStringPermissions(permissionService.getPermissionsName(role.getId()));  
            }  
            return info;  
        }  
        // 返回null將會致使用戶訪問任何被攔截的請求時都會自動跳轉到unauthorizedUrl指定的地址  
        return null;  
    }  
}  

 

  MyShiroFilterFactoryBean:數據庫

 1 package com.sun.configuration;  
 2   
 3 import org.apache.shiro.mgt.SecurityManager;  
 4 import org.apache.shiro.spring.web.ShiroFilterFactoryBean;  
 5 import org.apache.shiro.web.filter.mgt.FilterChainManager;  
 6 import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;  
 7 import org.apache.shiro.web.mgt.WebSecurityManager;  
 8 import org.apache.shiro.web.servlet.AbstractShiroFilter;  
 9 import org.springframework.beans.factory.BeanInitializationException;  
10   
11 import javax.servlet.FilterChain;  
12 import javax.servlet.ServletException;  
13 import javax.servlet.ServletRequest;  
14 import javax.servlet.ServletResponse;  
15 import javax.servlet.http.HttpServletRequest;  
16 import java.io.IOException;  
17 import java.util.HashSet;  
18 import java.util.Set;  
19   
20 /**  
21  * Created by sun on 2017-4-2.  
22  */  
23 public class MyShiroFilterFactoryBean extends ShiroFilterFactoryBean {  
24     // ShiroFilter將直接忽略的請求  
25     private Set<String> ignoreExt;  
26   
27     public MyShiroFilterFactoryBean(){  
28         super();  
29         ignoreExt = new HashSet<String>();  
30         ignoreExt.add(".jpg");  
31         ignoreExt.add(".png");  
32         ignoreExt.add(".gif");  
33         ignoreExt.add(".bmp");  
34         ignoreExt.add(".js");  
35         ignoreExt.add(".css");  
36     }  
37     /**  
38      * 啓動時加載  
39      */  
40     @Override  
41     protected AbstractShiroFilter createInstance() throws Exception {  
42         SecurityManager securityManager = getSecurityManager();  
43         if (securityManager == null){  
44             throw new BeanInitializationException("SecurityManager property must be set.");  
45         }  
46   
47         if (!(securityManager instanceof WebSecurityManager)){  
48             throw new BeanInitializationException("The security manager does not implement the WebSecurityManager interface.");  
49         }  
50   
51         PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();  
52         FilterChainManager chainManager = createFilterChainManager();  
53         chainResolver.setFilterChainManager(chainManager);  
54         return new MySpringShiroFilter((WebSecurityManager)securityManager, chainResolver);  
55     }  
56   
57     /**  
58      * 啓動時加載  
59      */  
60     private class MySpringShiroFilter extends AbstractShiroFilter {  
61         public MySpringShiroFilter(  
62                 WebSecurityManager securityManager, PathMatchingFilterChainResolver chainResolver) {  
63             super();  
64             if (securityManager == null){  
65                 throw new IllegalArgumentException("WebSecurityManager property cannot be null.");  
66             }  
67             setSecurityManager(securityManager);  
68             if (chainResolver != null){  
69                 setFilterChainResolver(chainResolver);  
70             }  
71         }  
72         /**  
73          * 頁面上傳輸的url先進入此方法驗證  
74          */  
75         @Override  
76         protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse,  
77                                         FilterChain chain)  
78                 throws ServletException, IOException {  
79             HttpServletRequest request = (HttpServletRequest)servletRequest;  
80             String str = request.getRequestURI().toLowerCase();  
81             boolean flag = true;  
82             int idx = 0;  
83             if ((idx = str.lastIndexOf(".")) > 0){  
84                 str = str.substring(idx);  
85                 if (ignoreExt.contains(str.toLowerCase())){  
86                     flag = false;  
87                 }  
88             }  
89             if (flag){  
90                 super.doFilterInternal(servletRequest, servletResponse, chain);  
91             } else {  
92                 chain.doFilter(servletRequest, servletResponse);  
93             }  
94         }  
95     }  
96 }  

 

ehcache-shiro.xml:apache

 1 <?xml version="1.0" encoding="UTF-8"?>  
 2 <ehcache updateCheck="false" name="shiroCache">  
 3     <!--  
 4        name:緩存名稱。  
 5        maxElementsInMemory:緩存最大數目  
 6        maxElementsOnDisk:硬盤最大緩存個數。  
 7        eternal:對象是否永久有效,一但設置了,timeout將不起做用。  
 8        overflowToDisk:是否保存到磁盤,當系統當機時  
 9        timeToIdleSeconds:設置對象在失效前的容許閒置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,默認值是0,也就是可閒置時間無窮大。  
10        timeToLiveSeconds:設置對象在失效前容許存活時間(單位:秒)。最大時間介於建立時間和失效時間之間。僅當eternal=false對象不是永久有效時使用,默認是0.,也就是對象存活時間無窮大。  
11        diskPersistent:是否緩存虛擬機重啓期數據 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.  
12        diskSpoolBufferSizeMB:這個參數設置DiskStore(磁盤緩存)的緩存區大小。默認是30MB。每一個Cache都應該有本身的一個緩衝區。  
13        diskExpiryThreadIntervalSeconds:磁盤失效線程運行時間間隔,默認是120秒。  
14        memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。默認策略是LRU(最近最少使用)。你能夠設置爲FIFO(先進先出)或是LFU(較少使用)。  
15         clearOnFlush:內存數量最大時是否清除。  
16          memoryStoreEvictionPolicy:  
17             Ehcache的三種清空策略;  
18             FIFO,first in first out,這個是你們最熟的,先進先出。  
19             LFU, Less Frequently Used,就是上面例子中使用的策略,直白一點就是講一直以來最少被使用的。如上面所講,緩存的元素有一個hit屬性,hit值最小的將會被清出緩存。  
20             LRU,Least Recently Used,最近最少使用的,緩存的元素有一個時間戳,當緩存容量滿了,而又須要騰出地方來緩存新的元素的時候,那麼現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。  
21     -->  
22     <defaultCache  
23             maxElementsInMemory="10000"  
24             eternal="false"  
25             timeToIdleSeconds="120"  
26             timeToLiveSeconds="120"  
27             overflowToDisk="false"  
28             diskPersistent="false"  
29             diskExpiryThreadIntervalSeconds="120"  
30     />  
31     <!-- 登陸記錄緩存鎖定10分鐘 -->  
32     <cache name="passwordRetryCache"  
33            maxEntriesLocalHeap="2000"  
34            eternal="false"  
35            timeToIdleSeconds="3600"  
36            timeToLiveSeconds="0"  
37            overflowToDisk="false"  
38            statistics="true">  
39     </cache>  
40 </ehcache>  

登陸的controller類:緩存

  1 package com.sun.permission.controller;  
  2   
  3 import com.sun.permission.model.User;  
  4 import com.sun.permission.service.PermissionService;  
  5 import com.sun.util.CommonUtils;  
  6 import org.apache.commons.lang3.StringUtils;  
  7 import org.apache.log4j.Logger;  
  8 import org.apache.shiro.SecurityUtils;  
  9 import org.apache.shiro.authc.*;  
 10 import org.apache.shiro.session.Session;  
 11 import org.apache.shiro.subject.Subject;  
 12 import org.springframework.beans.factory.annotation.Autowired;  
 13 import org.springframework.stereotype.Controller;  
 14 import org.springframework.validation.BindingResult;  
 15 import org.springframework.web.bind.annotation.RequestMapping;  
 16 import org.springframework.web.bind.annotation.RequestMethod;  
 17 import org.springframework.web.servlet.ModelAndView;  
 18 import org.springframework.web.servlet.mvc.support.RedirectAttributes;  
 19   
 20 import javax.validation.Valid;  
 21   
 22 /**  
 23  * Created by sun on 2017-4-2.  
 24  */  
 25 @Controller  
 26 public class LoginController {  
 27     private static final Logger logger = Logger.getLogger(LoginController.class);  
 28     @Autowired  
 29     private PermissionService permissionService;  
 30   
 31     @RequestMapping(value="/login",method= RequestMethod.GET)  
 32     public ModelAndView loginForm(){  
 33         ModelAndView model = new ModelAndView();  
 34         model.addObject("user", new User());  
 35         model.setViewName("login");  
 36         return model;  
 37     }  
 38   
 39     @RequestMapping(value="/login",method=RequestMethod.POST)  
 40     public String login(@Valid User user, BindingResult bindingResult, RedirectAttributes redirectAttributes){  
 41         if(bindingResult.hasErrors()){  
 42             return "redirect:login";  
 43         }  
 44         String email = user.getEmail();  
 45         if(StringUtils.isBlank(user.getEmail()) || StringUtils.isBlank(user.getPswd())){  
 46             logger.info("用戶名或密碼爲空! ");  
 47             redirectAttributes.addFlashAttribute("message", "用戶名或密碼爲空!");  
 48             return "redirect:login";  
 49         }  
 50         //驗證  
 51         UsernamePasswordToken token = new UsernamePasswordToken(user.getEmail(), user.getPswd());  
 52         //獲取當前的Subject  
 53         Subject currentUser = SecurityUtils.getSubject();  
 54         try {  
 55             //在調用了login方法後,SecurityManager會收到AuthenticationToken,並將其發送給已配置的Realm執行必須的認證檢查  
 56             //每一個Realm都能在必要時對提交的AuthenticationTokens做出反應  
 57             //因此這一步在調用login(token)方法時,它會走到MyRealm.doGetAuthenticationInfo()方法中,具體驗證方式詳見此方法  
 58             logger.info("對用戶[" + email + "]進行登陸驗證..驗證開始");  
 59             currentUser.login(token);  
 60             logger.info("對用戶[" + email + "]進行登陸驗證..驗證經過");  
 61         }catch(UnknownAccountException uae){  
 62             logger.info("對用戶[" + email + "]進行登陸驗證..驗證未經過,未知帳戶");  
 63             redirectAttributes.addFlashAttribute("message", "未知帳戶");  
 64         }catch(IncorrectCredentialsException ice){  
 65             logger.info("對用戶[" + email + "]進行登陸驗證..驗證未經過,錯誤的憑證");  
 66             redirectAttributes.addFlashAttribute("message", "密碼不正確");  
 67         }catch(LockedAccountException lae){  
 68             logger.info("對用戶[" + email + "]進行登陸驗證..驗證未經過,帳戶已鎖定");  
 69             redirectAttributes.addFlashAttribute("message", "帳戶已鎖定");  
 70         }catch(ExcessiveAttemptsException eae){  
 71             logger.info("對用戶[" + email + "]進行登陸驗證..驗證未經過,錯誤次數大於5次,帳戶已鎖定");  
 72             redirectAttributes.addFlashAttribute("message", "用戶名或密碼錯誤次數大於5次,帳戶已鎖定");  
 73         }catch (DisabledAccountException sae){  
 74             logger.info("對用戶[" + email + "]進行登陸驗證..驗證未經過,賬號已經禁止登陸");  
 75             redirectAttributes.addFlashAttribute("message", "賬號已經禁止登陸");  
 76         }catch(AuthenticationException ae){  
 77             //經過處理Shiro的運行時AuthenticationException就能夠控制用戶登陸失敗或密碼錯誤時的情景  
 78             logger.info("對用戶[" + email + "]進行登陸驗證..驗證未經過,堆棧軌跡以下");  
 79             ae.printStackTrace();  
 80             redirectAttributes.addFlashAttribute("message", "用戶名或密碼不正確");  
 81         }  
 82         //驗證是否登陸成功  
 83         if(currentUser.isAuthenticated()){  
 84             logger.info("用戶[" + email + "]登陸認證經過(這裏能夠進行一些認證經過後的一些系統參數初始化操做)");  
 85             //把當前用戶放入session  
 86             Session session = currentUser.getSession();  
 87             User tUser = permissionService.findByUserEmail(email);  
 88             session.setAttribute("currentUser",tUser);  
 89             return "/welcome";  
 90         }else{  
 91             token.clear();  
 92             return "redirect:login";  
 93         }  
 94     }  
 95   
 96     @RequestMapping(value="/logout",method=RequestMethod.GET)  
 97     public String logout(RedirectAttributes redirectAttributes ){  
 98         //使用權限管理工具進行用戶的退出,跳出登陸,給出提示信息  
 99         SecurityUtils.getSubject().logout();  
100         redirectAttributes.addFlashAttribute("message", "您已安全退出");  
101         return "redirect:login";  
102     }  
103   
104     @RequestMapping("/403")  
105     public String unauthorizedRole(){  
106         logger.info("------沒有權限-------");  
107         return "errorPermission";  
108     }  
109   
110 }  
相關文章
相關標籤/搜索