1. 程式人生 > >java專案許可權控制的理解和示例(基於shiro和傳統攔截器filter兩種方式)

java專案許可權控制的理解和示例(基於shiro和傳統攔截器filter兩種方式)

1.概念

  • 個人理解,許可權就是做到對不同使用者進行訪問限制,前提是保證在許可權需求設計範圍內不會出現非法也能訪問到不該訪問到的東西.因此按資料表設計將許可權分為:部門,使用者,角色,角色許可權中間表,許可權.(個人覺得一般專案可以考慮部門和角色合為一個),這裡用一個具體訪問的url地址代表一個許可權(也可以使用別的方式).下面用一個例子通過來分析.

2.場景

  • 一個超級管理員admin
    • 可以訪問並且操作網站所有功能
  • 經理manager
    • 比超級管理員許可權少一點
  • 銷售員soler和採購員buy
    • 只能使用部分功能,比經理許可權少
  • 普通使用者customer
    • 能使用的功能更少

針對以上來做各種變通分析許可權控制的需求

3.需求

  • 表設計:

    • 使用者表t_user
    • 角色表t_role(有父子關係)
    • 許可權表t_auth(有父子關係)
    • 角色和許可權中間表t_role_auth
  • 使用技術

    • 攔截器來處理
    • shiro框架處理
  • 表的使用

    • 不管是什麼身份的使用者,都是通過角色來分配許可權,也就是使用者選擇了角色就相當於選中了應有的許可權,禁止使用者對t_auth表直接關聯
    • 許可權表中是一條資訊對應一個訪問地址,並設定上下級關係
  • 對不同使用者分配角色

    • 超級管理員:
    • 這是一個特例,一個專案就一個,可以通過程式碼來控制,不過為了便於維護,在角色表新增一個叫admin的角色,然後在這個角色下面新增所有的訪問地址
    • 經理:
    • 在角色表裡面建立一個叫manager的經理角色,再對這個角色新增對應許可權
    • 銷售員soler和採購員buy
    • 在角色表裡面分別建立一個soler和buy角色,並對兩個角色新增對應許可權
    • 普通使用者customer
    • 在角色表中建立一個customer角色,並新增對應許可權
  • 相關問題

    1. 要是一個銷售員除了有soler角色外還有一些其他許可權怎麼辦呢?比如銷售員還有部分採購員一部分許可權

      • 這種情況可以採用量兩種方式解決這個問題:

        1. 建立一個新角色(比如soler2)來包含原本soler角色裡面的所有許可權和新擁有的許可權

        2. 建立一個新角色,僅包含新擁有許可權即可,然後設定該新角色的父級為soler角色,形成從屬關係

    2. 還有沒有其他特殊情況沒考慮進來的嗎?

      • 個人覺得在一個超級管理員情況下,在上面設計裡面變通幾乎能滿足所有常規的中小型專案的許可權設計方案.

