溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理

發(fā)布時(shí)間:2022-03-29 14:11:27 來源:億速云 閱讀:173 作者:iii 欄目:大數(shù)據(jù)

今天小編給大家分享一下Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

知識(shí)點(diǎn)補(bǔ)充

Shiro緩存

流程分析

在原來的項(xiàng)目當(dāng)中,由于沒有配置緩存,因此每次需要驗(yàn)證當(dāng)前主體有沒有訪問權(quán)限時(shí),都會(huì)去查詢數(shù)據(jù)庫。由于權(quán)限數(shù)據(jù)是典型的讀多寫少的數(shù)據(jù),因此,我們應(yīng)該要對(duì)其加入緩存的支持。

當(dāng)我們加入緩存后,shiro在做鑒權(quán)時(shí)先去緩存里查詢相關(guān)數(shù)據(jù),緩存里沒有,則查詢數(shù)據(jù)庫并將查到的數(shù)據(jù)寫入緩存,下次再查時(shí)就能從緩存當(dāng)中獲取數(shù)據(jù),而不是從數(shù)據(jù)庫中獲取。這樣就能改善我們的應(yīng)用的性能。

接下來,我們?nèi)?shí)現(xiàn)shiro的緩存管理部分。

Shiro會(huì)話機(jī)制

Shiro 提供了完整的企業(yè)級(jí)會(huì)話管理功能,不依賴于底層容器(如 web 容器 tomcat),不管 JavaSE 還是 JavaEE 環(huán)境都可以使用,提供了會(huì)話管理、會(huì)話事件監(jiān)聽、會(huì)話存儲(chǔ) / 持久化、容器無關(guān)的集群、失效 / 過期支持、對(duì) Web 的透明支持、SSO 單點(diǎn)登錄的支持等特性。

我們將使用 Shiro 的會(huì)話管理來接管我們應(yīng)用的web會(huì)話,并通過Redis來存儲(chǔ)會(huì)話信息。

整合步驟

添加緩存

CacheManager

在Shiro當(dāng)中,它提供了CacheManager這個(gè)類來做緩存管理。

使用Shiro默認(rèn)的EhCache實(shí)現(xiàn)

在shiro當(dāng)中,默認(rèn)使用的是EhCache緩存框架。EhCache 是一個(gè)純Java的進(jìn)程內(nèi)緩存框架,具有快速、精干等特點(diǎn)。

引入shiro-EhCache依賴
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

在SpringBoot整合Redis的過程中,還要注意版本匹配的問題,不然有可能報(bào)方法未找到的異常。

在ShiroConfig中添加緩存配置
private void enableCache(MySQLRealm realm){
    //開啟全局緩存配置
    realm.setCachingEnabled(true);
    //開啟認(rèn)證緩存配置
    realm.setAuthenticationCachingEnabled(true);
    //開啟授權(quán)緩存配置
    realm.setAuthorizationCachingEnabled(true);

    //為了方便操作,我們給緩存起個(gè)名字
    realm.setAuthenticationCacheName("authcCache");
    realm.setAuthorizationCacheName("authzCache");
    //注入緩存實(shí)現(xiàn)
    realm.setCacheManager(new EhCacheManager());
}

然后再在getRealm中調(diào)用這個(gè)方法即可。

提示:在這個(gè)實(shí)現(xiàn)當(dāng)中,只是實(shí)現(xiàn)了本地的緩存。也就是說緩存的數(shù)據(jù)同應(yīng)用一樣共用一臺(tái)機(jī)器的內(nèi)存。如果服務(wù)器發(fā)生宕機(jī)或意外停電,那么緩存數(shù)據(jù)也將不復(fù)存在。當(dāng)然你也可通過cacheManager.setCacheManagerConfigFile()方法給予緩存更多的配置。

接下來我們將通過Redis緩存我們的權(quán)限數(shù)據(jù)

使用Redis實(shí)現(xiàn)

添加依賴
<!--shiro-redis相關(guān)依賴-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
            <!--    里面這個(gè)shiro-core版本較低,會(huì)引發(fā)一個(gè)異常
					ClassNotFoundException: org.apache.shiro.event.EventBus
                    需要排除,直接使用上面的shiro
                    shiro1.3 加入了時(shí)間總線。-->
            <exclusions>
                <exclusion>
                    <groupId>org.apache.shiro</groupId>
                    <artifactId>shiro-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
