本系列博文目錄:http://www.javashuo.com/article/p-ewndobct-kn.htmljavascript
本文所提供的項目實例,是我將公司項目中的shiro代碼進行了抽取、整理並添加了一些註釋而造成的。html
因此例子中並不包含shiro全部的功能,可是本系列文章前9篇所講解的內容在這裏都是能夠找到的。java
本示例項目所使用的技術以下:linux
集成開發環境爲IDEA,項目構建使用spring boot,包管理使用maven,頁面展現使用freemaker,控制層使用spring mvc等。git
在本篇博文中會貼出主要代碼,完整的項目已經上傳到碼雲你們能夠下載查看使用。web
項目碼雲地址:http://git.oschina.net/imlichao/shiro-exampleajax
package pub.lichao.shiro.config; import com.jagregory.shiro.freemarker.ShiroTags; import org.springframework.boot.autoconfigure.freemarker.FreeMarkerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import java.util.HashMap; import java.util.Map; /** * FreeMarker配置文件 */ @Configuration public class FreemarkerConfig { @Bean public FreeMarkerConfigurer freeMarkerConfigurer(FreeMarkerProperties freeMarkerProperties) { FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); configurer.setTemplateLoaderPaths(freeMarkerProperties.getTemplateLoaderPath()); //模板加載路徑默認 "classpath:/templates/" configurer.setDefaultEncoding("utf-8");//設置頁面默認編碼(不設置頁面中文亂碼) Map<String,Object> variables=new HashMap<String,Object>(); variables.put("shiro", new ShiroTags()); configurer.setFreemarkerVariables(variables);//添加shiro自定義標籤 return configurer; } }
package pub.lichao.shiro.config; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.codec.Base64; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.springframework.boot.context.embedded.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.DelegatingFilterProxy; import pub.lichao.shiro.shiro.AuthenticationFilter; import pub.lichao.shiro.shiro.ShiroRealm; import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * Shiro配置 */ @Configuration public class ShiroConfig { /** * 建立EhCache緩存類 * @return */ @Bean(name = "shiroCacheManager") public EhCacheManager shiroCacheManager() { EhCacheManager ehCacheManager = new EhCacheManager(); ehCacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml");//指定緩存配置文件路徑 return ehCacheManager; } /** * 建立安全認證資源類 * (本身實現的登錄和受權認證規則) */ @Bean(name = "shiroRealm") public ShiroRealm shiroRealm(EhCacheManager shiroCacheManager) { ShiroRealm realm = new ShiroRealm(); realm.setCacheManager(shiroCacheManager); //爲資源類配置緩存 return realm; } /** * 建立保存記住我信息的Cookie */ @Bean(name = "rememberMeCookie") public SimpleCookie getSimpleCookie() { SimpleCookie simpleCookie = new SimpleCookie(); simpleCookie.setName("rememberMe");//cookie名字 simpleCookie.setHttpOnly(true); //設置cookieHttpOnly,保證cookie安全 simpleCookie.setMaxAge(604800); //保存7天 單位秒 return simpleCookie; } /** * 建立記住我管理器 */ @Bean(name = "rememberMeManager") public CookieRememberMeManager getCookieRememberMeManager(SimpleCookie rememberMeCookie) { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); byte[] cipherKey = Base64.decode("wGiHplamyXlVB11UXWol8g==");//建立cookie祕鑰 cookieRememberMeManager.setCipherKey(cipherKey); //存入cookie祕鑰 cookieRememberMeManager.setCookie(rememberMeCookie); //存入記住我Cookie return cookieRememberMeManager; } /** * 建立默認的安全管理類 * 整個安全認證流程的管理都由此類負責 */ @Bean(name = "securityManager") public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm,EhCacheManager shiroCacheManager,CookieRememberMeManager rememberMeManager) { DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); //建立安全管理類 defaultWebSecurityManager.setRealm(shiroRealm); //指定資源類 defaultWebSecurityManager.setCacheManager(shiroCacheManager);//爲管理類配置Session緩存 defaultWebSecurityManager.setRememberMeManager(rememberMeManager);//配置記住我cookie管理類 return defaultWebSecurityManager; } /** * 得到攔截器工廠類 */ @Bean (name = "authenticationFilter") public AuthenticationFilter authenticationFilter() { return new AuthenticationFilter(); } @Bean(name = "shiroFilter") public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager,AuthenticationFilter authenticationFilter) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager);//設置SecurityManager,必輸 shiroFilterFactoryBean.setLoginUrl("/login");//配置登陸路徑(登陸頁的路徑和表單提交的路徑必須是同一個,頁面的GET方式,表單的POST方式) shiroFilterFactoryBean.setSuccessUrl("/home");//配置登陸成功頁路徑 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");//配置沒有權限跳轉的頁面 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/", "anon"); //無需登陸認證和受權就可訪問的路徑使用anon攔截器 filterChainDefinitionMap.put("/home/**", "user");//須要登陸認證的路徑使用authc或user攔截器 filterChainDefinitionMap.put("/user/**", "user,perms[user-jurisdiction]");//須要權限受權的路徑使用perms攔截器 filterChainDefinitionMap.put("/admin/**", "user,perms[admin-jurisdiction]");//authc和perms攔截器可同時使用 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);//設置攔截規則 Map<String, Filter> map = new HashMap<String, Filter>(); map.put("authc", authenticationFilter);//自定義攔截器覆蓋了FormAuthenticationFilter登陸攔截器所用的攔截器名authc shiroFilterFactoryBean.setFilters(map);//添加自定義攔截器 return shiroFilterFactoryBean; } /** * 註冊shiro攔截器 */ @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistration = new FilterRegistrationBean(); filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter")); //建立代理攔截器,並指定代理shiro攔截器 filterRegistration.addInitParameter("targetFilterLifecycle", "true");//設置攔截器生命週期管理規則。false(默認)由SpringApplicationContext管理,true由ServletContainer管理。 filterRegistration.setEnabled(true);// 激活註冊攔截器 filterRegistration.addUrlPatterns("/*");//添加攔截路徑 return filterRegistration; } }
package pub.lichao.shiro.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletRequest; /** * Controller - 主頁 */ @Controller public class HomeController { /** * 進入主頁 * @param request * @param redirectAttributes * @return */ @RequestMapping(value = "/home", method = RequestMethod.GET) public String home(HttpServletRequest request, RedirectAttributes redirectAttributes) { return "home"; } /** * 進入無權限提示頁 * @param request * @param redirectAttributes * @return */ @RequestMapping(value = "/unauthorized", method = RequestMethod.GET) public String unauthorized(HttpServletRequest request, RedirectAttributes redirectAttributes) { return "unauthorized"; } /** * 進入admin頁(用戶無此權限) */ @RequestMapping(value = "/admin", method = RequestMethod.GET) public String admin(HttpServletRequest request, RedirectAttributes redirectAttributes) { return "admin"; } }
package pub.lichao.shiro.controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletRequest; /** * Controller - 登錄 */ @Controller public class LoginController { /** * 根路徑重定向到登陸頁 * @return */ @RequestMapping(value = "/", method = RequestMethod.GET) public String loginForm() { return "redirect:login"; } /** * 進入登陸頁面 * @return */ @RequestMapping(value = "/login", method = RequestMethod.GET) public String loginInput(@ModelAttribute("message") String message) { if (message != null && !message.equals("")){ //此處演示一下重定向到此方法時經過addFlashAttribute添加的參數怎麼獲取 System.out.println("addFlashAttribute添加的參數 :"+ message); } //判斷是否已經登陸 或 是否已經記住我 if (SecurityUtils.getSubject().isAuthenticated() || SecurityUtils.getSubject().isRemembered()) { return "redirect:/home"; } else { return "login"; } } /** * 登陸表單提交 * @param request * @param redirectAttributes * @return */ @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(HttpServletRequest request, RedirectAttributes redirectAttributes) { //若是認證未經過得到異常並重定向到登陸頁 String message = null; String loginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);//取得登錄失敗異常 if (loginFailure.equals("pub.lichao.shiro.shiro.CaptchaAuthenticationException")) { message = "驗證碼錯誤";//自定義登錄認證異常 - 用於驗證碼錯誤提示 } else if (loginFailure.equals("org.apache.shiro.authc.UnknownAccountException")) { message = "用戶不存在";//未找到帳戶異常 }else if (loginFailure.equals("org.apache.shiro.authc.IncorrectCredentialsException")) { message = "密碼錯誤";//憑證(密碼)錯誤異常 } else if (loginFailure.equals("org.apache.shiro.authc.AuthenticationException")) { message = "帳號認證失敗";//認證異常 }else{ message = "未知認證錯誤";//未知認證錯誤 } //重定向參數傳遞,可以將參數傳遞到最終頁面 // (用addAttribute的時候參數會寫在url中因此要用addFlashAttribute) redirectAttributes.addFlashAttribute("message", message); return "redirect:login"; } /** * 退出登陸 * @param redirectAttributes * @return */ @RequestMapping(value = "/logout", method = RequestMethod.GET) public String logout(RedirectAttributes redirectAttributes) { //調用shiro管理工具類的退出登陸方法 SecurityUtils.getSubject().logout(); redirectAttributes.addFlashAttribute("message", "您已安全退出"); return "redirect:login"; //退出後返回到登陸頁 } }
package pub.lichao.shiro.shiro; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.util.WebUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Filter - 自定義登錄攔截器 * 繼承並重寫默認的登陸攔截器 */ public class AuthenticationFilter extends FormAuthenticationFilter { /** * 建立Token */ @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { String username = getUsername(request);//獲取用戶名 表單name:username String password = getPassword(request);//獲取密碼 表單name:password boolean rememberMe = isRememberMe(request);//獲取是否記住我 表單name:rememberMe String captchaId = WebUtils.getCleanParam(request, "captchaId");//獲取驗證碼id String captcha = WebUtils.getCleanParam(request, "captcha");//獲取用戶輸入的驗證碼字符 return new CaptchaAuthenticationToken(username, password,captchaId, captcha, rememberMe);//存入本身定義的包含驗證碼的Token } /** * 登陸驗證成功以後 */ @Override protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { return super.onLoginSuccess(token, subject, request, response); } /** * 當訪問被拒絕 */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { //訪問被拒絕時默認行爲是返回登陸頁,可是當使用ajax進行登陸時要返回403錯誤 HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String requestType = request.getHeader("X-Requested-With"); //獲取http頭參數X-Requested-With if (requestType != null && requestType.equalsIgnoreCase("XMLHttpRequest")) { //頭參數X-Requested-With存在而且值爲XMLHttpRequest說明是ajax請求 response.sendError(HttpServletResponse.SC_FORBIDDEN); //返回403錯誤 - 執行訪問被禁止 return false; }else{ return super.onAccessDenied(servletRequest, servletResponse); } } }
package pub.lichao.shiro.shiro; import org.apache.shiro.authc.AuthenticationException; /** * 自定義登錄認證異常 - 用於驗證碼錯誤提示 */ public class CaptchaAuthenticationException extends AuthenticationException { }
package pub.lichao.shiro.shiro; import org.apache.shiro.authc.UsernamePasswordToken; /** * Token - 自定義登陸令牌 * 繼承並重寫默認的登陸令牌 */ public class CaptchaAuthenticationToken extends UsernamePasswordToken { /** * 自定義構造方法 */ public CaptchaAuthenticationToken(String username, String password, String captchaId, String captcha, boolean rememberMe) { super(username, password, rememberMe); this.captcha=captcha; this.captchaId=captchaId; } /** * 自定義參數 */ private String captchaId; //驗證碼id private String captcha; //錄入的驗證碼字符 public String getCaptchaId() { return captchaId; } public void setCaptchaId(String captchaId) { this.captchaId = captchaId; } public String getCaptcha() { return captcha; } public void setCaptcha(String captcha) { this.captcha = captcha; } }
package pub.lichao.shiro.shiro; /** * 身份信息 */ public class Principal implements java.io.Serializable{ /** 用戶ID */ private Long userId; /** 用戶名 */ private String username; public Principal(Long userId, String username) { this.userId = userId; this.username = username; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
登陸認證和受權邏輯實現都在這裏面算法
/** * @(#)ShiroRealm.java * Description: * Version : 1.0 * Copyright: Copyright (c) 苗方清顏 版權全部 */ package pub.lichao.shiro.shiro; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.Sha256Hash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import java.util.ArrayList; import java.util.List; /** * 安全認證資源類 */ public class ShiroRealm extends AuthorizingRealm { /** * 登陸認證(身份驗證) */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { CaptchaAuthenticationToken authenticationToken = (CaptchaAuthenticationToken) token; //得到登陸令牌 String username = authenticationToken.getUsername(); String password = new String(authenticationToken.getPassword());//將char數組轉換成String類型 String captchaId = authenticationToken.getCaptchaId(); String captcha = authenticationToken.getCaptcha(); // 驗證用戶名密碼和驗證碼是否正確 usernamePasswordAndCaptchaAuthentication(username,password,captchaId,captcha); //建立身份信息類(自定義的) Principal principal = new Principal(1L, username); //認證經過返回認證信息類 return new SimpleAuthenticationInfo(principal, password, getName()); } /** * 受權 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //獲取當前登陸用戶的信息(登陸認證時取得的) Principal principal = (Principal) principals.getPrimaryPrincipal(); //使用登陸信息中存入的userId獲取當前用戶全部權限 List<String> authorities = getAuthority(principal.getUserId()); //建立受權信息類 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //將權限存入受權信息類 authorizationInfo.addStringPermissions(authorities); return authorizationInfo; } /** * 驗證用戶名密碼和驗證碼是否正確 * @param username * @param password * @param captchaId * @param captcha * @return */ private void usernamePasswordAndCaptchaAuthentication(String username,String password,String captchaId,String captcha){ //驗證驗證碼是否正確 if(!captchaId.equals("1") || !captcha.equals("yyyy")){ throw new CaptchaAuthenticationException(); //驗證碼錯誤時拋出自定義的 驗證碼錯誤異常 } //驗證用戶名是否存在 if(!username.equals("admin")){ throw new UnknownAccountException(); //用戶並不存在異常 } //密碼加密(SHA256算法) String salt = "c1bac4173f3df3bf0241432a45ac3922";//密言通常由系統爲每一個用戶隨機生成 String sha256 = new Sha256Hash(password, salt).toString(); //使用sha256進行加密密碼 //驗證密碼是否正確 if(!sha256.equals("aa07342954e1ca7170257e74515139cc27710ff703e6fee784d0a4ea1e09f9da")){ throw new IncorrectCredentialsException();//密碼錯誤異常 } } /** * 獲取用戶權限 * @param userId * @return */ private List<String> getAuthority(Long userId){ List<String> authority = new ArrayList<String>(); if(userId.equals(1L)){ authority.add("user-jurisdiction"); // authority.add("admin-jurisdiction"); } return authority; } }
<?xml version="1.0" encoding="UTF-8"?> <ehcache name="shiro-ehcache"> <!-- 緩存文件存放目錄 --> <!-- java.io.tmpdir表明操做系統默認的臨時文件目錄,不一樣操做系統路徑不一樣 --> <!-- windows 7 C:\Users\Administrator\AppData\Local\Temp --> <!-- linux /tmp --> <diskStore path="${java.io.tmpdir}/shiro/ehcache"/> <!-- 設置緩存規則--> <!-- maxElementsInMemory:緩存文件在內存上最大數目 maxElementsOnDisk:緩存文件在磁盤上的最大數目 eternal:緩存是否永不過時。true永不過時,false會過時 timeToIdleSeconds :緩存最大空閒時間,空閒超過此時間則過時(單位:秒)。當eternal爲false時有效 timeToLiveSeconds :緩存最大的生存時間,從建立開始超過這個時間則過時(單位:秒)。當eternal爲false時有效 overflowToDisk:若是內存中數據超過內存限制,是否緩存到磁盤上 diskPersistent:是否在磁盤上持久化緩存,系統重啓後緩存依然存在 --> <defaultCache maxElementsInMemory="1000" maxElementsOnDisk="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true" diskPersistent="false"/> </ehcache>
loginspring
<!DOCTYPE html> <html lang="en" class="no-js"> <head> <title>管理平臺</title> </head> <body style="text-align:center"> <div style="margin:0 auto;"> <form action="/login" method="post"> <h3>管理平臺登陸</h3> <label>用戶名:</label> <input type="text" placeholder="請輸入用戶名" name="username" id="username" value="admin"/><br><br> <label>密 碼:</label> <input type="password" placeholder="請輸入密碼" name="password" id="password" value="111111"/><br><br> <label>驗證碼:</label> <input type="text" placeholder="請輸入驗證碼" name="captcha" id="captcha" value="yyyy"/><br><br> <input type="hidden" name="captchaId" id="captchaId" value="1"/> <label>保持登陸:</label> <input type="checkbox" name="rememberMe" id="rememberMe" />(保持7天)<br><br> <button type="submit">登陸</button><br><br> <#if message??><span style="color: red" >${message}</span></#if> </form> </div> </body> </html>
home apache
<!DOCTYPE html> <html lang="en" class="no-js"> <head> <title>管理平臺</title> </head> <body style="text-align:center"> <h1>歡迎登陸管理平臺!</h1> <br><br> <a href="logout">退出登陸</a> <br><br> <@shiro.hasPermission name = "user-jurisdiction"> 用戶擁有user-jurisdiction權限才能看見此內容! </@shiro.hasPermission> <br><br> <button onclick="javascript:window.location.href='admin'"> 進入到admin頁(須要有權限) </button> </body> </html>
admin
<!DOCTYPE html> <html lang="en" class="no-js"> <head> <title>管理平臺</title> </head> <body style="text-align:center"> <h1>用戶須要有admin-jurisdiction權限才能看到此頁內容</h1><br><br> <a href="javascript:history.go(-1)">返回</a> </body> </html>
unauthorized
<!DOCTYPE html> <html lang="en" class="no-js"> <head> <title>管理平臺</title> </head> <body style="text-align:center"> 你沒有訪問此功能的權限! <a href="javascript:history.go(-1)">返回</a> </body> </html>