天天看點

Springboot內建Shiro實作權限認證

請多多留言指教

什麼是Shiro?

Apache Shiro是一個強大且易用的Java安全架構,執行身份驗證、授權、密碼和會話管理。

Shiro核心元件

Subject,SecurityManager,Realms.

1、Subject:即“目前操作使用者”。但是,在Shiro中,Subject這一概念并不僅僅指人,也可以是第三方程序、背景帳戶或其他類似事物。它僅僅意味着“目前跟軟體互動的東西”。

Subject代表了目前使用者的安全操作,SecurityManager則管理所有使用者的安全操作。

2、SecurityManager:它是Shiro架構的核心,典型的Facade模式,Shiro通過SecurityManager來管理内部元件執行個體,并通過它來提供安全管理的各種服務。

3、Realm: Realm充當了Shiro與應用安全資料間的“橋梁”或者“連接配接器”。也就是說,當對使用者執行認證(登入)和授權(通路控制)驗證時,Shiro會從應用配置的Realm中查找使用者及其權限資訊。

通俗說三者的關系:

Subject收集使用者名密碼,交給SecurityManager,SecurityManager負責排程Realms來比對使用者名密碼,如果傳回true則驗證成功,否則驗證失敗。

Shiro的特性?

Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)被 Shiro 架構的開發團隊稱之為應用安全的四大基石。

Authentication(認證):使用者身份識别,通常被稱為使用者“登入”。

Authorization(授權):通路控制。比如某個使用者是否具有某個操作的使用權限。

Session Management(會話管理):特定于使用者的會話管理。

Cryptography(加密):在對資料源使用加密算法加密的同時,保證易于使用

shiro介紹不多說了,直接上自己測試的案例代碼!!!

注:

<a> 資料庫:MySQL

<b> 相關表:使用者表,角色表,權限表,使用者角色表,角色權限表(使用者、角色、權限等資料查詢代碼省略)

<c> CommonConstant.SHIRO_ENCRYPTION_NUMBER公共常量值1024,即密碼加鹽加密次數

1、pom.xml配置

注:相關springboot依賴省略!
<!-- shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.2</version>
</dependency>
<!-- shiro ehcache緩存 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.5.3</version>
</dependency>
<!-- shiro-core -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.3</version>
</dependency>
           

2、shiro config配置

package com.ayiol.business.config;
 
import com.ayiol.business.AyiolBackendApplication;
import com.ayiol.business.constant.CommonConstant;
import com.ayiol.business.shiro.MyShiroRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.LinkedHashMap;
 
/**
 * shiro config
 *
 * @Author: LJG
 * @Date: 2020-07-18
 */
@Configuration
public class ShiroConfig {
    private static final Logger logger = LoggerFactory.getLogger(AyiolBackendApplication.class);
 
    /**
     * 密碼校驗規則HashedCredentialsMatcher
     * 這個類是為了對密碼進行編碼的 ,
     * 防止密碼在資料庫裡明碼儲存 , 當然在登陸認證的時候 ,
     * 這個類也負責對form裡輸入的密碼進行編碼
     * 處理認證比對處理器:如果自定義需要實作繼承HashedCredentialsMatcher
     */
    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //加密方式 雜湊演算法:MD2、MD5、SHA-1、SHA-256、SHA-384、SHA-512等。
        credentialsMatcher.setHashAlgorithmName("MD5");
        //加密次數
        credentialsMatcher.setHashIterations(CommonConstant.SHIRO_ENCRYPTION_NUMBER);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }
 
    /**
     * 自定義身份認證 realm;
     * <p>
     * 必須寫這個類,并加上 @Bean 注解,目的是注入 MyShiroRealm,否則會影響 MyShiroRealm類 中其他類的依賴注入
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        return myShiroRealm;
    }
 
    /**
     * 注入 securityManager
     * 權限管理,配置主要是Realm的管理認證
     */
    @Bean("securityManager")
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }
 
    /**
     * Filter工廠,設定對應的過濾條件和跳轉條件
     *
     * @param securityManager securityManager
     * @return ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        logger.info("======>load shiro config");
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //設定securityManager
        bean.setSecurityManager(securityManager);
        //設定登入頁面
        bean.setLoginUrl("/login");
//        //設定登入成功跳轉的頁面
//        bean.setSuccessUrl("/pages/index.html");
//        //設定未授權跳轉的頁面
//        bean.setUnauthorizedUrl("/pages/unauthorized.html");
        //定義過濾器
        LinkedHashMap<String, String> filterMapper = new LinkedHashMap<>();
        /**
         * 放行靜态資源
         */
        filterMapper.put("/css/**", "anon");
        filterMapper.put("/images/**", "anon");
        filterMapper.put("/js/**", "anon");
        /**
         * 放行公共界面
         */
        filterMapper.put("/login", "anon");
        filterMapper.put("/logout", "anon");
        //需要登入通路的資源 , 一般将/**放在最下邊
        filterMapper.put("/**", "authc");
        bean.setFilterChainDefinitionMap(filterMapper);
        return bean;
    }
 
    /**
     * 開啟aop注解支援
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
 
    @Bean
    public RememberMeManager rememberMeManager() {
        CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
        //注入自定義cookie(主要是設定壽命, 預設的一年太長)
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        simpleCookie.setHttpOnly(true);
        //設定RememberMe的cookie有效期
        simpleCookie.setMaxAge(CommonConstant.SHIRO_COOKIE_EXPIRE_TIME);
        rememberMeManager.setCookie(simpleCookie);
        //手動設定對稱加密秘鑰,防止重新開機系統後系統生成新的随機秘鑰,防止導緻用戶端cookie無效
        rememberMeManager.setCipherKey(Base64.decode("YV95aW9fbDIwXzIwc2VfcnZlcg=="));
        return rememberMeManager;
    }
 
    @Bean
    public EhCacheManager getCache() {
        return new EhCacheManager();
    }
}
           

3、MyShiroRealm自定義Beam

package com.ayiol.business.shiro;

import com.ayiol.business.AyiolBackendApplication;
import com.ayiol.business.Utils.RedisUtil;
import com.ayiol.business.Utils.SpringBeanFactoryUtil;
import com.ayiol.business.constant.CommonConstant;
import com.ayiol.business.entity.User;
import com.ayiol.business.entity.UserRole;
import com.ayiol.business.service.RoleService;
import com.ayiol.business.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * shiro realm
 *
 * @Author: LJG
 * @Date: 2020-07-08 16:00
 */