配置redis

在application.yml中添加redis的相關(guān)配置

spring:
   redis:
     host: 127.0.0.1
     port: 6379
     password: hewenping
     timeout: 3000
     jedis:
       pool:
         min-idle: 5
         max-active: 20
         max-idle: 15

修改ShiroConfig配置類,添加shiro-redis插件配置

/**shiro配置類
 * @author 賴柄灃 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/6 9:11
 */
@Configuration
public class ShiroConfig {

    private static final String CACHE_KEY = "shiro:cache:";
    private static final String SESSION_KEY = "shiro:session:";
    private static final int EXPIRE = 18000;
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;
    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * 創(chuàng)建ShiroFilter攔截器
     * @return ShiroFilterFactoryBean
     */
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //配置不攔截路徑和攔截路徑,順序不能反
        HashMap<String, String> map = new HashMap<>(5);

        map.put("/authc/**","anon");
        map.put("/login.html","anon");
        map.put("/js/**","anon");
        map.put("/css/**","anon");

        map.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        //覆蓋默認(rèn)的登錄url
        shiroFilterFactoryBean.setLoginUrl("/authc/unauthc");
        return shiroFilterFactoryBean;
    }

    @Bean
    public Realm getRealm(){
        //設(shè)置憑證匹配器,修改為hash憑證匹配器
        HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher();
        //設(shè)置算法
        myCredentialsMatcher.setHashAlgorithmName("md5");
        //散列次數(shù)
        myCredentialsMatcher.setHashIterations(1024);
        MySQLRealm realm = new MySQLRealm();
        realm.setCredentialsMatcher(myCredentialsMatcher);
        //開啟緩存
        realm.setCachingEnabled(true);
        realm.setAuthenticationCachingEnabled(true);
        realm.setAuthorizationCachingEnabled(true);
        return realm;
    }

    /**
     * 創(chuàng)建shiro web應(yīng)用下的安全管理器
     * @return DefaultWebSecurityManager
     */
    @Bean
    public DefaultWebSecurityManager getSecurityManager( Realm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
    
        securityManager.setCacheManager(cacheManager());
        SecurityUtils.setSecurityManager(securityManager);
        return securityManager;
    }



    /**
     * 配置Redis管理器
     * @Attention 使用的是shiro-redis開源插件
     * @return
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(maxIdle+maxActive);
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        redisManager.setJedisPoolConfig(jedisPoolConfig);
        return redisManager;
    }


    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(CACHE_KEY);
        // shiro-redis要求放在session里面的實(shí)體類必須有個(gè)id標(biāo)識(shí)
        //這是組成redis中所存儲(chǔ)數(shù)據(jù)的key的一部分
        redisCacheManager.setPrincipalIdFieldName("username");
        return redisCacheManager;
    }

}

修改MySQLRealm中的doGetAuthenticationInfo方法,將User對(duì)象整體作為SimpleAuthenticationInfo的第一個(gè)參數(shù)。shiro-redis將根據(jù)RedisCacheManagerprincipalIdFieldName屬性值從第一個(gè)參數(shù)中獲取id值作為redis中數(shù)據(jù)的key的一部分。

/**
 * 認(rèn)證
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    if(token==null){
        return null;
    }
    String principal = (String) token.getPrincipal();
    User user = userService.findByUsername(principal);
    SimpleAuthenticationInfo simpleAuthenticationInfo = new MyAuthcInfo(
            //由于shiro-redis插件需要從這個(gè)屬性中獲取id作為redis的key
            //所有這里傳的是user而不是username
            user,
            //憑證信息
            user.getPassword(),
            //加密鹽值
            new CurrentSalt(user.getSalt()),
            getName());
    
    return simpleAuthenticationInfo;
}

并修改MySQLRealm中的doGetAuthorizationInfo方法,從User對(duì)象中獲取主身份信息。

/**
 * 授權(quán)
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
   User user = (User) principals.getPrimaryPrincipal();
    String username = user.getUsername();
    List<Role> roleList = roleService.findByUsername(username);
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    for (Role role : roleList) {
        authorizationInfo.addRole(role.getRoleName());
    }
    List<Long> roleIdList  = new ArrayList<>();
    for (Role role : roleList) {
        roleIdList.add(role.getRoleId());
    }

    List<Resource> resourceList = resourceService.findByRoleIds(roleIdList);
    for (Resource resource : resourceList) {
        authorizationInfo.addStringPermission(resource.getResourcePermissionTag());
    }
    return authorizationInfo;
}
自定義Salt

由于Shiro里面默認(rèn)的SimpleByteSource沒有實(shí)現(xiàn)序列化接口,導(dǎo)致ByteSource.Util.bytes()生成的salt在序列化時(shí)出錯(cuò),因此需要自定義Salt類并實(shí)現(xiàn)序列化接口。并在自定義的Realm的認(rèn)證方法使用new CurrentSalt(user.getSalt())傳入鹽值。

/**由于shiro當(dāng)中的ByteSource沒有實(shí)現(xiàn)序列化接口,緩存時(shí)會(huì)發(fā)生錯(cuò)誤
 * 因此,我們需要通過自定義ByteSource的方式實(shí)現(xiàn)這個(gè)接口
 * @author 賴柄灃 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/8 16:17
 */
