springMVC整合shiro權限框架示例與實踐

爲何寫這篇文章

看過那麼多框架、教程,大部分shiro的文章或教程是我見過思路最糟糕的。做者完不清楚想要表達什麼起到什麼做用,把大段大段的理論講一通。你見過哪一個java教程上來就給你講一堆基礎類庫,講虛擬機的。或者hibernate教程上來就給講他有的設計有多精妙,管理的東西有多龐大的。html

而後我曾經硬着頭看了1周的所謂shiro教程,看完發現本身仍是什麼都不會,什麼也作不出來。倍受打擊。當年初學時看think in java都沒這麼失落過。前端

後來想一想不對,就直接去找spring整合shiro的教程。折騰了一週總算作出來一個能夠項目實用的東西了。但中間走過很多坑,其中有些多是做者漏了,還有些是由於我也是整合shiro的要適應項目裏的各個東西,適合本身項目的用法(這裏吐槽一下,shiro會亂的緣由就是配置的方式太多種了,並且好多文章都力求全面講,對於一個項目真不須要全用到)。 不要跟我講什麼使用文本管理配置權限,什麼寫根據角色控制訪問,哪一個能用的項目會這麼搞。浪費lz時間。 說什麼從簡單入手,你這個簡單沒鳥用,我後要改爲從數據庫讀權限列表,讀角色,根本就不可能在你這個簡單的例子上逐漸改造,這還不是浪費時間仍是什麼,並且會用到你這框架的人,本身沒幾個項目拿來練手麼。這不是浪費時間是什麼。java

另外各文章或教程,shiro的運行原理或者方式,隻字未提,極力各類介紹概念。喂,咱們不是搞學術的。後面看有些文章會把各個過程當中加入做用說明和本身的理解的話,這個還挺不錯的。但一直內心有一個疑問困擾着我,shiro是基於session(sessionId),仍是基於tokken(每次訪問都要傳),由於我一直不知道前端要傳什麼參數,登陸功能完成後,一直調不通登陸以後的接口,初學時也不少東西不知道。諷刺的是這個答案要等本身調通了才知道。答案:shiro是基於session(sessionId),至少默認是這樣的,tokken方式我沒研究過(也不是個人菜,總感受性能太差了,心生厭惡。流量不大,小項目仍是session好用)。 真·心累。寫這篇文章就是爲了解救像我以前同樣迷茫的同窗。web

解決什麼問題

搭建一個項目能夠用的shiro。 集成如下內容,使用相同內容的同窗能夠直接搬過去用了:ajax

  1. spring,使用xml配置。喜歡註解配置的能夠本身改造或找別的文章,個別文章我也是根據註解去配置xml的,看得懂就行。
  2. springMVC
  3. ACL(訪問控制列表,用戶角色權限都有),從數據庫讀取,在系統中是可配置的。
  4. 基於註解在contrllor方法之上的。這是我使用shiro的最初需求,由於restfull API中的通配符和path value,使用傳統url攔截器(或過濾器),識別和判斷很麻煩,檢測太仔細又怕性能太差。
  5. ORM什麼的隨便,這個基本和shiro不要緊,我這裏用了hibernate
  6. 先後端分離,返回json數據。因此不須要跳轉(未登陸或沒權限時)。

執行步驟

  1. 訪問服務器,web.xml中的shiroFilter攔截,
  2. 攔截進入applicationContext-shiro.xml中配置的shiroFilter。在filterChainDefinitions中配置,/api/sys/login = anon 開放登錄接口,能夠直接訪問。
  3. 調用Login2Controller中的接口login方法,在方法中,帳號和密碼生成token,執行登陸subject.login(token)
  4. 調用MyRealm#doGetAuthenticationInfo方法,驗證登陸信息。若是登陸失敗,拋出異常throw new UnknownAccountException("帳號或密碼錯誤")。若是登陸成功,返回一個由用戶信息、密碼初始化的 SimpleAuthenticationInfo
  5. 回到login方法。處理登陸失敗和成功的狀況:登陸失敗時這裏直接拋出異常,會有@ControllerAdvice統一處理springmvc的異常,返回json數據。登陸成功會把一些當前登陸用戶信息放到session中方便取用提升程序性能,如用戶信息、部門、角色、權限等。要存在哪裏看具體狀況,普通sesssion、shiro的session,我直接存在principal裏。到這裏完成登陸。
  6. 訪問須要登陸的接口,看applicationContext-shiro.xml中配置的shiroFilter,不是= anon的接口。會調用配置的shiroLoginFilter。
  7. 執行ShiroLoginFilter#isAccessAllowed方法,判斷是否已登陸。若是未登陸執行下面的onAccessDenied方法,自定義返回結果。
  8. 若是訪問的接口有配置(註解)權限,會調用MyRealm#doGetAuthorizationInfo方法,組織權限列表和controller方法上註解的@RequiresPermissions("user")匹配,匹配失敗時會拋出UnauthorizedException異常,這個由@ControllerAdvice統一處理並返回json數據。若是匹配權限經過則正常返回接口執行結果。
  9. 退出登陸,調用此方法SecurityUtils.getSubject().logout()。

示例

文件列表或涉及配置文件

  1. shiroFilter 在web.xml配置過濾器,shiro的入口
  2. applicationContext-shiro.xml,shiro在spring中的配置信息,由spring引入。
  3. MyRealm 自定義登陸的判斷doGetAuthenticationInfo(登陸時執行一次),以及權限列表的組裝doGetAuthorizationInfo(每次訪問須要權限的接口都會執行)。在applicationContext-shiro.xml中配置
  4. ShiroLoginFilter.java,是否已登陸判斷和未登陸的處理,這裏是返回json。
  5. Login2Controller 登陸的接口#login,還有登出的接口#logout,以及當前登陸信息的獲取。

關鍵代碼

WEB-INF/web.xml,其餘不相關內容省略算法

<web-app>
  <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>
</web-app>

applicationContext-shiro.xml,獨立的文件引入到spring的配置中,能夠在web.xml中引入也能夠在總的applicationContext.xml importspring

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
  http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

    <!-- id屬性值要對應 web.xml中shiro的filter對應的bean -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"></property>
        <property name="filters">
            <util:map>
                <entry key="authc" value-ref="shiroLoginFilter" />
            </util:map>
        </property>
        <!-- 未登陸跳轉頁面,請求地址將由formAuthenticationFilter進行表單認證 -->
        <!-- 本項目經過ajax訪問,由ShiroLoginFilter中處理返回json信息 -->
        <!--<property name="loginUrl" value="/notLogin"></property>-->

        <!-- 認證成功統一跳轉到頁面,建議不配置,shiro認證成功會默認跳轉到上一個請求路徑 -->
        <!-- 本項目經過ajax訪問,loginController#loging中直接返回json信息 -->
        <!-- <property name="successUrl" value="/first.action"></property> -->

        <!-- 經過unauthorizedUrl指定沒有權限操做時跳轉頁面,這個位置會攔截不到,下面有給出解決方法 -->
        <!-- 本項目經過ajax訪問,由BaseController中@ExceptionHandler捕獲異常處理 -->
        <!--<property name="unauthorizedUrl" value="/refuse"></property>-->

        <!-- 過濾器定義,從上到下執行,通常將/**放在最下面 -->
        <property name="filterChainDefinitions">
            <value>
                <!-- 對靜態資源設置匿名訪問 -->
                /assets/** = anon
                <!--開放登錄接口-->
                /api/sys/login = anon
                /api/sys/logout = anon
                /login.html = anon
                <!-- /**=authc 全部的url都必須經過認證才能夠訪問 -->
                /** = authc
                <!-- /**=anon 全部的url均可以匿名訪問,不能配置在最後一排,否則全部的請求都不會攔截 -->
            </value>
        </property>
    </bean>
    <!--使用ajax訪問,自定義未登陸返回信息-->
    <bean id="shiroLoginFilter" class="com.hammer.acl.shiro.ShiroLoginFilter"></bean>

    <!-- 解決shiro配置的沒有權限訪問時,unauthorizedUrl不跳轉到指定路徑的問題 -->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="org.apache.shiro.authz.UnauthorizedException">/refuse</prop>
            </props>
        </property>
    </bean>

    <!-- securityManager安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"></property>
    </bean>

    <!-- 配置自定義Realm -->
    <bean id="myRealm" class="com.hammer.acl.shiro.MyRealm">
        <!-- 將憑證匹配器設置到realm中,realm按照憑證匹配器的要求進行散列 -->
        <property name="credentialsMatcher" ref="credentialsMatcher"></property>
    </bean>

    <!-- 憑證匹配器 -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <!-- 加密算法 -->
        <property name="hashAlgorithmName" value="md5"></property>
        <!-- 迭代次數 -->
        <property name="hashIterations" value="1"></property>
    </bean>
</beans>

MyRealm.java數據庫

/**
 * 自定義的Realm
 */
public class MyRealm extends AuthorizingRealm {

  @Autowired
  private UserService userService;
  @Autowired
  private LoginService loginService;

  // 設置realm的名稱
  @Override
  public void setName(String name) {
    super.setName("customRealm");
  }