public class MyShiroRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(AyiolBackendApplication.class);

    @Autowired
    private UserService userService;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private RoleService roleService;

    /**
     * 授權
     *
     * @param principalCollection 主要的控制器
     * @return Authorization
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("=========> 進入自定義權限設定方法!");
        if (null == userService) {
            userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
        }
        if (null == roleService) {
            roleService = (RoleService) SpringBeanFactoryUtil.getBeanByName("roleServiceImpl");
        }

        // 1 擷取使用者資訊
        User user = (User) principalCollection.getPrimaryPrincipal();
        if (null == user) {
            return null;
        }

        // 2 擷取使用者的角色id
        List<UserRole> userRoles = userService.getAuthUserRole(null == user ? "" : user.getId());
        List<String> roleIds = userRoles.stream().filter(f -> null != f.getRole()).map(ur -> ur.getRole().getId()).collect(Collectors.toList());
        List<String> roleCodes = userRoles.stream().filter(f -> null != f.getRole()).map(ur -> ur.getRole().getCode()).collect(Collectors.toList());
        Set<String> roleCodesSet = new HashSet<>(roleCodes);// 轉換為Set類型

        // 3 擷取使用者的權限id
        List<String> permissionNames = roleService.getAuthPermission(roleIds);
        Set<String> permissionNamesSet = new HashSet<>(permissionNames);// 轉換為Set類型

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 4 設定權限
        simpleAuthorizationInfo.setStringPermissions(permissionNamesSet);
        // 5 設定角色
        simpleAuthorizationInfo.setRoles(roleCodesSet);
        return simpleAuthorizationInfo;
    }

    /**
     * 身份認證
     *
     * @param token 認證token
     * @return Authentication
     * @throws AuthenticationException 認證異常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info("=========> 進入自定義登入認證方法!");
        //表示沒有登入
        //加這一步的目的是在Post請求的時候會先進認證,然後在到請求
        if (null == token.getPrincipal()) {
            return null;
        }

        // 如果userService為空時,則手動加載userService
        if (null == userService) {
            userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
        }

        // 實際項目中,這裡可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘内不會重複執行該方法
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String username = usernamePasswordToken.getUsername();
        String pwd = String.valueOf(usernamePasswordToken.getPassword());

        // 通過username從資料庫中查找 User對象
        User user = userService.queryByUsername(username);
        if (null == user) {
            throw new UnknownAccountException(); // 使用者不存在
        }

        // ------------------------------- 認證方式:SimpleAuthenticationInfo加鹽 -----------------------------
        // 給登入使用者名、密碼加鹽,按特定規則生成一個新的密碼;指派給usernamePasswordToken用于跟SimpleAuthenticationInfo校驗密碼是否一緻
        String saltPwd = createSalting(username, pwd);
        usernamePasswordToken.setPassword(saltPwd.toCharArray());

        //根據使用者的情況,來建構AuthenticationInfo對象,通常使用的實作類為SimpleAuthenticationInfo
        //以下資訊是從資料庫中擷取的
        //1)principal:認證的實體資訊,可以是username,也可以是資料庫表對應的使用者的實體對象
        Object principal = user.getUsername();
        //2)credentials:密碼
        Object credentials = user.getPassword();
        //3)realmName:目前realm對象的name,調用父類的getName()方法即可
        String realmName = getName();
        //4)credentialsSalt鹽值
        ByteSource credentialsSalt = ByteSource.Util.bytes(principal);//使用賬号作為鹽值
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, credentials, credentialsSalt, realmName);
        return info;
    }

    /**
     * 密碼加鹽
     *
     * @param username   使用者名
     * @param crdentials 密碼原值
     * @return 加鹽後的密碼
     */
    private static String createSalting(String username, String crdentials) {
        String hashAlgorithmName = "MD5";//加密方式
        ByteSource salt = ByteSource.Util.bytes(username);//以使用者名為鹽值
        int hashIterations = CommonConstant.SHIRO_ENCRYPTION_NUMBER;//加密次數
        Object result = new SimpleHash(hashAlgorithmName, crdentials, salt, hashIterations);
        return result + "";
    }
}
           

4、LoginController.java /login登入路由

@RequestMapping(value = "/login", method = RequestMethod.POST)
    public Map<String, Object> login(@RequestBody Map paramMap) {
        // 擷取參數 校驗
        String username = StringUtils.isEmpty(paramMap.get("username")) ? "" : paramMap.get("username").toString();
        String password = StringUtils.isEmpty(paramMap.get("password")) ? "" : paramMap.get("password").toString();
        if ("".equals(username)) {
            return ResultUtil.badRequest("username", paramMap.get("username"), "使用者名不能為空");
        }
        if ("".equals(password)) {
            return ResultUtil.badRequest("password", paramMap.get("password"), "密碼不能為空");
        }

//        //如果使用者已登入,先踢出
//        ShiroSecurityHelper.kickOutUser(user.getUsername());

        /**
         * 使用shiro認證
         */
        //1,擷取subject
        Subject subject = SecurityUtils.getSubject();
        //2,封裝使用者資料
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        if (!StringUtils.isEmpty(paramMap.get("rememberMe"))
                && CommonConstant.ONE_STRING.equals(paramMap.get("rememberMe").toString())) {
            token.setRememberMe(true);
        }
        //3,執行登入方法
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {
            // 使用者名未知
            return ResultUtil.failureResponse("使用者不存在!");
        } catch (IncorrectCredentialsException ice) {
            // 憑據不正确,例如密碼不正确
            return ResultUtil.failureResponse("密碼錯誤!");
        } catch (LockedAccountException lae) {
            // 使用者被鎖定,例如管理者把某個使用者禁用
            return ResultUtil.failureResponse("使用者被鎖定!");
        } catch (ExcessiveAttemptsException eae) {
            // 嘗試認證次數多餘系統指定次數
            return ResultUtil.failureResponse("嘗試認證次數過多,請稍後重試!");
        } catch (AuthenticationException ae) {
            // 其他未指定異常
            return ResultUtil.failureResponse("未知異常!");
        }

        // 從shiro中擷取使用者資訊
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        user.setPassword(null);
        return ResultUtil.ok(user);
    }
           

5、LoginController.java /logout登出路由

@RequestMapping(value = "/logout", method = RequestMethod.POST)
    public Map<String, Object> logout(@RequestBody Map paramMap) {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            User user = (User) subject.getPrincipal();// 擷取登入使用者資訊
            subject.logout();
        }
        return ResultUtil.ok("登出成功!");
    }
           

6、項目為前後端分離,這裡使用的注解方式測試認證、授權

import com.alibaba.fastjson.JSON;

    @RequiresRoles(value = {"admin"})
    @RequiresPermissions(value = {"all", "read"}, logical = Logical.OR)
    @PostMapping("")
    public Map<String, Object> save(@RequestBody Map paramMap) {
        if (StrUtil.isEmptyIfStr(paramMap.get("username"))) {
            return ResultUtil.failureResponse("登入使用者名不能為空");
        }

        User exsistUser = userService.queryByUsername(paramMap.get("username").toString());
        if (null != exsistUser) {
            return ResultUtil.failureResponse("登入使用者名已經存在");
        }

        //轉換為使用者實體類
        User user = JSON.parseObject(JSON.toJSONString(paramMap), User.class);
        user.setId(UUID.randomUUID().toString());
        user.setCreatedAt(new Date());
        userService.save(user);
        user.setPassword(null);
        return ResultUtil.ok(user);
    }
           

到此就完成了!有什麼問題可以留言。