public class CurrentSalt extends SimpleByteSource implements Serializable {
    public CurrentSalt(String string) {
        super(string);
    }

    public CurrentSalt(byte[] bytes) {
        super(bytes);
    }

    public CurrentSalt(char[] chars) {
        super(chars);
    }

    public CurrentSalt(ByteSource source) {
        super(source);
    }

    public CurrentSalt(File file) {
        super(file);
    }

    public CurrentSalt(InputStream stream) {
        super(stream);
    }
}

添加Shiro自定義會(huì)話

添加自定義會(huì)話ID生成器

/**SessionId生成器
 * <p>@author 賴柄灃 laibingf_dev@outlook.com</p>
 * <p>@date 2020/8/15 15:19</p>
 */
public class ShiroSessionIdGenerator implements SessionIdGenerator {

    /**
     *實(shí)現(xiàn)SessionId生成
     * @param session
     * @return
     */
    @Override
    public Serializable generateId(Session session) {
        Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
        return String.format("login_token_%s", sessionId);
    }
}

添加自定義會(huì)話管理器

/**
 * <p>@author 賴柄灃 laibingf_dev@outlook.com</p>
 * <p>@date 2020/8/15 15:40</p>
 */
public class ShiroSessionManager extends DefaultWebSessionManager {

    //定義常量
    private static final String AUTHORIZATION = "Authorization";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    //重寫構(gòu)造器
    public ShiroSessionManager() {
        super();
        this.setDeleteInvalidSessions(true);
    }

    /**
     * 重寫方法實(shí)現(xiàn)從請(qǐng)求頭獲取Token便于接口統(tǒng)一
     *      * 每次請(qǐng)求進(jìn)來,
     *      Shiro會(huì)去從請(qǐng)求頭找Authorization這個(gè)key對(duì)應(yīng)的Value(Token)
     * @param request
     * @param response
     * @return
     */
    @Override
    public Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果請(qǐng)求頭中存在token 則從請(qǐng)求頭中獲取token
        if (!StringUtils.isEmpty(token)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            // 這里禁用掉Cookie獲取方式
            return null;
        }
    }
}

配置自定義會(huì)話管理器

在ShiroConfig中添加對(duì)會(huì)話管理器的配置

/**
 * SessionID生成器
 *
 */
@Bean
public ShiroSessionIdGenerator sessionIdGenerator(){
    return new ShiroSessionIdGenerator();
}

/**
 * 配置RedisSessionDAO
 */
@Bean
public RedisSessionDAO redisSessionDAO() {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager());
    redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
    redisSessionDAO.setKeyPrefix(SESSION_KEY);
    redisSessionDAO.setExpire(EXPIRE);
    return redisSessionDAO;
}

/**
 * 配置Session管理器
 * @Author Sans
 *
 */
@Bean
public SessionManager sessionManager() {
    ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
    shiroSessionManager.setSessionDAO(redisSessionDAO());
    //禁用cookie
    shiroSessionManager.setSessionIdCookieEnabled(false);
    //禁用會(huì)話id重寫
    shiroSessionManager.setSessionIdUrlRewritingEnabled(false);
    return shiroSessionManager;
}

目前最新版本(1.6.0)中,session管理器的setSessionIdUrlRewritingEnabled(false)配置沒有生效,導(dǎo)致沒有認(rèn)證直接訪問受保護(hù)資源出現(xiàn)多次重定向的錯(cuò)誤。將shiro版本切換為1.5.0后就解決了這個(gè)bug。