  /**
   * 認證的方法,登陸時執行
   *
   * @param token
   * @return
   * @throws AuthenticationException
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //System.out.println("————身份認證方法————");
    // token是用戶輸入的用戶名和密碼
    // 第一步從token中取出用戶名
    final String loginId = (String) token.getPrincipal();
    String password = null;
    final Object credentials = token.getCredentials();
    if (credentials instanceof char[]) {
      password = new String((char[]) credentials);
    }

    // 第二步:根據用戶輸入從數據庫查詢用戶信息
    User user = loginService.getUse4Login(loginId, password);
    if (user == null) {
      throw new UnknownAccountException("帳號或密碼錯誤");
    }

    // 從數據庫查詢到密碼
    //配合shiro配置的mc5加密(應該能夠配置爲不加密)
    if (password != null) {
      password = DigestUtils.md5Hex(password);
    }
    //加密的鹽
    //String salt = user.getSalt();

    final HashMap<String, Object> principal = new HashMap<>();
    principal.put("user", user);
    return new SimpleAuthenticationInfo(principal, password, this.getName());
  }

  /**
   * 受權的方法,每次訪問須要權限的接口都會執行
   *
   * @param principals
   * @return
   */
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    //System.out.println("————權限認證————");
    //從principals獲取主身份信息
    //將getPrimaryPrincipal方法返回值轉爲真實身份類型(在上邊doGetAuthenticationInfo認證經過填充到SimpleAuthenticationInfo中的身份類型)

    //如下方法等效SecurityUtils.getSubject().getPrincipal() principals.getPrimaryPrincipal()
    //Map principal = (Map) SecurityUtils.getSubject().getPrincipal();
    Map principal = (Map) principals.getPrimaryPrincipal();
    User user = (User) principal.get("user");
    List<String> permissions = (List<String>) principal.get("permissions");

    //查到權限數據,返回受權信息(要包括上邊的permissions)
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    simpleAuthorizationInfo.addStringPermissions(permissions);//這裏添加用戶有的權限列表
    simpleAuthorizationInfo.addRole(user.getRoleId());//這裏添加用戶所擁有的角色

    return simpleAuthorizationInfo;
  }

}

ShiroLoginFilter.javaapache

public class ShiroLoginFilter extends FormAuthenticationFilter {
  private static final Logger log = LoggerFactory.getLogger(ShiroLoginFilter.class);
  /**
   * 若是isAccessAllowed返回false 則執行onAccessDenied
   *
   * @param request
   * @param response
   * @param mappedValue
   * @return
   */
  @Override
  protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    boolean isAllowed = false;
    //前端(某些框架)測試接口(OPTIONS)直接放行
    if (request instanceof HttpServletRequest) {
      if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
        isAllowed = true;
      }
    }
    isAllowed = super.isAccessAllowed(request, response, mappedValue);
    if (isAllowed) {
		//登陸狀態,做一些日誌記錄
    }
    return isAllowed;
  }

  /**
   * 未登陸時的處理
   *
   * @param request
   * @param response
   * @return true-繼續往下執行,false-該filter過濾器已經處理,不繼續執行其餘過濾器
   * @throws Exception
   */
  @Override
  protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    log.info("用戶未登陸");
    final HttpServletRequest request2 = (HttpServletRequest) request;
    final HttpServletResponse response2 = (HttpServletResponse) response;

    //ajax訪問接口返回數據結構
    if (WebUtil.isAjax(request2)) {// ajax接口
      //這裏是個坑,若是不設置的接受的訪問源,那麼前端都會報跨域錯誤,由於這裏還沒到corsConfig裏面
      response2.setHeader("Access-Control-Allow-Origin", request2.getHeader("Origin"));
      response2.setHeader("Access-Control-Allow-Credentials", "true");
      response2.setCharacterEncoding("UTF-8");
      response2.setContentType("application/json");
	  
	  Map responseData = new HashMap();
      responseData.put("state", "unauthorized");
	  responseData.put("code", 401);
	  responseData.put("msg", "用戶未登陸");
     
	  String result = Json.toJson(responseData);
	  PrintWriter out;
      try {
        out = response2.getWriter();
        out.print(result.toString());
        out.flush();
      } catch (IOException e) {
        log.error("返回數據失敗!", e);
      }
    } else {
      //其餘狀況
      //shiro處理
      super.onAccessDenied(request, response);

      //其餘處理方式

      // 頁面,直接跳轉登陸頁面
      //redirect("login.html", request2, response2);

      //web.xml處理
      //response2.setStatus(401);// 客戶試圖未經受權訪問受密碼保護的頁面。
    }
    return false;
  }
}

Login2Controller.javajson

/**
 * shiro登陸
 */
@Slf4j
@RestController
@RequestMapping("/api/sys")
public class Login2Controller{
  @Autowired
  private LoginService service;

  @Autowired
  private LoginService loginService;


