- * 項目環境搭建
- * 配置ShiroConfig,用於shiro的基本配置和注入自定義規則
- * 實現自定義的realm,繼承AuthorizingRealm
- * 編寫測試controller和頁面
- 基本環境准備
- 導入依賴坐標
- maven管理、shiro1.4.0 和spring-shiro1.4.0依賴
- 導入數據源,配置thymeleaf,redis,等等
- shiro配置
- 配置shiroConfig
- 編寫自定義的realm
- 實現具體的doGetAuthorizationInfo(授權)方法和doGetAuthenticationInfo(認證)
具體實現:
shiroConfig配置
@Bean(name = "securityManager") public DefaultWebSecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm){ DefaultWebSecurityManager ds = new DefaultWebSecurityManager(); ds.setRealm(myRealm); return ds; } @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager){ ShiroFilterFactoryBean sf = new ShiroFilterFactoryBean(); //設置安全管理器 sf.setSecurityManager(defaultSecurityManager); sf.setLoginUrl("/login"); sf.setUnauthorizedUrl("/non"); sf.setSuccessUrl("/index"); /** * 自定義過濾器 * anon: * authc: * user: 只有實現了remberme的操作才能訪問 * perms: 必須得到資源權限才能訪問 * role: 必須得到角色權限的時候才能訪問 */ Map<String,String> china = new LinkedHashMap<>(); china.put("/index","authc"); china.put("/update","authc"); china.put("/non","authc"); china.put("/toLogin","anon"); china.put("/add","perms[user:add]"); china.put("/**","anon"); sf.setFilterChainDefinitionMap(china); return sf; } @Bean(name = "myRealm") public MyRealm getRealm(){ return new MyRealm(); }
自定義編寫realm 繼承AuthorizingRealm 實現doGetAuthorizationInfo 和doGetAuthenticationInfo方法
/** * 自定義授權邏輯 * Authorization * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //給資源授權 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //加上授權字符串(當前用戶已經授權) simpleAuthorizationInfo.addStringPermission("user:add"); return simpleAuthorizationInfo; } /** * 自定義認證的邏輯 * 判斷用戶名和密碼 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //直接強轉 UsernamePasswordToken auth = (UsernamePasswordToken) authenticationToken; System.out.println("處理用戶登錄邏輯"); char[] password = auth.getPassword(); String username = auth.getUsername(); if(!"user".equals(username)){ //返回null 會拋出 UnknownAccountException return null; } //用戶傳入的密碼 數據庫中加載出來的密碼 return new SimpleAuthenticationInfo(password,"123",""); }
首先配置shiro的配置類使用@Configuration注解類上,這里面我們需要基本的三個配置類
@Bean
ShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager)
@Bean(name = "securityManager")
DefaultWebSecurityManager (@Qualifier("myRealm") MyRealm myRealm)
@Bean(name = "myRealm")
MyRealm()
第一個是可以自定義認證授權規則,配置權限攔截規則指定跳轉頁面
第二個是將shiro的安全管理器注入,然后返回
第三種是自定義實現自己認證授權邏輯
自定義realm
繼承自AuthorizingRealm,實現其中的兩個方法
`protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)`
1. 此方法是授權邏輯的實現,指定用戶可以擁有那些權限,將其set到addStringPermission集合中即可
2. 其中授權類是其的一個子類
3. SimpleAuthorizationInfo simpleAuthorizationInfo
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
此方式認證邏輯,用於判斷用戶是否登錄成功,或者異常
用戶名判斷錯誤返回 null
密碼使用new SimpleAuthenticationInfo(用戶輸入的密碼,數據庫的密碼,"");
舉個例子:
/** * 自定義授權邏輯 * Authorization * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //給資源授權 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //加上授權字符串(當前用戶已經授權) simpleAuthorizationInfo.addStringPermission("user:add"); return simpleAuthorizationInfo; } /** * 自定義認證的邏輯 * 判斷用戶名和密碼 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken auth = (UsernamePasswordToken) authenticationToken; System.out.println("處理用戶登錄邏輯"); char[] password = auth.getPassword(); String username = auth.getUsername(); if(!"user".equals(username)){ //返回null 會拋出 UnknownAccountException return null; } return new SimpleAuthenticationInfo(password,"123",""); } /** * 自定密碼判斷 * * @param pass * @return */ private boolean isPassWord(char[] pass){ String password = "123"; if(pass.length != password.length()){ return false; } char[] chars = password.toCharArray(); for(int i = 0; i < pass.length; i++){ if(chars[i] != pass[i]){ return false; } } return true; }
// 當然授權也可以在controller層中通過@RequiresPermissions("user:update")注解在當前用戶操作的地址上授權
<p style="color:red">
注意: 這里面需要在shiroconfig中配置以下內容:
</p>
/**
* 解決@RequiresPermissions("XX:XXX:...")注解無效
*
* @return
*/
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(defaultSecurityManager);
return advisor;
}
並且在controller里面配置的權限檢驗,用戶驗證失敗,會跳轉到/error頁面上,需要自己自定義頁面
注意:
這里面有個大坑,開始使用如下代碼發現並未解決問題
@Bean public SimpleMappingExceptionResolver simpleMappingExceptionResolver(){ SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver(); Properties properties = new Properties(); properties.put("org.apache.shiro.authz.UnauthorizedException","/non"); simpleMappingExceptionResolver.setExceptionMappings(properties); return simpleMappingExceptionResolver; }
最后通過springMVC的異常處理類指定跳轉頁面,才解決此問題。
@ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(UnauthenticatedException.class) public String nauthenticatedException(Model model){ model.addAttribute("errorMsg","當前無用戶!"); return "nonauth"; } @ExceptionHandler(UnauthorizedException.class) public String nauthorizedException(Model model){ model.addAttribute("errorMsg","當前用戶沒有權限"); return "nonauth"; } }
不錯
現在已經解決了兩個問題:
1. 在用戶未登錄的時候,直接訪問帶有@RequiresPermissions權限的注解時會報錯,而不是跳轉到指定頁面,或者返回相應的信息
2. 未使用在remal配置的權限過濾器的時候,而是在@Controller上直接帶有@RequiresPermissions("user:add") 會報500錯誤,而不是可控操作
在doGetAuthenticationInfo方法中,獲取用戶登錄時候的信息操作,驗證用戶,將用戶存入session,以后通過subject對象強轉。
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( user, user.getPassword(), ByteSource.Util.bytes(salt), //realm name getName());
user 是開始通過用戶名查詢到的用戶信息
salt是加鹽處理,硬編碼加鹽處理
#shiro密碼匹配#
這里先來個簡單的直接加鹽 然后使用MD5加密方式
密碼匹配則時先將用戶輸入的密碼加鹽再字符串比較
實現自定義的Realm 繼承自AuthorizingRealm doGetAuthenticationInfo方法中使用 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( 用戶對象 user, 數據庫用戶密碼 user.getPassword(), 加鹽處理 ByteSource.Util.bytes(salt), //realm name getName()); 將用戶對象保存至 String string = MD5Util.encryptString(password+salt); UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,string);
使用shiro的HashedCredentialsMatcher自定義密碼加密
先模擬用戶和密碼數據 使用單元測試:
@Test public void contextLoads() { String algorithmName = "md5"; String username = "admin"; String password = "123"; String salt1 = username; String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex(); int hashIterations = 3; SimpleHash hash = new SimpleHash(algorithmName, password, salt1 + salt2, hashIterations); String encodedPassword = hash.toHex(); System.out.println(encodedPassword); System.out.println(salt2); }
將生成的數據保存到數據庫中,驗證的時候將密碼和鹽拿出來,這時候,用戶的注冊名和密碼在注冊的時候保存到數據庫中
在Shiro的配置類中,注入
/** * 憑證匹配器 * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 * 所以我們需要修改下doGetAuthenticationInfo中的代碼; * ) * @return */ @Bean(name = "hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new MyHashedCredent(); //使用指定的散列算法 hashedCredentialsMatcher.setHashAlgorithmName("MD5"); //幾次散列? hashedCredentialsMatcher.setHashIterations(3); //設置16進制編碼 hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return hashedCredentialsMatcher; }
在將其自定義的密碼加載類注入到自定義的realm中
@Bean(name = "myRealm") public MyRealm getRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher){ MyRealm myRealm = new MyRealm(); // 設置自定義加密 myRealm.setCredentialsMatcher(hashedCredentialsMatcher); return myRealm; }
下面實現自定義密碼:
/** * @author zhangyi * @date 2018/12/12 20:43 */ public class MyHashedCredent extends HashedCredentialsMatcher { public MyHashedCredent(){} /** * 以后放到redis中保存 */ private Cache<String, AtomicInteger> passwordRetryCache; public MyHashedCredent(CacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //限制每個用戶的請求登錄次數(密碼錯誤的時候) String username = (String) token.getPrincipal(); if(!Objects.isNull(passwordRetryCache)) { // retry count + 1 AtomicInteger retryCount = passwordRetryCache.get(username); if (retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if (retryCount.incrementAndGet() > 5) { // if retry count > 5 throw throw new ExcessiveAttemptsException(); } } boolean matches = super.doCredentialsMatch(token, info); if (matches) { // clear retry count // passwordRetryCache.remove(username); } return matches; } }
按照開濤神的方法,計算其密碼重試的次數,將其保存到EhCache中,這里我保存到redis,因為好用吧。
開始使用硬編碼加鹽,網上說可能會有安全問題,現在采用
用戶名+密碼+隨機數 --> 在N次散列 加密,應該好一點點
我這里是繼承了HashedCredentialsMatcher這中加密方式,還可以繼承SimpleCredentialsMatcher,或者繼承PasswordMatcher,或者實現CredentialsMatcher接口來加密,不過本質上差不多