1. 程式人生 > >SpringBoot整合shiro實現用戶的認證授權

SpringBoot整合shiro實現用戶的認證授權

fig 判斷 sys token boolean ebe admin 解決 ora

  • * 項目環境搭建
  • * 配置ShiroConfig,用於shiro的基本配置和註入自定義規則
  • * 實現自定義的realm,繼承AuthorizingRealm
  • * 編寫測試controller和頁面
  1. 基本環境準備
  2. 導入依賴坐標
  3. maven管理、shiro1.4.0 和spring-shiro1.4.0依賴
  4. 導入數據源,配置thymeleaf,redis,等等

  1. shiro配置
  2. 配置shiroConfig
  3. 編寫自定義的realm
  4. 實現具體的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接口來加密,不過本質上差不多

SpringBoot整合shiro實現用戶的認證授權