4.相關技術實現(在spring+springMVC框架下maven專案)

  • 使用攔截器實現

    1. 在spring配置檔案中新增攔截器配置

      <!-- 配置登入攔截器 -->
      <mvc:interceptors>
          <mvc:interceptor>
              <mvc:mapping path="/**"/>
              <bean id="li" class="cn.util.LoginIntercepter" ></bean>
          </mvc:interceptor>
      </mvc:interceptors>

    2.攔截器處理許可權類

    • 繼承HandlerInterceptorAdapter類
    • HandlerInterceptorAdapter類的preHandle()在執行controller對應訪問方法前執行,也就是在這個方法裡面做許可權判斷,返回false後許可權驗證失敗,返回true的話進入controller對應的訪問方法
    • HandlerInterceptorAdapter類的postHandle()在controller對應訪問方法後執行
    public class LoginIntercepter extends HandlerInterceptorAdapter {
    private static String [] urls = {"uc/checklogin"};
    public static boolean checkUrl(String requestname){
        //篩選靜態資源
        if(requestname==null||requestname.equals("")){
            return true;
        }   if(requestname.endsWith(".js")||requestname.endsWith(".css")||requestname.endsWith(".jpg")){
            return true;
        }
        for (int i = 0; i < urls.length; i++) {
            if(urls[i].equals(requestname)){
                return true;
            }
        }
        return false;
    }
    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception {
        //1.登入攔截
        //2.許可權驗證,驗證當前使用者是否擁有當前請求這個許可權
        HttpSession session = request.getSession();
        //獲取當前請求的名字
        String uri = request.getRequestURI();
        String contextpath = request.getContextPath();
        String requestname = uri.substring(contextpath.length()+1, uri.length());
        //動態獲取專案的網路地址
        String basepath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+contextpath+"/";
        if(checkUrl(requestname)){
            return true;
        }
        //驗證使用者是否登入
        Object obj = session.getAttribute("users");
        if(obj==null){
            response.sendRedirect(basepath+"index.jsp");
            return false;
        }
        //某些請求(比如修改自己的基本資訊,退出登入...),是隻要求登入就可以訪問,而沒有納入許可權管理範圍的
        //對於這部分請求,應該只做登入驗證,而不進入許可權驗證範圍
        ServletContext application = session.getServletContext();
        List<String> allurl = (List<String>) application.getAttribute("allurl");
        if(!allurl.contains(requestname)){
            return true;
        }
        //如果程式執行到這裡,使用者已經登入,並且當前請求不是請求的靜態資源
        //所以這裡需要開始許可權判斷,判斷當前使用者是否擁有當前請求的許可權
        Users users = (Users) obj;
        if(users.getRole()==null){
            response.sendRedirect(basepath+"nopriviliage.jsp");
            return false;
        }
        //得到當前使用者所擁有的的所有許可權資訊
        List<Priviliage> prilist = users.getRole().getPrilist();
        if(!checkpri(prilist, requestname)){
            response.sendRedirect(basepath+"nopriviliage.jsp");
            return false;
        }
        return true;
    }
    public static boolean checkpri(List<Priviliage> prilist,String requestname){
        for (int i = 0; i < prilist.size(); i++) {
            Priviliage pri = prilist.get(i);
            if(pri.getPriUrl()!=null&&pri.getPriUrl().equals(requestname)){
                return true;
            }
        }
        return false;
    }
    
    @Override
    public void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
    }
    }
  • 使用shiro安全框架實現

    1. 匯入shiro支援包jar包

      <!-- shiro核心包 -->
       <dependency>
         <groupId>org.apache.shiro</groupId>
         <artifactId>shiro-core</artifactId>
         <version>1.2.5</version>
       </dependency>
       <!-- 新增shiro web支援 -->
       <dependency>
         <groupId>org.apache.shiro</groupId>
         <artifactId>shiro-web</artifactId>
         <version>1.2.5</version>
       </dependency>
       <!-- 新增shiro spring支援 -->
       <dependency>
         <groupId>org.apache.shiro</groupId>
         <artifactId>shiro-spring</artifactId>
         <version>1.2.5</version>
       </dependency>
       <!-- 新增log4j日誌 -->
       <dependency>
         <groupId>commons-logging</groupId>
         <artifactId>commons-logging</artifactId>
         <version>1.2</version>
       </dependency>
       <dependency>
         <groupId>log4j</groupId>
         <artifactId>log4j</artifactId>
         <version>${log4j.version}</version>
       </dependency>
       <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-api</artifactId>
         <version>${slf4j.version}</version>
       </dependency>
       <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-log4j12</artifactId>
         <version>${slf4j.version}</version>
       </dependency>

    2.在web.xml下配置過濾器

    <!-- shiro過慮器,DelegatingFilterProxy通過代理模式將spring容器中的bean和filter關聯起來 -->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <!-- 設定true由servlet容器控制filter的生命週期 -->
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
        <!-- 設定spring容器filter的bean id,如果不設定則找與filter-name一致的bean-->
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>shiroFilter</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    3.在spring配置檔案中配置shiro支援(這裡使用自定義realm)

    <!--web.xml中shiro的filter對應的bean-->
    <!-- Shiro 的Web過濾器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <!-- loginUrl認證提交地址,如果沒有認證將會請求此地址進行認證,請求此地址將由formAuthenticationFilter進行表單認證 -->
        <property name="loginUrl" value="/login.action" />
        <!--認證成功統一跳轉到first.actio,建議不配置,不配置的話shiro認證成功會自動到上一個請求路徑-->
        <property name="successUrl" value="/first.action"/>
        <property name="unauthorizedUrl" value="/refuse.jsp" />
        <!-- 過慮器鏈定義,從上向下順序執行,一般將/**放在最下邊 -->
        <property name="filterChainDefinitions">
            <value>
                <!--對靜態資源設定匿名訪問-->
                /images/**=anon
                /js/**=anon
                /style/**=anon
    
                <!--/**=anon 表示所有的url都可以匿名訪問,anon是shiro中一個過濾器的簡寫,關於shiro中的過濾器介紹見-->
                /**=anon
    
            </value>
        </property>
    </bean>
    
    <!--securityManage-->
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm" />
    </bean>
    
    <!--自定義realm-->
    <bean id="myRealm" class="com.lumingshan.web.utils.MyRealm">
    </bean>

    4.自定義realm類

    • 這裡是先執行下面的doGetAuthenticationInfo()方法,再執行doGetAuthorizationInfo()方法
    • 密碼不需要手動驗證,shiro幫忙驗證,按下面對應傳參即可
    • 這裡的驗證分為角色驗證,許可權驗證,從資料庫查詢角色和許可權分別裝在一個set集合中,在doGetAuthorizationInfo()方法中的呼叫authorizationInfo.setRoles()和authorizationInfo.setStringPermissions()把seet集合放進去,然後shiro會與上面spring配置檔案的filterChainDefinitions屬性配置檔案對應比較許可權是否符合
    public class MyRealm extends AuthorizingRealm {
    
      @Resource(name = "roleServiceImpl")
      private RoleService roleServiceImpl;
      @Resource(name = "userServiceImpl")
      private UserService userServiceImpl;
    
      @Resource(name = "authServiceImpl")
      private AuthService authServiceImpl;
    
      // 為當前登陸成功的使用者授予許可權和角色,已經登陸成功了
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(
              PrincipalCollection principals) {
          String username = (String) principals.getPrimaryPrincipal(); //獲取使用者名稱
          SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
          User loginUser = new User();
          loginUser.setUsername(username);
          User user = userServiceImpl.login(loginUser);
          Role role = roleServiceImpl.getRoleByRoleId(user.getRole().getRoleId());
          //裝成set集合
          HashSet<String> roleSet = new HashSet<>();
          roleSet.add(role.getEnName());
          authorizationInfo.setRoles(roleSet);
    
          List<Auth> authList = authServiceImpl.getAuthByRoleId(role.getRoleId());
          HashSet<String> authSet = new HashSet<>();
          for (Auth auth : authList) {
              authSet.add(auth.getEnName());
          }
    
          authorizationInfo.setStringPermissions(authSet);
          return authorizationInfo;
      }
    
      // 驗證當前登入的使用者,獲取認證資訊
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(
              AuthenticationToken token) throws AuthenticationException {
          String username = (String) token.getPrincipal(); // 獲取使用者名稱
          User loginUsers = new User();
          loginUsers.setUsername(username);
          User user = userServiceImpl.login(loginUsers);
          if(user != null) {
              //此步驗證密碼
              AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getUserpwd(), "myRealm");
              return authcInfo;
          } else {
              return null;
          }
      }
    }

    5.controller呼叫realm

    @Controller
    @RequestMapping("user")
    public class UserController {
      @RequestMapping("login")
      @ResponseBody
      public Object login(@RequestBody User user,HttpServletRequest request) {
          Subject subject = SecurityUtils.getSubject();
          UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getUserpwd());
          try{
              subject.login(token);//跳到自定義的realm中
              request.getSession().setAttribute("user", user);
              return new Result<>(null);
          }catch(Exception e){
              e.printStackTrace();
              return new Result<>(0,null);
          }
      }
    }