看過那麼多框架、教程,大部分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
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); } }
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>