本來這篇文章應(yīng)該是昨晚發(fā)的,因?yàn)檫@個(gè)原因搞了好久,所有今天才發(fā)。。。

修改自定義Realm的doGetAuthenticationInfo認(rèn)證方法

在認(rèn)證信息返回前,我們需要做一個(gè)判斷:如果當(dāng)前用戶已在舊設(shè)備上登錄,則需要將舊設(shè)備上的會(huì)話id刪掉,使其下線。

/**
 * 認(rèn)證
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    if(token==null){
        return null;
    }
    String principal = (String) token.getPrincipal();
    User user = userService.findByUsername(principal);
    SimpleAuthenticationInfo simpleAuthenticationInfo = new MyAuthcInfo(
            //由于shiro-redis插件需要從這個(gè)屬性中獲取id作為redis的key
            //所有這里傳的是user而不是username
            user,
            //憑證信息
            user.getPassword(),
            //加密鹽值
            new CurrentSalt(user.getSalt()),
            getName());

    //清除當(dāng)前主體舊的會(huì)話,相當(dāng)于你在新電腦上登錄系統(tǒng),把你之前在舊電腦上登錄的會(huì)話擠下去
    ShiroUtils.deleteCache(user.getUsername(),true);
    return simpleAuthenticationInfo;
}

修改login接口

我們將會(huì)話信息存儲(chǔ)在redis中,并在用戶認(rèn)證通過后將會(huì)話Id以token的形式返回給用戶。用戶請(qǐng)求受保護(hù)資源時(shí)帶上這個(gè)token,我們根據(jù)token信息去redis中獲取用戶的權(quán)限信息,從而做訪問控制。

@PostMapping("/login")
public HashMap<Object, Object> login(@RequestBody LoginVO loginVO) throws AuthenticationException {
    boolean flags = authcService.login(loginVO);
    HashMap<Object, Object> map = new HashMap<>(3);
    if (flags){
        Serializable id = SecurityUtils.getSubject().getSession().getId();
        map.put("msg","登錄成功");
        map.put("token",id);
        return map;
    }else {
        return null;
    } 
}

添加全局異常處理

/**shiro異常處理
 * @author 賴柄灃 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/7 18:01
 */
@ControllerAdvice(basePackages = "pers.lbf.springbootshiro")
public class AuthExceptionHandler {

    //==================認(rèn)證異常====================//

    @ExceptionHandler(ExpiredCredentialsException.class)
    @ResponseBody
    public String expiredCredentialsExceptionHandlerMethod(ExpiredCredentialsException e) {
        return "憑證已過期";
    }

    @ExceptionHandler(IncorrectCredentialsException.class)
    @ResponseBody
    public String incorrectCredentialsExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "用戶名或密碼錯(cuò)誤";
    }

    @ExceptionHandler(UnknownAccountException.class)
    @ResponseBody
    public String unknownAccountExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "用戶名或密碼錯(cuò)誤";
    }

    
    @ExceptionHandler(LockedAccountException.class)
    @ResponseBody
    public String lockedAccountExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "賬戶被鎖定";
    }

    //=================授權(quán)異常=====================//

    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public String unauthorizedExceptionHandlerMethod(UnauthorizedException e){
        return "未授權(quán)!請(qǐng)聯(lián)系管理員授權(quán)";
    }
}

實(shí)際開發(fā)中,應(yīng)該對(duì)返回結(jié)果統(tǒng)一化,并給出業(yè)務(wù)錯(cuò)誤碼。這已經(jīng)超出了本文的范疇,如有需要,請(qǐng)根據(jù)自身系統(tǒng)特點(diǎn)考量。

進(jìn)行測(cè)試

認(rèn)證

登錄成功的情況

Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理

用戶名或密碼錯(cuò)誤的情況

Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理

為了安全起見,不要暴露具體是用戶名錯(cuò)誤還是密碼錯(cuò)誤。

訪問受保護(hù)資源

認(rèn)證后訪問有權(quán)限的資源

Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理

認(rèn)證后訪問無權(quán)限的資源

Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理

未認(rèn)證直接訪問的情況

Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理

查看redis

Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理

三個(gè)鍵值分別對(duì)應(yīng)認(rèn)證信息緩存、授權(quán)信息緩存和會(huì)話信息緩存。

以上就是“Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI