項目版本:
springboot2.x
shiro:1.3.2
Maven配置:
java
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency>
寫在前面的話:
springboot中集成shiro相對簡單,只須要兩個類:一個是shiroConfig類,一個是CustonRealm類。web
ShiroConfig類:
顧名思義就是對shiro的一些配置,相對於以前的xml配置。包括:過濾的文件和權限,密碼加密的算法,其用註解等相關功能。算法
CustomRealm類:
自定義的CustomRealm繼承AuthorizingRealm。而且重寫父類中的doGetAuthorizationInfo(權限相關)、doGetAuthenticationInfo(身份認證)這兩個方法。最基本的配置:
shiroConfig配置:spring
package com.cj.shirodemo.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import java.util.LinkedHashMap; import java.util.Map; /** * 描述: * * @author caojing * @create 2019-01-27-13:38 */ @Configuration public class ShiroConfig { @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setUnauthorizedUrl("/notRole"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // <!-- authc:全部url都必須認證經過才能夠訪問; anon:全部url都均可以匿名訪問--> filterChainDefinitionMap.put("/webjars/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/front/**", "anon"); filterChainDefinitionMap.put("/api/**", "anon"); filterChainDefinitionMap.put("/admin/**", "authc"); filterChainDefinitionMap.put("/user/**", "authc"); //主要這行代碼必須放在全部權限設置的最後,否則會致使全部 url 都被攔截 剩餘的都須要認證 filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(); defaultSecurityManager.setRealm(customRealm()); return defaultSecurityManager; } @Bean public CustomRealm customRealm() { CustomRealm customRealm = new CustomRealm(); return customRealm; } }
shiroConfig 也不復雜,基本就三個方法。再說這三個方法以前,我想給你們說一下shiro的三個核心概念:數據庫
Subject: 表明當前正在執行操做的用戶,但Subject表明的能夠是人,也能夠是任何第三方系統賬號。固然每一個subject實例都會被綁定到SercurityManger上。
SecurityManger:SecurityManager是Shiro核心,主要協調Shiro內部的各類安全組件,這個咱們不須要太關注,只須要知道能夠設置自定的Realm。
Realm:用戶數據和Shiro數據交互的橋樑。好比須要用戶身份認證、權限認證。都是須要經過Realm來讀取數據。
shiroFilter方法:
這個方法看名字就知道了:shiro的過濾器,能夠設置登陸頁面(setLoginUrl)、權限不足跳轉頁面(setUnauthorizedUrl)、具體某些頁面的權限控制或者身份認證。
注意:這裏是須要設置SecurityManager(setSecurityManager)。
默認的過濾器還有:anno、authc、authcBasic、logout、noSessionCreation、perms、port、rest、roles、ssl、user過濾器。
具體的你們能夠查看package org.apache.shiro.web.filter.mgt.DefaultFilter。這個類,經常使用的也就authc、anno。
securityManager 方法:
查看源碼能夠知道 securityManager是一個接口類,咱們能夠看下它的實現類:apache
具體怎麼實現的,感興趣的同窗能夠看下。因爲項目是一個web項目,因此咱們使用的是DefaultWebSecurityManager ,而後設置本身的Realm。
CustomRealm 方法:
將 customRealm的實例化交給spring去管理,固然這裏也能夠利用註解的方式去注入。customRealm配置:api
package com.cj.shirodemo.config; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; 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.apache.shiro.util.ByteSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import java.util.HashSet; import java.util.Set; /** * 描述: * * @author caojing * @create 2019-01-27-13:57 */ public class CustomRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String username = (String) SecurityUtils.getSubject().getPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<String> stringSet = new HashSet<>(); stringSet.add("user:show"); stringSet.add("user:admin"); info.setStringPermissions(stringSet); return info; } /** * 這裏能夠注入userService,爲了方便演示,我就寫死了賬號了密碼 * private UserService userService; * <p> * 獲取即將須要認證的信息 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("-------身份認證方法--------"); String userName = (String) authenticationToken.getPrincipal(); String userPwd = new String((char[]) authenticationToken.getCredentials()); //根據用戶名從數據庫獲取密碼 String password = "123"; if (userName == null) { throw new AccountException("用戶名不正確"); } else if (!userPwd.equals(password )) { throw new AccountException("密碼不正確"); } return new SimpleAuthenticationInfo(userName, password,getName()); } }
說明:
自定義的Realm類繼承AuthorizingRealm類,而且重載doGetAuthorizationInfo和doGetAuthenticationInfo兩個方法。
doGetAuthorizationInfo: 權限認證,即登陸事後,每一個身份不必定,對應的所能看的頁面也不同。
doGetAuthenticationInfo:身份認證。即登陸經過帳號和密碼驗證登錄人的身份信息。安全
controller類:
新建一個HomeIndexController類,加入以下代碼: springboot
@RequestMapping(value = "/login", method = RequestMethod.GET) @ResponseBody public String defaultLogin() { return "首頁"; } @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public String login(@RequestParam("username") String username, @RequestParam("password") String password) { // 從SecurityUtils裏邊建立一個 subject Subject subject = SecurityUtils.getSubject(); // 在認證提交前準備 token(令牌) UsernamePasswordToken token = new UsernamePasswordToken(username, password); // 執行認證登錄 try { subject.login(token); } catch (UnknownAccountException uae) { return "未知帳戶"; } catch (IncorrectCredentialsException ice) { return "密碼不正確"; } catch (LockedAccountException lae) { return "帳戶已鎖定"; } catch (ExcessiveAttemptsException eae) { return "用戶名或密碼錯誤次數過多"; } catch (AuthenticationException ae) { return "用戶名或密碼不正確!"; } if (subject.isAuthenticated()) { return "登陸成功"; } else { token.clear(); return "登陸失敗"; } }
測試:
咱們可使用postman進行測試:app
ok 身份認證是沒問題了,咱們再來考慮如何加入權限。
利用註解配置權限:
其實,咱們徹底能夠不用註解的形式去配置權限,由於在以前已經加過了:DefaultFilter類中有perms(相似於perms[user:add])這種形式的。可是試想一下,這種控制的粒度可能會很細,具體到某一個類中的方法,那麼若是是配置文件配,是否是每一個方法都要加一個perms?可是註解就不同了,直接寫在方法上面,簡單快捷。
很簡單,主須要在shiroConfig類中再加入以下代碼,就能開啓註解:
@Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * * * 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 * * * 配置如下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)便可實現此功能 * * @return */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; }
新建一個UserController類。以下:
@RequestMapping("/user") @Controller public class UserController { @RequiresPermissions("user:list") @ResponseBody @RequestMapping("/show") public String showUser() { return "這是學生信息"; } }
重複剛纔的登陸步驟,登陸成功後,postman 輸入localhost:8080/user/show
確實是沒有權限。方法上是 @RequiresPermissions("user:list"),而customRealm中是 user:show、user:admin。咱們能夠調整下方法上的權限改成user:show。調試一下,發現成功了。
這裏有一個問題:當沒有權限時,系統會報錯,而沒有跳轉到對應的沒有權限的頁面,也就是setUnauthorizedUrl這個方法沒起做用,這個問題,下一篇會給出解決方案-。-
密碼採用加密方式進行驗證:
其實上面的功能已經基本知足咱們的需求了,可是惟一一點美中不足的是,密碼都是採用的明文方式進行比對的。那麼shiro是否提供給咱們一種密碼加密的方式呢?答案是確定。
shiroConfig中加入加密配置:
@Bean(name = "credentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // 散列算法:這裏使用MD5算法; hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列的次數,好比散列兩次,至關於 md5(md5("")); hashedCredentialsMatcher.setHashIterations(2); // storedCredentialsHexEncoded默認是true,此時用的是密碼加密用的是Hex編碼;false時用Base64編碼 hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return hashedCredentialsMatcher; }
customRealm初始化的時候耶須要作一些改變:
@Bean public CustomRealm customRealm() { CustomRealm customRealm = new CustomRealm(); // 告訴realm,使用credentialsMatcher加密算法類來驗證密文 customRealm.setCredentialsMatcher(hashedCredentialsMatcher()); customRealm.setCachingEnabled(false); return customRealm; }
流程是這樣的,用戶註冊的時候,程序將明文經過加密方式加密,存到數據庫的是密文,登陸時將密文取出來,再經過shiro將用戶輸入的密碼進行加密對比,同樣則成功,不同則失敗。
咱們能夠看到這裏的加密採用的是MD5,並且是加密兩次(MD5(MD5))。
shiro提供了SimpleHash類幫助咱們快速加密:
public static String MD5Pwd(String username, String pwd) { // 加密算法MD5 // salt鹽 username + salt // 迭代次數 String md5Pwd = new SimpleHash("MD5", pwd, ByteSource.Util.bytes(username + "salt"), 2).toHex(); return md5Pwd; }
也就是說註冊的時候調用一下上面的方法獲得密文以後,再存入數據庫。
在CustomRealm進行身份認證的時候咱們也須要做出改變:
System.out.println("-------身份認證方法--------"); String userName = (String) authenticationToken.getPrincipal(); String userPwd = new String((char[]) authenticationToken.getCredentials()); //根據用戶名從數據庫獲取密碼 String password = "2415b95d3203ac901e287b76fcef640b"; if (userName == null) { throw new AccountException("用戶名不正確"); } else if (!userPwd.equals(userPwd)) { throw new AccountException("密碼不正確"); } //交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配 return new SimpleAuthenticationInfo(userName, password, ByteSource.Util.bytes(userName + "salt"), getName());
這裏惟一須要注意的是:你註冊的加密方式和設置的加密方式還有Realm中身份認證的方式都是要如出一轍的。 本文中的加密 :MD5兩次、salt=username+salt加密。