Apache Shiro 是一個強大且易用的Java安全框架,用於實現身份認證、鑑權、會話管理及加密功能。
框架提供了很是簡單且易於上手的API,能夠支持快速爲web應用程序實現安全控制能力。
官網地址
github 地址html
Apache Shiro 的設計初衷是讓安全管理變得易於上手和容易理解,它能夠實現:java
官網-Featuresgit
主要概念 包括了
Authentication(身份鑑別)、Authorization(權限管理)、Session Management(會話管理)、Cryptography(加密)
這號稱軟件安全的四大基石.. 關於幾個概念,用下面的表格說明:github
名稱 | 解釋 |
---|---|
Authentication(身份鑑別) | 指鑑別登陸用戶的身份 |
Authorization(權限認證) | 決定用戶是否有權訪問某物 |
Session Management(會話管理) | 支持獨立的會話管理 |
Cryptography(加密) | 利用加密算法保證數據安全 |
其餘特性非核心,可是很是有用web
看看下面的圖:算法
圖中涉及了若干個模塊,關於每一個模塊的大體做用以下:spring
Subject
交互實體,對應於當前用戶。數據庫
SecurityManager
安全管理器,Shiro最核心的模塊,管理各安全模塊的工做;apache
Authenticator
身份鑑別組件,執行和反饋用戶的認證(登陸),
該組件從Realm中獲取用戶信息。segmentfault
Authentication Strategy
若是配置了多個Realm,該怎麼協調?這就用到策略
Authorizer
權限認證,顧名思義,就是用於負責用戶訪問控制的模塊。
SessionManager
會話管理器,在Web環境中Shiro通常會沿用Servlet容器的會話。
但脫離了Web環境就會使用獨立的會話管理。
SessionDAO
執行會話持久化的工具
CacheManager
一個緩存管理器,可爲 Shiro 的其餘組件提供緩存能力。
Cryptography
加密組件,提供了大量簡單易用的安全加密API
到這裏,不須要爲這麼多的模塊而苦惱,在使用Shiro時,只須要緊緊記住下面的實體關係,便不會產生理解上的困難。
簡而言之
應用程序依賴於 Subject 實體來標識當前的用戶,而SecurityManager 則經過Realm接口讀取數據,進而實現 Subject 的關聯管理。
爲了幫助讀者更快速理解Shiro,下面上一段QuickStart的代碼
// 加載 shiro.ini並構造 SecurityManager Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); // 設置當前的 SecurityManager對象 SecurityUtils.setSecurityManager(securityManager); // 獲取當前用戶 Subject currentUser = SecurityUtils.getSubject(); // 操做會話 Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } // 執行登陸 if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } catch (AuthenticationException ae) { // unexpected condition? error? } } // 輸出用戶信息 log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); // 檢查角色 if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } // 檢查權限 if (currentUser.isPermitted("lightsaber:weild")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } // 結束,執行註銷 currentUser.logout(); System.exit(0);
上面這段代碼來自 shiro-sample/QuickStart.java,
關於代碼的解釋.. 老司機認爲看下注釋是必定能懂的了。
咱們嘗試將 Shiro 整合到 SpringBoot 項目,翻了下官網並無太多介紹,
猜測這可能與 SpringBoot 框架還比較新有關係,Shiro是個老框架(2010年出的第一個版本)..
但最終老司機仍是成功找到了 膠合組件:shiro-spring-boot-starter
接下來,爲項目引入依賴:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.4.0</version> </dependency>
接下來,咱們將完成一個 URL訪問安全控制 的示例,經過這個案例
讀者能夠了解到如何根據業務定製必要的功能模塊。
圖示中,名爲lilei 的用戶擁有 normal (普通用戶)的角色,而相應的具有customer.profile的讀寫權限。
以上是基於RBAC(基於角色的權限控制) 的設計,RBAC 目前的應用很是普遍
在 web應用訪問中,某些頁面是容許任何人訪問的,某些須要登陸用戶,好比我的中心
而某些頁面須要具有一些特權,好比vip資料.. 以下圖所示:
一般,在設計用戶權限時都會考慮用戶信息、角色信息以及對應的權限
用戶實體
public static class UserInfo { private String username; private String passwordHash; private String salt;
須要注意到 salt是用於密碼存儲的加鹽值(用於防止暴力破解)
passwordHash 是原始密碼通過加鹽哈希計算後的值(16進制形式)
角色實體
public static class RoleInfo { private String roleName; private List<String> perms;
爲了簡化,咱們直接將權限用字符串形式表示,一個角色RoleInfo包含了一組權限perm。
用戶管理器
在咱們的樣例中,須要實現一個UserManager類,用於作用戶信息、權限信息的管理。
public class ShiroUserManager { // 用戶表 private final Map<String, UserInfo> users = new HashMap<String, UserInfo>(); // 角色權限表 private final Map<String, List<RoleInfo>> userRoles = new HashMap<String, List<RoleInfo>>(); private static final Logger logger = LoggerFactory.getLogger(ShiroUserManager.class); // 密鑰匹配類 private ShiroHashMatcher matcher; public ShiroUserManager(ShiroHashMatcher matcher) { this.matcher = matcher; } public ShiroHashMatcher getMatcher() { return this.matcher; } @PostConstruct private void init() { // 預置信息 register("lilei", "111111", "123"); grant("normal", new RoleInfo("customer", "customer.profile.read")); grant("normal", new RoleInfo("customer", "customer.profile.write")); } /** * 獲取用戶信息 * * @param username * @return */ public UserInfo getUser(String username) { if (StringUtils.isEmpty(username)) { return null; } return users.get(username); } /** * 獲取權限信息 * * @param username * @return */ public List<RoleInfo> getRoles(String username) { if (StringUtils.isEmpty(username)) { return Collections.emptyList(); } return userRoles.get(username); } /** * 添加用戶 * * @param username * @param password * @param salt * @return */ public UserInfo register(String username, String password, String salt) { if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password) || StringUtils.isEmpty(salt)) { return null; } // 生成加鹽密碼Hash值 String passwordHash = matcher.getCredentialHash(password, salt); logger.info("user {} register with passHash :{}", username, passwordHash); UserInfo user = new UserInfo(username, passwordHash, salt); users.put(username, user); return user; } /** * 受權操做 * * @param username * @param role */ public void grant(String username, RoleInfo role) { if (userRoles.containsKey(username)) { userRoles.get(username).add(role); } else { List<RoleInfo> roleList = new ArrayList<RoleInfo>(); roleList.add(role); userRoles.put(username, roleList); } }
在上面的實現中,咱們僅僅將用戶、角色信息放在內存中管理,並內置了名爲lilei的用戶角色。
在真實應用中,用戶權限須要經過持久層(DB)實現
咱們基於Shiro的基礎類HashedCredentialsMatcher進行了擴展。
選用SHA-256哈希算法,設置迭代次數爲1024。
public class ShiroHashMatcher extends HashedCredentialsMatcher { public ShiroHashMatcher() { setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME); setHashIterations(1024); setStoredCredentialsHexEncoded(true); } public String getCredentialHash(Object credentials, Object salt) { return new SimpleHash(this.getHashAlgorithmName(), credentials, salt, this.getHashIterations()).toHex(); }
在Shiro 框架中, Realm 是用做用戶權限信息查詢的接口,咱們的實現以下:
public class ShiroRealm extends AuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(ShiroRealm.class); private ShiroUserManager userManager; public ShiroRealm(ShiroUserManager userManager) { this.setCredentialsMatcher(userManager.getMatcher()); this.userManager = userManager; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { logger.info("check authorization info"); SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(); // 獲取當前用戶 UserInfo userInfo = (UserInfo) principals.getPrimaryPrincipal(); // 查詢角色信息 List<RoleInfo> roleInfos = userManager.getRoles(userInfo.getUsername()); if (roleInfos != null) { for (RoleInfo roleInfo : roleInfos) { authInfo.addRole(roleInfo.getRoleName()); if (roleInfo.getPerms() != null) { for (String perm : roleInfo.getPerms()) { authInfo.addStringPermission(perm); } } } } return authInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { logger.info("check authentication info"); String username = (String) token.getPrincipal(); // 獲取用戶信息 UserInfo user = userManager.getUser(username); if (user == null) { return null; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPasswordHash(), ByteSource.Util.bytes(user.getSalt()), getName()); return authenticationInfo; }
將實現好的 ShiroRealm 註冊爲Bean,並初始化 WebSecurityManager
@Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm()); return securityManager; } @Bean public ShiroRealm realm() { ShiroRealm realm = new ShiroRealm(userManager()); return realm; } @Bean public ShiroUserManager userManager() { return new ShiroUserManager(matcher()); } @Bean public ShiroHashMatcher matcher() { return new ShiroHashMatcher(); }
攔截器鏈經過 ShiroFilterFactoryBean實現定製,實現以下:
@Bean public ShiroFilterFactoryBean filter(org.apache.shiro.mgt.SecurityManager securityManager) { logger.info("config shiro filter"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 定義URL攔截鏈 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 容許匿名用戶訪問首頁 filterChainDefinitionMap.put("/shiro/index", "anon"); // 定義註銷路徑 filterChainDefinitionMap.put("/shiro/logout", "logout"); // 全部用戶界面都須要身份驗證,不然會跳轉到loginurl,由FormAuthenticationFilter處理 filterChainDefinitionMap.put("/shiro/user/**", "authc"); // 爲login路徑定義攔截,由FormAuthenticationFilter處理 filterChainDefinitionMap.put("/shiro/login", "authc"); // 全部vip路徑要求具有vip角色權限 filterChainDefinitionMap.put("/shiro/vip/**", "roles[vip]"); // 指定loginurl 路徑 shiroFilterFactoryBean.setLoginUrl("/shiro/login"); // 登陸成功後跳轉路徑 shiroFilterFactoryBean.setSuccessUrl("/shiro/user/"); // for un authenticated shiroFilterFactoryBean.setUnauthorizedUrl("/shiro/unauth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 自定義filters,可覆蓋默認的Filter列表,參考 DefaultFilter Map<String, Filter> filters = new LinkedHashMap<String, Filter>(); // 定製logout 過濾,指定註銷後跳轉到登陸頁(默認爲根路徑) LogoutFilter logoutFilter = new LogoutFilter(); logoutFilter.setRedirectUrl("/shiro/login"); filters.put("logout", logoutFilter); // 定製authc 過濾,指定登陸表單參數 FormAuthenticationFilter authFilter = new FormAuthenticationFilter(); authFilter.setUsernameParam("username"); authFilter.setPasswordParam("password"); filters.put("authc", authFilter); shiroFilterFactoryBean.setFilters(filters); return shiroFilterFactoryBean; }
跟着老司機的註釋,上面代碼應該不難理解(儘管有點冗長),filterChainDefinitionMap的定義中,
key對應於url路徑,而value則對應了過濾器的縮寫,Shiro內置的過濾器可參考DefaultFilter枚舉
配置 | 過濾器 | 功能 |
---|---|---|
anon | AnonymousFilter | 可匿名訪問 |
authc | FormAuthenticationFilter | form表單登陸攔截 |
authcBasic | BasicHttpAuthenticationFilter | basic登陸攔截 |
logout | LogoutFilter | 註銷處理 |
noSessionCreation | NoSessionCreationFilter | 禁止建立會話 |
perms | PermissionsAuthorizationFilter | 指定權限 |
port | PortFilter | 指定端口 |
rest | HttpMethodPermissionFilter | HttpMethod轉換 |
roles | RolesAuthorizationFilter | 指定角色 |
ssl | SslFilter | 須要https |
user | UserFilter | 已登陸或Rememberme |
深刻一點
FormAuthenticationFilter 實現了表單登陸的攔截邏輯:
扒一扒源碼,能夠看到相應的邏輯實現:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); } return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } //allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Authentication url [" + getLoginUrl() + "]"); } saveRequestAndRedirectToLogin(request, response); return false; } }
isLoginSubmission 方法的判斷中,認爲來自 loginUrl 的 POST 請求就是登陸操做。
protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) { return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD); }
在登陸失敗後,寫入上下文信息,這裏使用的是異常類的名稱
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { if (log.isDebugEnabled()) { log.debug( "Authentication exception", e ); } setFailureAttribute(request, e); //login failed, let request continue back to the login page: return true; } protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) { String className = ae.getClass().getName(); request.setAttribute(getFailureKeyAttribute(), className); }
看到這裏,你應該能理解爲何在過濾鏈定義中,loginUrl 也須要被攔截了。
filterChainDefinitionMap.put("/shiro/login", "authc");
基於上面的分析後,咱們即可以輕鬆的完成Controller的編寫,以下:
@Controller @RequestMapping("/shiro") public class ShiroController { /** * 登陸界面,展現登陸表單 * * @return */ @GetMapping("/login") public String login() { return "shiro/login"; } /** * 登陸表單處理 * * @return */ @PostMapping("/login") public String doLogin(HttpServletRequest servletRequest, final RedirectAttributes redirectAttrs) { // FormAuthenticationFilter已經作了登陸校驗處理, // 若登陸成功會跳轉到loginSuccessUrl,這裏只作異常處理 String errorException = (String) servletRequest .getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME); // 登陸失敗,errorException 非空 if (!StringUtils.isEmpty(errorException)) { // 設置錯誤消息,執行跳轉 redirectAttrs.addFlashAttribute("loginErrorMsg", "LoginFailed:" + errorException); return "redirect:/shiro/login"; } return "OK"; } /** * 用戶信息界面 * * @return */ @GetMapping("/user") @ResponseBody public String user() { Subject subject = SecurityUtils.getSubject(); UserInfo user = (UserInfo) subject.getPrincipals().getPrimaryPrincipal(); return "Welcome back, " + user.getUsername(); } /** * VIP 用戶信息界面 * * @return */ @GetMapping("/vip") @ResponseBody public String userVip() { Subject subject = SecurityUtils.getSubject(); UserInfo user = (UserInfo) subject.getPrincipals().getPrimaryPrincipal(); return "Hi, " + user.getUsername() + ", This is for the vip"; } /** * 匿名訪問界面 * * @return */ @GetMapping("/annon/*") @ResponseBody public String annon() { return "this is the content anyone can access"; } /** * 無權限界面 * * @return */ @GetMapping("/unauth") @ResponseBody public String unauth() { return "you are no allow to access"; }
登陸頁面
登陸頁面爲一個簡單的HTML界面,包含一個POST表單,使用username/password做爲請求參數。
在登陸失敗時由Controller跳轉回登陸頁,並顯示出錯信息,效果以下:
前面的例子演示了 Shiro的經典用法,然而,老司機認爲註解會更好用。
Shiro 的註解是基於AOP實現的,在方法上聲明所須要的權限,相比URL攔截要更加靈活。
shiro-spring-boot-starter 爲咱們自動注入了AOP 代理配置,可直接使用註解。
若是使用了註解,咱們能夠對url 啓用匿名訪問,這樣訪問控制則經過註解和異常處理來實現。
// 對於全部shiroan路徑一概不攔截 filterChainDefinitionMap.put("/shiroan/**", "anon");
/** * vip 界面,須要vip角色 * * @return */ @RequiresRoles("vip") @GetMapping("/vip") @ResponseBody public String vip() { return "this is the vip info"; } /** * home 界面,須要登陸 * * @return */ @RequiresAuthentication @GetMapping("/home") @ResponseBody public String home() { return "this is the home page"; } /** * 資料界面,須要資料權限 * * @return */ @RequiresPermissions("customer.profile.read") @GetMapping("/profile") @ResponseBody public String profile() { return "this is the profile info"; } /** * 讀取相冊界面,須要詳情權限 * * @return */ @RequiresPermissions("customer.album.read") @GetMapping("/album") @ResponseBody public String album() { return "this is the album info"; }
@RequiredRoles、@RequiredPermissions、@RequiredAuthentication 定義了方法執行所需的權限。
除此以外,Shiro還內置了其餘註解,以下:
名稱 | 功能 |
---|---|
@RequiresRoles | 指定的角色能夠訪問 |
@RequiresPermissions | 指定的權限能夠訪問 |
@RequiresAuthentication | 登陸用戶能夠訪問 |
@RequiresGuest | 僅遊客能夠訪問 |
@RequiresUser | 已登陸或 "記住我"的用戶 |
在訪問方法未經過權限檢查時,會拋出AuthorizationException,咱們須要定義一個攔截器進行處理
/** * 自定義攔截,處理鑑權異常 * * @author atp * */ @ControllerAdvice(assignableTypes = ShiroAnnotateController.class) public static class AuthExceptionHandler { @ExceptionHandler(value = { AuthorizationException.class }) public ResponseEntity<String> handle(AuthorizationException e, HandlerMethod m) { logger.info("Authorization Failed {} -- {}", e.getClass(), e.getMessage()); String msg = "not allow to access"; if (e instanceof UnauthorizedException) { // 沒有權限 msg = "you have no permissions"; } else if (e instanceof UnauthenticatedException) { // 未登陸 msg = "you must login first"; } return ResponseEntity.status(HttpStatus.FORBIDDEN).body(msg); } }
一樣,因爲沒有了過濾鏈,咱們須要自行實現 login 邏輯,代碼很是簡單:
/** * 模擬登陸接口 * * @param username * @param password * @return */ @RequestMapping("/login") @ResponseBody public String login(@RequestParam("username") String username, @RequestParam("password") String password) { Subject subject = SecurityUtils.getSubject(); AuthenticationToken token = new UsernamePasswordToken(username, password.toCharArray()); try { // 執行登陸 subject.login(token); } catch (UnknownAccountException e) { // 未知用戶 logger.warn("the account {} is not found", username); return "account not found"; } catch (IncorrectCredentialsException e) { // 用戶或密碼不正確 logger.warn("the account or password is not correct"); return "account or password not correct"; } return "login success"; }
一些常見的登陸異常以下表,可按業務須要使用:
異常 | 描述 |
---|---|
UnknownAccountException | 找不到用戶 |
IncorrectCredentialsException | 用戶名密碼不正確 |
LockedAccountException | 用戶被鎖定 |
ExcessiveAttemptsException | 密碼重試超過次數 |
ExpiredCredentialsException | 密鑰已通過期 |
登出的代碼:
@RequestMapping("/logout") @ResponseBody public String logout() { Subject subject = SecurityUtils.getSubject(); // 執行註銷 if (subject.isAuthenticated()) { subject.logout(); } return "OK"; }
深刻一點
shiro-spring-boot-starter 爲咱們實現了大量的自動裝配功能,如如下代碼片斷:
@SuppressWarnings("SpringFacetCodeInspection") @Configuration @ConditionalOnProperty(name = "shiro.annotations.enabled", matchIfMissing = true) public class ShiroAnnotationProcessorAutoConfiguration extends AbstractShiroAnnotationProcessorConfiguration { @Bean @DependsOn("lifecycleBeanPostProcessor") @ConditionalOnMissingBean @Override public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { return super.defaultAdvisorAutoProxyCreator(); } @Bean @ConditionalOnMissingBean @Override public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { return super.authorizationAttributeSourceAdvisor(securityManager); } }
其中,DefaultAdvisorAutoProxyCreator 是AOP實現的關鍵類,有興趣能夠繼續深刻了解
Shiro 的功能很是靈活,本文中的樣例僅供參考,若是要在生產環境中使用,你須要思考更多方面的東西:
Shiro-integrating-with-spring
Shiro-integrating-with-springboot
Shiro-1.2.x-refence-waylau
Shirot-SprintBoot優雅整合
Apache Shiro 是一個強大易用的安全框架,其自己也提供了很是多的特性模塊。
本文旨在介紹如何將Shiro與當前流行的SpringBoot 框架結合使用,並提供了極簡單的案例。
筆者在問題求證過程當中經過閱讀部分源碼,更深刻理解了其框架原理。目前認爲,Shiro強大之處
還在於框架保持了簡單易用、靈活擴展的特色,相信這也是許多人青睞它的緣由吧。
最後,歡迎繼續關注"美碼師的補習系列-springboot篇" ,若是以爲老司機的文章還不賴,請多多分享轉發^-^