一、背景
其實很早的時候,就在專案中有使用到shiro做登陸認證,直到今天才又想起來這茬,自己抽空搭了一個spring+springmvc+mybatis和shiro進行整合的種子專案,當然裡面還有很簡單的測試。本文將講述在maven下如何進行整合,希望對你有所幫助,喜歡請推薦。至於shiro相關的,最近也會寫幾篇介紹的,希望能夠有一個主觀的瞭解。
二、整合步驟
說明:關於spring+springmvc+mybatis的整合請移步另一篇部落格:Spring+SpringMvc+Mybatis框架整合搭建教程
1.第一步引入shiro依賴
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.3</version> </dependency>
2.在web.xml中引入shiro的filter
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
3.resources檔案下的spring目錄下新建spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?> <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.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <description>Shiro Configuration</description> <!--custom myself realm--> <bean id="customRealm" class="com.hafiz.www.shiro.CustomRealm"/> <!--Shiro`s main business-tier object for web-enable applications--> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="customRealm"/> </bean> <!--shiro filter--> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.html"/> <property name="successUrl" value="/index.html"/> <property name="unauthorizedUrl" value="/unauthorized.html"/> <property name="filters"> <util:map> <entry key="auth"> <bean class="com.hafiz.www.filter.AuthorizeFilter"/> </entry> </util:map> </property> <property name="filterChainDefinitions"> <value> /login.json = anon /logout.json = anon /js/** = anon / = authc /** = auth </value> </property> </bean> </beans>
4.新建自定義的Realm,CustomRealm.java
package com.hafiz.www.shiro; import com.hafiz.www.po.UserEntity; import com.hafiz.www.service.UserService; import org.apache.commons.collections4.CollectionUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; 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.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Desc:自定義Realm * Created by hafiz.zhang on 2017/7/21. */ public class CustomRealm extends AuthorizingRealm{ private static final String _WILDCARD = "*"; private static final String _PATTERN_APPEND = "+.*"; @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 如果專案不需要授權,則該方法直接 return null; UserEntity operator = (UserEntity) principalCollection.getPrimaryPrincipal(); //獲取該使用者具有的所有的角色資源(把null換成findResourceUrlById()) List<String> resourceList = null; SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<String> allPermissions = new HashSet<>(resourceList); allPermissions.remove(""); allPermissions.remove(null); List<String> patternPermissions = new ArrayList<>(); //通配url,以*,或者.* if (CollectionUtils.isNotEmpty(allPermissions)) { for (String per : allPermissions) { if (per.endsWith(_WILDCARD)) { patternPermissions.add(per); } } } if (CollectionUtils.isNotEmpty(patternPermissions)) { allPermissions.removeAll(patternPermissions); for (String pat : patternPermissions) { if(pat.endsWith(_WILDCARD)){ info.addObjectPermission(new CustomRegexPermission(pat.substring(0,pat.length()-1)+_PATTERN_APPEND)); }else{ info.addObjectPermission(new CustomRegexPermission(pat+_PATTERN_APPEND)); } } } info.addStringPermissions(allPermissions); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); List<UserEntity> users = userService.getByUserName(username); if (CollectionUtils.isEmpty(users)) { throw new UnknownAccountException(); } if (users.size() != 1) { throw new LockedAccountException("使用者名稱重複,請聯絡技術"); } UserEntity user = users.get(0); username = user.getUserName(); String password = user.getPassword(); // 第一個引數也可以放user物件,這樣在doGetAuthorizationInfo()方法中可以直接使用 return new SimpleAuthenticationInfo(username, password, getName()); } }
說明:doGetAuthorizationInfo()是做授權,比如專案中有很多資源,指定角色的人員只有指定的資源,這種情況可以使用這個方法來做授權,doGetAuthenticationInfo()方法做認證,我們一般是用作使用者登陸主邏輯,這個方法中我們只需要根據使用者提供的使用者名稱去資料庫中查詢對應的使用者資訊,然後用該資訊返回一個SimpleAuthenticationInfo物件即可,不需要比較資料庫中的密碼和token中的密碼是否一直,因為在登陸時shiro會幫我們做這件事,不匹配會丟擲IncorrectCredentialsException來提示密碼錯誤。
5.自定義AuthroizeFilter.java
package com.hafiz.www.filter; import com.alibaba.fastjson.JSON; import com.hafiz.www.consts.AppConst; import com.hafiz.www.vo.JsonResult; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authz.AuthorizationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.Writer; /** * Desc:認證驗證過濾器 * Created by hafiz.zhang on 2017/7/21. */ public class AuthorizeFilter extends AuthorizationFilter { @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; Subject subject = getSubject(request, response); if (!subject.isAuthenticated()) { String requestURI = request.getRequestURI(); if (requestURI.endsWith(".json")) { JsonResult jr = new JsonResult(); jr.setCode(AppConst.UNAUTHORIZED); jr.setMsg("登陸超時,請重新登入"); response.setStatus(AppConst.UNAUTHORIZED); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); Writer w = response.getWriter(); w.write(JSON.toJSONString(jr)); w.flush(); w.close(); } else { response.sendRedirect(request.getContextPath() + "/login.html"); } return false; } Boolean isAjax = isAjax(request); if (subject.getPrincipal() != null && isAjax) { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); response.setStatus(AppConst.UNAUTHORIZED); Writer w = response.getWriter(); JsonResult jr = new JsonResult(); jr.setCode(AppConst.UNAUTHORIZED); jr.setMsg("無許可權操作!"); w.write(JSON.toJSONString(jr)); w.flush(); w.close(); return false; } return super.onAccessDenied(servletRequest, servletResponse); } /** * 根據請求頭判斷是不是ajax請求 * * @param request 請求實體 * * @return */ private Boolean isAjax(HttpServletRequest request) { Boolean isAjax = request.getHeader("X-Requested-With") != null && "XMLHttpRequest".equals( request.getHeader("X-Requested-With").toString()); return isAjax; } /** * 判斷使用者是否可以訪問請求的資源 * * @param request 使用者請求 * * @param response 響應 * * @param mappedValue * * @return * * @throws Exception */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { // 登陸請求直接放行 if (isLoginRequest(request, response)) { return true; } // 獲取使用者認證資訊 Subject subject = getSubject(request, response); if (!subject.isAuthenticated()) { HttpServletRequest httpServletRequest = (HttpServletRequest)request; HttpSession session = httpServletRequest.getSession(); String requestUrl = httpServletRequest.getRequestURL().toString(); session.setAttribute(AppConst.LAST_URL_KEY, requestUrl); return false; } // 判斷請求資源是否授權(如果專案不需要授權,下面省略,直接return true,否則加上下面註釋掉的程式碼,然後最後return false;) /*String resource = getPathWithinApplication(request); if (subject.isPermitted(resource)) { return true; }*/ return true; } }
6.自定義SessionUtils.java來管理shiro相關的session等
package com.hafiz.www.shiro; import com.hafiz.www.po.UserEntity; import com.hafiz.www.vo.JsonResult; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Desc: 使用者Session工具類 * Created by hafiz.zhang on 2017/7/22. */ public class SessionUtils { private static final Logger LOGGER = LoggerFactory.getLogger(SessionUtils.class); /** * 獲取登陸的key,即使用者名稱 * * @return */ public static String getLoginKey() { Subject subject = SecurityUtils.getSubject(); if (subject.isAuthenticated()) { return (String)subject.getPrincipal(); } return null; } /** * 獲取當前登陸使用者 * * @return */ public static UserEntity getLoginUser() { Subject subject = SecurityUtils.getSubject(); if (subject.isAuthenticated()) { Session session = subject.getSession(); Object loginUser = session.getAttribute("loginUser"); return loginUser == null ? null : (UserEntity)loginUser; } return null; } /** * 獲取當前登陸使用者id * * @return */ public static Long getLoginUserId() { UserEntity user = getLoginUser(); if (user != null) { return user.getId(); } return null; } /** * 獲取當前使用者是否登陸 * * @return */ public static Boolean isLoggedIn() { boolean isLoggedIn = SecurityUtils.getSubject().isAuthenticated(); return isLoggedIn; } public static JsonResult login(String userName, String password) { Subject subject = SecurityUtils.getSubject(); AuthenticationToken token = new UsernamePasswordToken(userName, password); JsonResult jr = new JsonResult(); try { subject.login(token); } catch (UnknownAccountException ue) { LOGGER.error("使用者不存在:{}", userName); jr.setSuccess(false); jr.setMsg("沒有該賬號"); } catch (LockedAccountException le) { LOGGER.error("使用者名稱重複"); jr.setSuccess(false); jr.setMsg("使用者名稱重複,請聯絡技術"); } catch (IncorrectCredentialsException ie) { LOGGER.error("密碼錯誤"); jr.setSuccess(false); jr.setMsg("密碼錯誤"); } catch (Exception e) { LOGGER.error("登陸出錯:{}", e.getLocalizedMessage()); jr.setSuccess(false); jr.setMsg("登陸出錯:" + e.getLocalizedMessage()); } return jr; } /** * 使用者退出登陸 */ public static void logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); } }
經過上面這些步驟,我們就完成了spring和shiro的整合,關於簡單的測試程式,不再貼出,在這裡提供該種子專案的github地址:https://github.com/hafizzhang/spring-shiro.git
三、總結
通過本文,我們就完成了spring整合shiro做登陸的授權和認證,其實很簡單,繼續努力成長!