- * 項目環境搭建
- * 配置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接口來加密,不過本質上差不多