  /**
   * 登錄
   *
   * @param loginId  登陸帳號
   * @param password 密碼
   */
  @RequestMapping(value = "/login")
  public RespObject login(String loginId, String password, HttpServletRequest req) {
    final String host = req.getRemoteHost();
    // 在認證提交前準備 token(令牌)
    UsernamePasswordToken token = new UsernamePasswordToken(loginId, password, host);
    try {
      // 從SecurityUtils裏邊建立一個 subject
      Subject subject = SecurityUtils.getSubject();
      // 執行認證登錄
      subject.login(token);
      //set session attribute
      final Map principal = (Map) subject.getPrincipal();
      User user = (User) principal.get("user");
	  // loginService.buildSessionAttr方法生成了包含 List<String> permissions,key爲"permissions";
      final Map sessionAttrs = loginService.buildSessionAttr(user);
      principal.putAll(sessionAttrs);
    } catch (UnknownAccountException e) {
      final String message = e.getMessage();
      log.info(String.format("%s[%s/%s]", message, loginId, password));
      throw new FailException(message);
    } catch (AuthenticationException e) {
      throw new FailException(e);
    }
    return RespObject.success(null, "登陸成功");
  }

  /**
   * 退出
   *
   * @return
   */
  @RequestMapping(value = "/logout", method = RequestMethod.GET)
  public RespObject logout() {
    Subject subject = SecurityUtils.getSubject();
    //註銷
    subject.logout();
    return RespObject.success(null, "成功註銷!");
  }


  /**
   * 當前session屬性
   *
   * @param attr
   * @return
   */
  @RequestMapping(value = "/current")
  public RespObject getCurrentAttr(String attr) {
    Map sessionAttrs = service.readSessionAttr();
    if (Strings.isEmpty(attr)) {
      return RespObject.success(sessionAttrs);
    } else {
      return RespObject.success(sessionAttrs.get(attr));
    }
  }

其餘代碼

MyControllerAdvice.java,統一處理spring MVC異常,代碼裏有些調用別地的經常使用處理方法,根據實際狀況修改。

/**
 * controller 加強器,應用到全部@RequestMapping註解方法
 */
@ControllerAdvice
public class MyControllerAdvice {
  private static final Logger log = LoggerFactory.getLogger(MyControllerAdvice.class);
  @ExceptionHandler
  @ResponseBody
  public Object errorHandler(HttpServletRequest request, Exception e, HttpServletResponse response) {
    //記錄日誌
    if (e instanceof UnauthorizedException) {
      //沒有權限
      String uri = request.getServletPath();
      final String queryString = request.getQueryString();
      if (null != queryString && queryString.trim().length() > 0) {
        uri = uri + "?" + queryString;
      }
      log.info(String.format("%s, [uri = %s]", e.getMessage(), uri));
    } else {
      if (e instanceof BaseException) {
        log.error(e.getMessage());
      } else {
        log.error("異常錯誤", e);
      }
    }
    Throwable e2 = WebUtil.deepestException(e);
    try {
      // 是否ajax調用
      boolean isAjax = true;
      if (WebUtil.isAjax(request)) {
        RespObject respObject;
        if (e instanceof UnauthorizedException) {
          respObject = RespObject.forbidden();
        } else if (e instanceof FailException) {
          respObject = RespObject.fail(RespObject.getExceptionMessage(e2));
        } else if (e instanceof ErrorException) {
          respObject = RespObject.error(e2);
        } else {
          respObject = RespObject.exception(e2);
        }
        respObject.setExtra(e2.getMessage());
        return respObject;
      } else {
        // 添加本身的異常處理邏輯,如日誌記錄
        request.setAttribute("exceptionMessage", e.getMessage());
        return "common/error";
      }
    } catch (Exception e3) {
      log.error("返回數據失敗!", e3);
    }
    return "common/error";
  }
}

UserController.java,測試接口調用

@RestController
@RequestMapping("/api/base/user")
public class UserController extends BaseController<User, String> {
  @RequiresPermissions(value = {"user"}, logical = Logical.OR)//執行此方法須要權限
  @RequestMapping(value = "/search")
  public RespObject search(PageParam pageParam, User bean) {
    return super.search(pageParam, bean);
  }
}

其餘注意

  • 有一個坑是applicationContext-shiro.xml中,shiroFilter <property name="filters"> 配置的<entry key="authc" value-ref="shiroLoginFilter" />,這裏的key="authc"不能隨便亂寫,有些文章是隨便寫的,會致使不會調shiroLoginFilter.java中的方法。

引入jar包或版本信息

spring相關的包怎麼引的隨便找,使用shiro這裏aop,確定要

<java.version>1.8</java.version>
<spring.version>5.1.7.RELEASE</spring.version>
<hibernate.version>5.4.2.Final</hibernate.version>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.4.1</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-ehcache</artifactId>
  <version>1.4.1</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-web</artifactId>
  <version>1.4.1</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.4.1</version>
</dependency>
相關文章
相關標籤/搜索