国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

SpringBoot+Shiro框架整合實現前后端分離的權限管理基礎Demo

這篇具有很好參考價值的文章主要介紹了SpringBoot+Shiro框架整合實現前后端分離的權限管理基礎Demo。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

記錄一下使用SpringBoot集成Shiro框架實現前后端分離Web項目的過程,后端使用SpringBoot整合Shiro,前端使用vue+elementUI,達到前后端使用token來進行交互的應用,這種方式通常叫做無狀態(tài),后端只需要使用Shiro框架根據前端傳來的token信息授權訪問相應資源。

案例源碼:SpringBoot+Shiro框架整合實現前后端分離的權限管理基礎Demo

首先新建SpringBoot項目,導入Springboot整合shiro所需要的依賴包

<!-- SpringBoot整合shiro所需相關依賴-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.10.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.10.0</version>
</dependency>

<!--web模塊的啟動器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

使用的SpringBoot版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

使用SpringBoot集合Shiro之前,需要建立相應的類和從數據庫獲取的用戶數據(這里新建一個java靜態(tài)類來模擬解決)

用戶登錄的類UserValidate.java

package boot.example.shiro.domain;

/**
 *  螞蟻舞
 */
public class UserValidate {

    String username;

    String password;

? ? // get  set
}

用戶類SysUsers.java

package boot.example.shiro.domain;

/**
 *  螞蟻舞
 */
public class SysUsers {

    private Integer user_id;

    private String username;

    private String password;

    private int user_type; // 用戶類型  -1表示超級賬號  1表示普通賬號

    private Integer role_id; // 用戶角色  拿權限需要的

    private Integer locked;  // 用戶狀態(tài)  1-正常  2=鎖定

    public SysUsers() {
    }

    public SysUsers(Integer user_id, String username, String password, int user_type, Integer role_id, Integer locked) {
        this.user_id = user_id;
        this.username = username;
        this.password = password;
        this.user_type = user_type;
        this.role_id = role_id;
        this.locked = locked;
    }

? ? // get set
}

模擬三個用戶shiro_admin, myw_admin, app_admin以及相關的方法和靜態(tài)mock數據

模擬數據庫的類ShiroDataMapper.java

package boot.example.shiro.config;

import boot.example.shiro.domain.SysUsers;

import java.util.ArrayList;
import java.util.List;

/**
 *  螞蟻舞
 */
public class ShiroDataMapper {

    private static final String shiro_admin = "shiro_admin";

    private static final String myw_admin = "myw_admin";

    private static final String app_admin = "app_admin";

    private static final SysUsers sysUsers_shiro_admin = new SysUsers(1, shiro_admin, "123", -1, 1, 1);

    private static final SysUsers sysUsers_myw_admin = new SysUsers(2, myw_admin, "1234", 1, 2, 1);

    private static final SysUsers sysUsers_app_admin = new SysUsers(3, app_admin, "12345",3, 3, 1);

    public static SysUsers getSysUsersByUserName(String username){
        if(username.equalsIgnoreCase(shiro_admin)){
            return sysUsers_shiro_admin;
        }
        if(username.equalsIgnoreCase(myw_admin)){
            return sysUsers_myw_admin;
        }
        if(username.equalsIgnoreCase(app_admin)){
            return sysUsers_app_admin;
        }
        return null;
    }

    public static List<String> listSysRolesPermissions(Integer roleId){
        if(roleId == 2){
            List<String> list = new ArrayList<>();
            list.add("sys:user:list");
            list.add("sys:user:update");
            list.add("sys:user:add");
            list.add("sys:user:delete");
            return list;
        }
        if(roleId == 3){
            List<String> list = new ArrayList<>();
            list.add("sys:user:list");
            return list;
        }
        return null;
    }


}

getSysUsersByUserName方法是用來模擬從數據庫獲取用戶對象數據的,listSysRolesPermissions是根據用戶的角色來獲取對應的權限列表的。

Shiro框架的ShiroRealm.java

shiro的realm主要用來實現認證(AuthenticationInfo)和授權(AuthorizationInfo)

package boot.example.shiro.config;

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class ShiroRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
? ? ? ? // to do
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
? ? ? ? // to do
    }
}

認證的實現,當用戶通過接口登錄后就會觸發(fā)這里的認證登錄

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //  獲取登錄username
    String username = (String)token.getPrincipal();
    //  從數據庫獲取用戶對象 (這里模擬的)
    SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(username);
    // 在數據庫里沒找到用戶,異常用戶,拋出異常(交給異常處理)
    if(sysUsers == null) {
        throw new UnknownAccountException();    //沒找到帳號
    }
    // 一般用戶允不允許登錄也是有一個鎖定狀態(tài)的 從用戶對象里拿到鎖定狀態(tài),判斷是否鎖定
    if(2 == sysUsers.getLocked()) {
        throw new LockedAccountException();     //帳號鎖定
    }
    //  交給SimpleAuthenticationInfo去驗證密碼
    return new SimpleAuthenticationInfo(sysUsers, sysUsers.getPassword(), this.getClass().getName());
}

授權實現,給超級管理所有權限,給具體的普通用戶對應的權限

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 獲取用戶對象
    SysUsers user = (SysUsers)principals.getPrimaryPrincipal();
    // 對象為null 拋出異常
    if(user == null){
        throw new UnknownAccountException();
    }
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(user.getUsername());
    if(sysUsers == null){
        throw new UnknownAccountException();
    }
    // // 用戶類型  -1表示超級賬號  1表示普通賬號
    if(sysUsers.getUser_type() < 0){
        authorizationInfo.addRole("*");  // roles的權限 所有
        authorizationInfo.addStringPermission("*:*:*"); // perms的權限 所有
    } else {
        // 用角色id從數據庫獲取權限列表,這里是模擬的
        List<String> mapList = ShiroDataMapper.listSysRolesPermissions(sysUsers.getRole_id());
        authorizationInfo.addRole("key");
        if (!mapList.isEmpty()) {
            Set<String> permsSet = new HashSet<>();
            for (String perm : mapList) {
                permsSet.addAll(Arrays.asList(perm.trim().split(",")));
            }
            authorizationInfo.setStringPermissions(permsSet);
        }
    }
    return authorizationInfo;
}

ShiroRealm.java完整代碼

package boot.example.shiro.config;


import boot.example.shiro.domain.SysUsers;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 螞蟻舞
 */
public class ShiroRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 獲取用戶對象
        SysUsers user = (SysUsers)principals.getPrimaryPrincipal();
        // 對象為null 拋出異常
        if(user == null){
            throw new UnknownAccountException();
        }
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(user.getUsername());
        if(sysUsers == null){
            throw new UnknownAccountException();
        }
        // // 用戶類型  -1表示超級賬號  1表示普通賬號
        if(sysUsers.getUser_type() < 0){
            authorizationInfo.addRole("*");  // roles的權限 所有
            authorizationInfo.addStringPermission("*:*:*"); // perms的權限 所有
        } else {
            // 用角色id從數據庫獲取權限列表,這里是模擬的
            List<String> mapList = ShiroDataMapper.listSysRolesPermissions(sysUsers.getRole_id());
            authorizationInfo.addRole("key");
            if (!mapList.isEmpty()) {
                Set<String> permsSet = new HashSet<>();
                for (String perm : mapList) {
                    permsSet.addAll(Arrays.asList(perm.trim().split(",")));
                }
                authorizationInfo.setStringPermissions(permsSet);
            }
        }
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //  獲取登錄username
        String username = (String)token.getPrincipal();
        //  從數據庫獲取用戶對象 (這里模擬的)
        SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(username);
        // 在數據庫里沒找到用戶,異常用戶,拋出異常(交給異常處理)
        if(sysUsers == null) {
            throw new UnknownAccountException();    //沒找到帳號
        }
        // 一般用戶允不允許登錄也是有一個鎖定狀態(tài)的 從用戶對象里拿到鎖定狀態(tài),判斷是否鎖定
        if(2 == sysUsers.getLocked()) {
            throw new LockedAccountException();     //帳號鎖定
        }
        //  交給SimpleAuthenticationInfo去驗證密碼
        return new SimpleAuthenticationInfo(sysUsers, sysUsers.getPassword(), this.getClass().getName());
    }



}

Shiro框架的ShiroConfig.java

SpringBoot集成Shiro有一個最主要的配置類,這個類里有Shiro框架的會話管理(SessionManager)和安全管理(SecurityManager)和訪問過濾器(ShiroFilterFactoryBean)和SpringBoot注解支持和生命周期相關的Bean配置

@Configuration必須加上的!

@Configuration
public class ShiroConfig {

}

ShiroConfig里首先來配置密碼校驗的bean

// 密碼校驗bean
@Bean("credentialMatcher")
public ShiroCredentialMatcher credentialMatcher() {
    return new ShiroCredentialMatcher();
}

密碼校驗繼承類ShiroCredentialMatcher.java

這里繼承了SimpleCredentialsMatcher 實現方式是將登錄的密碼和數據庫查詢出來的密碼進行一個equals對比,使用這種方式,密碼可以是明碼進行對比,也可以MD5后的密碼,同樣的登錄密碼和數據庫內的密碼也可以在這里分別經過各自某種加密解密后在對比(安全系數瞬間增強,即使從數據庫拿到了密碼也沒法簡單確認出登錄密碼)

package boot.example.shiro.config;


import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

/**
 *  螞蟻舞
 */
public class ShiroCredentialMatcher extends SimpleCredentialsMatcher {

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String password = new String(usernamePasswordToken.getPassword());
        String dbPassword = (String) info.getCredentials();
        System.out.println("usernamePasswordToken--"+usernamePasswordToken.getUsername()+"--"+password);
        System.out.println("info.getCredentials()-"+info.getCredentials()+"---info.getPrincipals()-"+info.getPrincipals());
        // 密碼比對
        return this.equals(password, dbPassword);
    }
}

SimpleCredentialsMatcher的源碼

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

身份認證和權限校驗Realm的bean

ShiroRealm就是授權和認證的類,設置的緩存管理使用的是內存,setCredentialsMatcher就是密碼校驗,MemoryConstrainedCacheManager緩存在內存中(方便快捷)

// 身份認證和權限校驗Realm
@Bean("shiroRealm")
public ShiroRealm shiroRealm(@Qualifier("credentialMatcher") ShiroCredentialMatcher matcher){
    ShiroRealm shiroRealm = new ShiroRealm();
    shiroRealm.setCacheManager(new MemoryConstrainedCacheManager());
    shiroRealm.setCredentialsMatcher(matcher);
    return shiroRealm;
}

SessionManager會話管理

shiro的會話管理SessionManager是用來管理應用中所有 Subject 的會話的創(chuàng)建、維護、刪除、失效、驗證,有三個默認的實現類

DefaultSessionManager

DefaultWebSessionManager:用于web環(huán)境的實現

ServletContainerSessionManager

shiro默認的會話管理是依賴于瀏覽器的cookie來維持的,也就是說前端代碼嵌入到了SpringBoot整合Shiro的環(huán)境中,Shiro的會話管理將sesionId 放到 cookie中,現在大多數項目都是前后端分離的,去拿cookie還不如用token機制,一種無狀態(tài)的機制,在登錄的時候獲取的token實際上就是shiro的sessionId,如此的話,那么可以繼承實現DefaultWebSessionManager類,修改一些需要改變的方法

//  會話管理, 管理用戶登錄后的會話
@Bean("sessionManager")
public ShiroSessionManager sessionManager(){
    //將繼承后重寫的ShiroSessionManager加入bean
    return new ShiroSessionManager();
}

token的靜態(tài)類ShiroConstant.java

package boot.example.shiro.config;
/**
 * 螞蟻舞
 */
public class ShiroConstant {
    //  定義的請求頭中使用的標記key,用來傳遞 token
    public static final String authorization_token = "token";
}

重寫會話管理類ShiroSessionManager.java

package boot.example.shiro.config;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * 螞蟻舞
 */
public class ShiroSessionManager extends DefaultWebSessionManager {

    public ShiroSessionManager() {
        super();
        //在這里設置ShiroSession失效時間
        setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15);
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //獲取請求頭中的token值,如果請求頭中有token值,則取巧認為其值為會話的sessionId(那么用戶在登陸的時候需要給前端傳送這個sessionId)
        String sessionId = WebUtils.toHttp(request).getHeader(ShiroConstant.authorization_token);
        System.out.println("sessionId--" + sessionId);
        if (StringUtils.isEmpty(sessionId)){
            /**
             * 注意: 在這里有一種特殊情況,那就是不經過shiroFilter過濾器的訪問,例如authc認證用戶
             * 既然不經過shiroFilter 那么當后端重啟清空了會話,可前端依舊把sessionId傳給了后端,
             * 出現這種情況,shiro會按照shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");設置跳轉到登錄頁面,重新登陸
             * 格式是http://127.0.0.1:20400/shiro-redirect/index;JSESSIONID=04d5ed45-85c1-420b-b7bd-fa622385309f
             * 如果是沒有分離的項目,那么直接跳轉到了登錄頁,如果是分離的項目,那就會給前端報出400的錯誤(這里是整合需要注意的關鍵點)
             */

            //如果沒有攜帶sessionId的參數,直接按照父類的方式在cookie進行獲取sessionId
            return super.getSessionId(request, response);
        } else {
            //請求頭中如果有token, 則其值為sessionId(登陸的時候就傳送這個sessionId)
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "request cookie");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); // 這里加上sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        }
    }

}

看看DefaultWebSessionManager父類getSessionId的源碼

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

調用了私有getReferencedSessionId方法

先調用this.getSessionIdCookieValue(request, response)獲取sessionId 如果sessionid不存在,則去判斷JSESSIONID的參數是不是帶有(這個在前后端分離的項目有個大坑,不經過shiroFilter里的訪問,接口會報出400錯誤,ShiroSessionManager的demo代碼里有說明),暫時不去分析那么多,前后端分離一般也不會用到類似authc認證用戶訪問的,一般都是接口訪問,有shiroFilter過濾器。


private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
    String id = this.getSessionIdCookieValue(request, response);
    if (id != null) {
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie");
    } else {
        id = this.getUriPathSegmentParamValue(request, "JSESSIONID");
        if (id == null && request instanceof HttpServletRequest) {
            String name = this.getSessionIdName();
            HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
            String queryString = httpServletRequest.getQueryString();
            if (queryString != null && queryString.contains(name)) {
                id = request.getParameter(name);
            }

            if (id == null && queryString != null && queryString.contains(name.toLowerCase())) {
                id = request.getParameter(name.toLowerCase());
            }
        }

        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url");
        }
    }

    if (id != null) {
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
    }

    request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, this.isSessionIdUrlRewritingEnabled());
    return id;
}

SecurityManager安全管理器 Shiro框架的核心組件

//  安全管理器
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
    // web的安全管理器
    DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
    // 設置授權和認證
    manager.setRealm(shiroRealm);
    // 設置會話管理
    manager.setSessionManager(sessionManager());
    return manager;
}

ShiroFilterFactoryBean訪問過濾器(經常說成是攔截器,實際上是攔截的功能)

//  訪問shiro的過濾器
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {

    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    Map<String, Filter> filterMap = new HashMap<>();
    filterMap.put("shiroFilter", new ShiroFilter());
    shiroFilterFactoryBean.setFilters(filterMap);
    // 跳轉到登錄頁,實際跳轉后訪問的是接口,接口返回請登錄的信息
    shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");
    //bean.setSuccessUrl("/shiro-redirect/index");
    //  實際跳轉到未認證頁面,請重新登陸
    shiroFilterFactoryBean.setUnauthorizedUrl("/shiro-redirect/unauthorized");

    LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

    //  靜態(tài)路徑放開  anon:匿名用戶可訪問
    filterChainDefinitionMap.put("/public/**", "anon");
    filterChainDefinitionMap.put("/static/**", "anon");

    //  調試工具全部放開    anon:匿名用戶可訪問
    filterChainDefinitionMap.put("/swagger-resources", "anon");
    filterChainDefinitionMap.put("/swagger-resources/**", "anon");
    filterChainDefinitionMap.put("/v2/api-docs", "anon");
    filterChainDefinitionMap.put("/webjars/**", "anon");
    filterChainDefinitionMap.put("/doc.html", "anon");

    // 登錄相關全部放開 anon:匿名用戶可訪問
    filterChainDefinitionMap.put("/shiro-login/**", "anon");
    filterChainDefinitionMap.put("/shiro-redirect/**", "anon");

    // 匿名用戶可訪問  anon:匿名用戶可訪問
    filterChainDefinitionMap.put("/shiro-anon/**", "anon");

    //  認證用戶可訪問 authc:認證用戶可訪問
    filterChainDefinitionMap.put("/shiro-authc/*", "authc");

    // 自定義過濾器過濾的內容
    filterChainDefinitionMap.put("/**", "shiroFilter");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

    return shiroFilterFactoryBean;
}

自定義過濾的類ShiroFilter.java

package boot.example.shiro.config;

import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 *  螞蟻舞
 */
public class ShiroFilter extends BasicHttpAuthenticationFilter {

    //  sendChallenge重寫的目的是避免前端在沒有登錄的情況下訪問@RequiresPermissions()等未授權接口返回401錯誤,
    //  給前端調用接口一個數據,讓前端去重新登陸
    //  如果使用瀏覽器訪問,瀏覽器會彈出一個輸入賬號密碼的彈框,重寫后瀏覽器訪問出現接口數據
    protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
        System.out.println("Authentication required: sending 401 Authentication challenge response.");
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        responseSkip(httpResponse, ResponseCode.noLoginSkipResponse());
        return false;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域時會首先發(fā)送一個option請求,這里我們給option請求直接返回正常狀態(tài)
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        //  在配置的ShiroFilterFactoryBean攔截過濾器里,必須使用無狀態(tài)的token 這里如果沒有token 直接告訴前端需要重新登陸
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader(ShiroConstant.authorization_token);
        if(authorization == null || authorization.length() == 0){
            //  未攜帶token  不需要提示前端自動跳轉重新登陸
            responseSkip(httpServletResponse, ResponseCode.noAuthHeaderTokenResponse("未攜帶token,請求無效"));
            return false;
        }

        //  驗證token的正確性
        Subject subject = SecurityUtils.getSubject();
        if(!subject.isAuthenticated()){
            //   token失效 提示前端需要自動跳轉重新登陸
            responseSkip(httpServletResponse, ResponseCode.invalidHeaderTokenSkipResponse());
            return false;
        }

        return super.preHandle(request, response);
    }

    private void responseSkip(HttpServletResponse response, Response customizeResponse){
        try {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            ObjectMapper objectMapper = new ObjectMapper();
            String str = objectMapper.writeValueAsString(customizeResponse);
            response.getWriter().println(str);
        } catch (IOException e1) {
            throw new RuntimeException(e1);
        }
    }


}

注解支持的bean配置

支持在SpringBoot在Controller使用@RequiresPermission()等標簽注解以及配置shiro的生命周期

//  支持在SpringBoot的Controller使用@RequiresPermission()等標簽注解 以及
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    return advisor;
}

@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    // 強制使用cglib,防止重復代理和可能引起代理出錯的問題 (沒明白)
    defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
    return defaultAdvisorAutoProxyCreator;
}

//  配置shiro的生命周期處理
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
}

ShiroConfig.java完整類

package boot.example.shiro.config;


import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 螞蟻舞
 */
@Configuration
public class ShiroConfig {

    // 密碼校驗bean
    @Bean("credentialMatcher")
    public ShiroCredentialMatcher credentialMatcher() {
        return new ShiroCredentialMatcher();
    }

    // 身份認證和權限校驗Realm
    @Bean("shiroRealm")
    public ShiroRealm shiroRealm(@Qualifier("credentialMatcher") ShiroCredentialMatcher matcher){
        ShiroRealm shiroRealm = new ShiroRealm();
        shiroRealm.setCacheManager(new MemoryConstrainedCacheManager());
        shiroRealm.setCredentialsMatcher(matcher);
        return shiroRealm;
    }

    //  會話管理, 管理用戶登錄后的會話
    @Bean("sessionManager")
    public ShiroSessionManager sessionManager(){
        //將繼承后重寫的ShiroSessionManager加入bean
        return new ShiroSessionManager();
    }

    //  安全管理器
    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
        // web的安全管理器
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 設置授權和認證
        manager.setRealm(shiroRealm);
        // 設置會話管理
        manager.setSessionManager(sessionManager());
        return manager;
    }

    //  訪問shiro的過濾器
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("shiroFilter", new ShiroFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        // 跳轉到登錄頁,實際跳轉后訪問的是接口,接口返回請登錄的信息
        shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");
        //bean.setSuccessUrl("/shiro-redirect/index");
        //  實際跳轉到未認證頁面,請重新登陸
        shiroFilterFactoryBean.setUnauthorizedUrl("/shiro-redirect/unauthorized");

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        //  靜態(tài)路徑放開  anon:匿名用戶可訪問
        filterChainDefinitionMap.put("/public/**", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");

        //  調試工具全部放開    anon:匿名用戶可訪問
        filterChainDefinitionMap.put("/swagger-resources", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/doc.html", "anon");

        // 登錄相關全部放開 anon:匿名用戶可訪問
        filterChainDefinitionMap.put("/shiro-login/**", "anon");
        filterChainDefinitionMap.put("/shiro-redirect/**", "anon");

        // 匿名用戶可訪問  anon:匿名用戶可訪問
        filterChainDefinitionMap.put("/shiro-anon/**", "anon");

        //  認證用戶可訪問 authc:認證用戶可訪問
        filterChainDefinitionMap.put("/shiro-authc/*", "authc");

        // 自定義過濾器過濾的內容
        filterChainDefinitionMap.put("/**", "shiroFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }



    //  支持在SpringBoot的Controller使用@RequiresPermission()等標簽注解 以及
    @Bean("authorizationAttributeSourceAdvisor")
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 強制使用cglib,防止重復代理和可能引起代理出錯的問題 (沒明白)
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    //  配置shiro的生命周期處理
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

}

SpringBoot整合Shiro的web應用需要Controller層來調用測試功能的

首先是ShiroConfig里設置的重定向類

BootShiroIndexRedirectController.java

package boot.example.shiro.controller;


import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 *  螞蟻舞
 */
@Controller
@RequestMapping("/shiro-redirect")
public class BootShiroIndexRedirectController {

    public Logger log = LoggerFactory.getLogger(this.getClass());

    @RequestMapping("/index")
    @ResponseBody
    public Response index() {
        log.warn("redirect index");
        return ResponseCode.noLoginResponse();
    }

    @RequestMapping("/unauthorized")
    @ResponseBody
    public Response unauthorized() {
        log.warn("redirect unauthorized");
        return ResponseCode.unauthorizedPermissionResponse();
    }


}

匿名游客訪問類BootShiroTestAnonController.java

package boot.example.shiro.controller;

import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 螞蟻舞
 */
@RestController
@RequestMapping(value="/shiro-anon")
public class BootShiroTestAnonController {

    @GetMapping(value="/hello")
    public Response anonHello() {
        return ResponseCode.successResponse("匿名游客用戶可訪問");
    }
}

已經認證也就是登錄的用戶訪問類BootShiroTestAuthcController.java

package boot.example.shiro.controller;

import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value="/shiro-authc")
public class BootShiroTestAuthcController {

    @GetMapping(value="/hello")
    public Response authCHello() {
        return ResponseCode.successResponse("你是認證用戶,可訪問此接口");
    }
}

使用權限注解的類BootShiroTestSysUserController.java

package boot.example.shiro.controller;

import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;

/**
 *  螞蟻舞
 */
@RestController
@RequestMapping(value="/sysUser")
public class BootShiroTestSysUserController {

    @GetMapping(value="/hello")
    public Response shiroFilterHello() {
        return ResponseCode.successResponse("你正在訪問登錄后shiroFilter過濾器里的,無注解的接口");
    }

    @RequiresPermissions("sys:user:list")
    @GetMapping(value="/list")
    @ResponseBody
    public Response userList() {
        return ResponseCode.successResponse("你已經成功訪問到查詢用戶接口");
    }

    @RequiresPermissions("sys:user:add")
    @GetMapping(value="/insert")
    @ResponseBody
    public Response userAdd() {
        return ResponseCode.successResponse("你已經成功訪問到新增用戶接口");
    }

    @RequiresPermissions("sys:user:update")
    @GetMapping(value="/update")
    @ResponseBody
    public Response userUpdate() {
        return ResponseCode.successResponse("你已經成功訪問到更新用戶接口");
    }

    @RequiresPermissions("sys:user:delete")
    @GetMapping(value="/delete")
    @ResponseBody
    public Response userDelete() {
        return ResponseCode.successResponse("你已經成功訪問到刪除用戶接口");
    }


}

登出類BootShiroLogoutController.java

package boot.example.shiro.controller;


import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 *  螞蟻舞
 */
@Controller
@RequestMapping("/shiro-logout")
public class BootShiroLogoutController {

    @GetMapping(value="/logout")
    @ResponseBody
    public Response logoutGet() {
        Subject subject = SecurityUtils.getSubject();
        if(subject != null){
            subject.logout();
            return ResponseCode.successResponse("登出成功");
        }
        return ResponseCode.failResponse("登出失敗");
    }

    @PostMapping(value="/logout")
    @ResponseBody
    public Response logoutPost() {
        Subject subject = SecurityUtils.getSubject();
        if(subject != null){
            subject.logout();
            return ResponseCode.successResponse("登出成功");
        }
        return ResponseCode.failResponse("登出失敗");
    }


}

登錄使用的類BootShiroLoginController.java

package boot.example.shiro.controller;

import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import boot.example.shiro.domain.SysUsers;
import boot.example.shiro.domain.UserValidate;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;

/**
 *  螞蟻舞
 */
@Controller
@RequestMapping("/shiro-login")
public class BootShiroLoginController {

    @GetMapping(value="/auth")
    @ResponseBody
    public Response authGet(@RequestParam(value = "username", required = true, defaultValue="shiro_admin") String username, @RequestParam(value = "password", required = true, defaultValue="123") String password) {
        UserValidate userValidate = new UserValidate();
        userValidate.setPassword(password);
        userValidate.setUsername(username);
        UsernamePasswordToken token = new UsernamePasswordToken(userValidate.getUsername(), userValidate.getPassword());
        Subject subject = SecurityUtils.getSubject();

        try {
            subject.login(token);
            SysUsers sysUsers = (SysUsers) subject.getPrincipal();
            Map<String, Object> map = new HashMap<>();
            map.put("token", subject.getSession().getId().toString());
            map.put("session", subject.getSession());
            map.put("sysUsers", sysUsers);
            return ResponseCode.successResponse(map);
        } catch ( UnknownAccountException uae ) {
            return ResponseCode.failResponse("error username");
        } catch ( IncorrectCredentialsException ice ) {
            return ResponseCode.failResponse("error password");
        } catch ( LockedAccountException lae ) {
            return ResponseCode.failResponse("locked user");
        }
    }

    @PostMapping(value="/auth")
    @ResponseBody
    public Response authPost(@RequestBody UserValidate userValidate, HttpSession session) {
        System.out.println(userValidate.toString());
        UsernamePasswordToken token = new UsernamePasswordToken(userValidate.getUsername(), userValidate.getPassword());
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            SysUsers sysUsers = (SysUsers) subject.getPrincipal();
            Map<String, Object> map = new HashMap<>();
            map.put("token", subject.getSession().getId().toString());
            map.put("session", subject.getSession());
            map.put("sysUsers", sysUsers);
            return ResponseCode.successResponse(map);
        } catch ( UnknownAccountException uae ) {
            return ResponseCode.failResponse("error username");
        } catch ( IncorrectCredentialsException ice ) {
            return ResponseCode.failResponse("error password");
        } catch ( LockedAccountException lae ) {
            return ResponseCode.failResponse("locked user");
        }
    }
}

SpringBoot整合Shiro的代碼里有拋出異常的情況,主要的異常在登錄的時候會在try catch里處理,返回給前端,但還是有些異常是捕獲不到的,因此需要加上異常處理

import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;

// shiro 未授權異常
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public Response UnauthorizedExceptionHandler(HttpServletRequest request, UnauthorizedException e) {
    log.error(request.getRequestURI()+"----"+e.toString());
    return ResponseCode.unauthorizedPermissionResponse("未授權,您的操作權限不夠,可聯系管理員獲取操作權限");
}

//  shiro 授權異常
@ExceptionHandler(AuthorizationException.class)
@ResponseBody
public Response AuthorizationException(HttpServletRequest request, AuthorizationException e) {
    log.error(request.getRequestURI()+"----"+e.toString());
    return ResponseCode.failResponse( "授權用戶不存在或已經過期,請重新登錄");
}

//  shiro 未經身份驗證或身份驗證異常
@ExceptionHandler(UnauthenticatedException.class)
@ResponseBody
public Response UnauthenticatedException(HttpServletRequest request, UnauthenticatedException e) {
    log.error(request.getRequestURI()+"----"+e.toString());
    return ResponseCode.failResponse("未經身份驗證,身份驗證異常,請重新登錄");
}

//  shiro 賬號鎖定異常
@ExceptionHandler(LockedAccountException.class)
@ResponseBody
public Response LockedAccountException(HttpServletRequest request, LockedAccountException e) {
    log.error(request.getRequestURI()+"----"+e.toString());
    return ResponseCode.failResponse("你的賬號已鎖定,請聯系管理員解鎖");
}

//  shiro 未找到用戶異常
@ExceptionHandler(UnknownAccountException.class)
@ResponseBody
public Response UnknownAccountException(HttpServletRequest request, UnknownAccountException e) {
    log.error(request.getRequestURI()+"----"+e.toString());
    return ResponseCode.failResponse("你的賬號不存在");
}

//  shiro 登錄用戶密碼校驗異常
@ExceptionHandler(IncorrectCredentialsException.class)
@ResponseBody
public Response IncorrectCredentialsException(HttpServletRequest request, IncorrectCredentialsException e) {
    log.error(request.getRequestURI()+"----"+e.toString());
    return ResponseCode.failResponse("你輸入的密碼錯誤");
}

完整的異常處理類GlobalExceptionHandler.java

package boot.example.shiro.config;


import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.authz.permission.InvalidPermissionStringException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;


/**
 * 螞蟻舞
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    public Logger log = LoggerFactory.getLogger(this.getClass());

    //  全局異常:默認異常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Response defaultExceptionHandler(HttpServletRequest request, Exception e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.exceptionResponse(request.getRequestURI()+e.toString());
    }

    @ExceptionHandler(BindException.class)
    @ResponseBody
    public Response bindExceptionHandler(HttpServletRequest request, BindException e) {
        return ResponseCode.exceptionResponse(e.toString());
    }


    //  全局異常:請求header缺少HeaderToken
    @ExceptionHandler(ServletRequestBindingException.class)
    @ResponseBody
    public Response ServletRequestBindingExceptionHandler(HttpServletRequest request, ServletRequestBindingException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.noAuthHeaderTokenResponse();
    }

    //  全局異常:請求內容類型異常
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    @ResponseBody
    public Response HttpMediaTypeNotSupportedExceptionHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.exceptionResponse(e.toString());
    }

    //  全局異常:請求方法異常
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseBody
    public Response HttpRequestMethodNotSupportedExceptionHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException e) {
        log.error(request.getRequestURI() +"----"+e.toString());
        return ResponseCode.exceptionResponse(e.toString());
    }

    //  全局異常:請求參數格式或者參數類型不正確異常
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseBody
    public Response HttpMessageNotReadableExceptionHandler(HttpServletRequest request, HttpMessageNotReadableException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.exceptionResponse(e.toString());
    }

    //  shiro 權限不可用
    @ExceptionHandler(InvalidPermissionStringException.class)
    @ResponseBody
    public Response InvalidPermissionStringException(HttpServletRequest request, IncorrectCredentialsException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.notPermissionResponse("你的權限不可用");
    }

    // shiro 未授權異常
    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public Response UnauthorizedExceptionHandler(HttpServletRequest request, UnauthorizedException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.unauthorizedPermissionResponse("未授權,您的操作權限不夠,可聯系管理員獲取操作權限");
    }

    //  shiro 授權異常
    @ExceptionHandler(AuthorizationException.class)
    @ResponseBody
    public Response AuthorizationException(HttpServletRequest request, AuthorizationException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.failResponse( "授權用戶不存在或已經過期,請重新登錄");
    }

    //  shiro 未經身份驗證或身份驗證異常
    @ExceptionHandler(UnauthenticatedException.class)
    @ResponseBody
    public Response UnauthenticatedException(HttpServletRequest request, UnauthenticatedException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.failResponse("未經身份驗證,身份驗證異常,請重新登錄");
    }

    //  shiro 賬號鎖定異常
    @ExceptionHandler(LockedAccountException.class)
    @ResponseBody
    public Response LockedAccountException(HttpServletRequest request, LockedAccountException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.failResponse("你的賬號已鎖定,請聯系管理員解鎖");
    }

    //  shiro 未找到用戶異常
    @ExceptionHandler(UnknownAccountException.class)
    @ResponseBody
    public Response UnknownAccountException(HttpServletRequest request, UnknownAccountException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.failResponse("你的賬號不存在");
    }

    //  shiro 登錄用戶密碼校驗異常
    @ExceptionHandler(IncorrectCredentialsException.class)
    @ResponseBody
    public Response IncorrectCredentialsException(HttpServletRequest request, IncorrectCredentialsException e) {
        log.error(request.getRequestURI()+"----"+e.toString());
        return ResponseCode.failResponse("你輸入的密碼錯誤");
    }



}

跨域支持的BeanConfig.java

package boot.example.shiro.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 *  螞蟻舞
 */
@Configuration
public class BeanConfig {
    @Bean
    public CorsFilter corsFilter(){
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("PUT");
        corsConfiguration.addAllowedMethod("GET");
        corsConfiguration.addAllowedMethod("POST");
        corsConfiguration.addAllowedMethod("PATCH");
        corsConfiguration.addAllowedMethod("OPTIONS");
        corsConfiguration.addAllowedMethod("DELETE");
        corsConfiguration.setMaxAge(1728000L);
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }


}

Response和靜態(tài)類ResponseCode是統一封裝的result結果集

public class Response {

    private boolean state;

    private int code;

    private String msg;

    private Object data;

    private long timestamp;

? ? // get set
}

完整的SpringBoot整合Shiro的代碼結構

└─boot-example-shiro-separate-2.0.5
│ pom.xml
│
├─doc
│ boot-example-shiro-separate-2.0.5-back.zip
│
└─src
├─main
│ ├─java
│ │ └─boot
│ │ └─example
│ │ └─shiro
│ │ │ ShiroApp.java
│ │ │
│ │ ├─config
│ │ │ BeanConfig.java
│ │ │ GlobalExceptionHandler.java
│ │ │ ShiroConfig.java
│ │ │ ShiroConstant.java
│ │ │ ShiroCredentialMatcher.java
│ │ │ ShiroDataMapper.java
│ │ │ ShiroFilter.java
│ │ │ ShiroRealm.java
│ │ │ ShiroSessionManager.java
│ │ │ SwaggerConfig.java
│ │ │
│ │ ├─controller
│ │ │ BootShiroIndexRedirectController.java
│ │ │ BootShiroLoginController.java
│ │ │ BootShiroLogoutController.java
│ │ │ BootShiroTestAnonController.java
│ │ │ BootShiroTestAuthcController.java
│ │ │ BootShiroTestSysUserController.java
│ │ │
│ │ └─domain
│ │ Response.java
│ │ ResponseCode.java
│ │ SysUsers.java
│ │ UserValidate.java
│ │
│ └─resources
│ application.properties
│ logback-spring.xml
│
└─test
└─java
└─boot
└─example
└─shiro
ShiroAppTest.java

啟動SpringBoot項目,訪問swagger-ui(實際前后端分離的情況,這種方式也適合將前端代碼嵌入到SpringBoot項目中)

http://localhost:20400/doc.html

瀏覽器和SwaggerUi測試

1.先不登錄,訪問匿名游客(anon)

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

2.先不登錄,訪問認證用戶可訪問(authc)這里有重定向

在瀏覽器直接訪問/shiro-authc/hello 因為沒有授權重定向到了 /shiro-redirect/index,但在接口上就看不到重定向操作了,直接得到數據未登錄的結果

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

3.登錄,使用預定的賬號訪問帶注解的接口

app_admin 12345
這個賬號登錄訪問的接口權限只有sys:user:list
也就是只能訪問@RequiresPermissions("sys:user:list")
shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

登錄信息里除了用戶信息還返回了token和session相關的信息,這個token就是前后端交互的

將token復制輸入到token框里面,能夠訪問@RequiresPermissions("sys:user:list")

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

將token復制輸入到token框里面,訪問@RequiresPermissions("sys:user:add")權限不夠

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

直接使用瀏覽器訪問@RequiresPermissions("sys:user:list")注解的接口,發(fā)現需要token,那是因為前后端交互用的就是token機制,無狀態(tài)的

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

4.當瀏覽器或swagger-ui上登錄后,后端SpringBoot項目重啟,訪問

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

可以看到登錄后使用瀏覽器直接輸入會跳轉,得到的確實是接口類型的數據,而不是顯示的某個頁面,但是在swagger-ui里,得到了JSESSIONID后面攜帶了token(sessionId)感覺像是沒有實現前后端分離,這里之前在ShiroSessionManager類提到了的,盡量將所有接口都放在shiroFilter過濾器里,就是不使用authc這些

將這個注釋掉
// 認證用戶可訪問 authc:認證用戶可訪問
filterChainDefinitionMap.put("/shiro-authc/*", "authc");

其他使用瀏覽器和SwaggerUI測試的就不截圖了(能避免的坑幾乎都避免了,比如shiro的彈窗登錄)

前后端交互要正真測試出能不能用,關鍵還是要使用獨立的前端代碼,這樣才能測試真正的效果,SpringBoot整合Shiro和Vue實現前后端分離web項目最常見,這里使用vue+elementui搭建的測試程序進行測試

使用Vue前后端分離的前端測試Demo

使用vue和elementui來簡單測試建立一個.vue文件就可以,核心代碼首先定義request.js

import axios from 'axios'
import { getToken} from '@/utils/cookies'
import { Notification } from 'element-ui'

const service = axios.create({
    baseURL: "http://127.0.0.1:20400",
    timeout: 60000,
    headers: {'Content-Type': 'application/json;charset=UTF-8'}
})

// 統一請求攔截器
service.interceptors.request.use(
    config => {
        if("/shiro-login/auth" === config.url){
            return config
        }

        // 把token給后端
        config.headers['token'] = getToken()
        if(getToken()){
            config.headers['token'] = getToken()
        } else {
            Notification({title: '消息',message: 'token失效,請重新登陸',type: 'warning',offset: 40})
            return
        }
        return config
    },
    error => {
        // 請求出錯
        console.log(error)
        return Promise.reject(error)
    }
)

// 統一響應攔截器
service.interceptors.response.use (
    response => {
        let res
        // IE9時response.data是undefined,因此需要使用response.request.responseText(Stringify后的字符串)
        if (response.data == undefined) {
            res = JSON.parse(response.request.responseText)
        } else {
            res = response.data
        }
        //console.log(res);

        //響應的邏輯判斷
        if(res){
            return res
        }
        return Promise.reject(new Error("請求錯誤" || 'Error'))
    },
    error => {
        //響應出錯
        console.log('err' + error)
        return Promise.reject(error)
    }
)

export default service

定義api接口shiroVue.js

import request from '@/axios/request'

// 登錄
export const login = (data) => {
    return request({
        url: '/shiro-login/auth',
        method: 'post',
        data
    })
}

// 登出
export const logout = () => {
    return request({
        url: '/shiro-logout/logout',
        method: 'post'
    })
}

// 匿名游客
export function anonHello() {
    return request({
        url: '/shiro-anon/hello',
        method: 'get'
    })
}

// 認證用戶
export function authcHello() {
    return request({
        url: '/shiro-authc/hello',
        method: 'get'
    })
}

// 查詢用戶
export function userList() {
    return request({
        url: '/sysUser/list',
        method: 'get'
    })
}

// 新增用戶
export function userInsert() {
    return request({
        url: '/sysUser/insert',
        method: 'get'
    })
}

// 更新用戶
export function userUpdate() {
    return request({
        url: '/sysUser/update',
        method: 'get'
    })
}

// 刪除用戶
export function userDelete() {
    return request({
        url: '/sysUser/delete',
        method: 'get'
    })
}

需要用到cookie cookies.js

/**
 *  token認證
 * 
 */
import Cookies from 'js-cookie'

const mywTokenKey = 'mywToken'

export function getToken() {
    return Cookies.get(mywTokenKey)
}
export function setToken(token) {
    return Cookies.set(mywTokenKey, token)
}
export function removeToken() {
    return Cookies.remove(mywTokenKey)
}

主要的測試代碼Home.vue

<template>

<div style="border-radius:4px;padding:4px;">
    <el-row style="padding-top:40px;">   
        <el-col :span="24">
            <div>SpringBoot+Shiro框架整合實現前后端分離的權限管理基礎Demo</div>
        </el-col>             
        <el-col :span="24" style="margin-top: 20px;">
            <el-input placeholder="用戶賬號" style="width:200px;margin-right:8px;" v-model="username" clearable></el-input>
            <el-input placeholder="用戶密碼" style="width:200px;margin-right:8px;" v-model="password" clearable></el-input>
        </el-col>
        <el-col :span="24" style="margin-top: 20px;">
            <el-button type="info" @click="handleLogin()">登錄系統</el-button>
            <div style="height:4px;">{{ resultLogin }}</div>            
        </el-col>
        <el-col :span="24" style="margin-top: 20px;">
            <el-button @click="handleLoGout()">登出系統</el-button>
            <div style="height:4px;">{{ resultLogout }}</div>
        </el-col>
        <el-col :span="24" style="margin-top: 20px;">
            <el-button @click="handleanonHello()">匿名游客</el-button>
            <div style="height:4px;">{{ resultAnonHello }}</div>
        </el-col>
        <el-col :span="24" style="margin-top: 20px;">
            <el-button type="info" @click="handleuserList()">查詢用戶</el-button>
            <div style="height:4px;">{{ resultuserList }}</div>
        </el-col>
        <el-col :span="24" style="margin-top: 20px;">
            <el-button type="warning" @click="handleuserInsert()">新增用戶</el-button>  
            <div style="height:4px;">{{ resultuserInsert }}</div>
        </el-col>
        <el-col :span="24" style="margin-top: 20px;">
            <el-button @click="handleuserUpdate()" type="success">編輯用戶</el-button>
            <div style="height:4px;">{{ resultuserUpdate }}</div>
        </el-col>
        <el-col :span="24" style="margin-top: 20px;">
            <el-button @click="handleuserDelete()">刪除用戶</el-button>
            <div style="height:4px;">{{ resultuserDelete }}</div>
        </el-col>
        
        <el-col :span="24" style="margin-top: 30px;">
            <el-button type="info" @click="handleauthcHello()">認證訪問(特殊)</el-button>
            <div style="height:4px;">{{ resultAuthcHello }}</div>
        </el-col>        
    </el-row>    
</div>
</template>

<script>
import { login, logout, anonHello, authcHello, userList, userInsert, userUpdate, userDelete} from '@/api/modules/shiroVue'
import {setToken, removeToken } from '../utils/cookies'
export default {
    name: 'Home',
    data() {
        return {
            username: "shiro_admin",
            password: "123",
            resultLogin: "",
            resultLogout: "",
            resultAnonHello: "",
            resultAuthcHello: "",
            resultuserList: "",
            resultuserInsert: "",
            resultuserUpdate: "",
            resultuserDelete: ""
        }
    },
    created() {
    },
    methods: {
        init() {
        },
        handleLogin(){
            if(this.username && this.password){
                var data = {username: this.username, password: this.password}
                login(data).then((response) => {
                    console.log(response)
                    if(response.state){
                        setToken(response.data.token)
                        this.resultLogin = "msg:"+response.msg+" token:"+ response.data.token
                    } else {
                        this.resultLogin = "msg:"+ response.msg
                    }
                }).catch(response => {
                    console.log(response);
                });
            }
        },
        handleLoGout(){
            logout().then((response) => {
                console.log(response)
                removeToken()
                this.resultLogout = "msg:"+ response.msg
            }).catch(response => {
                console.log(response);
            });
        },   
        handleanonHello(){
            anonHello().then((response) => {
                console.log(response)
                this.resultAnonHello = "msg:"+ response.msg
            }).catch(response => {
                console.log(response);
            });
        },
        handleauthcHello(){
            authcHello().then((response) => {
                console.log(response)
                this.resultAuthcHello = "msg:"+ response.msg
            }).catch(response => {
                console.log(response);
            });
        },
        handleuserList(){
            userList().then((response) => {
                console.log(response)
                this.resultuserList = "msg:"+ response.msg
            }).catch(response => {
                console.log(response);
            });
        },
        handleuserInsert(){
            userInsert().then((response) => {
                console.log(response)
                this.resultuserInsert = "msg:"+ response.msg
            }).catch(response => {
                console.log(response);
            });
        },
        handleuserUpdate(){
            userUpdate().then((response) => {
                console.log(response)
                this.resultuserUpdate = "msg:"+ response.msg
            }).catch(response => {
                console.log(response);
            });
        },
        handleuserDelete(){
            userDelete().then((response) => {
                console.log(response)
                this.resultuserDelete = "msg:"+ response.msg
            }).catch(response => {
                console.log(response);
            });
        }                                                               
    },
    mounted() {
        this.$nextTick(function () {
            this.init()
        })
    },
    watch: {
    }

}
</script>

首先將前端請求的token先注釋掉

1.不登錄的情況下訪問接口

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔
shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

在未登錄,也沒有前端攜帶token的情況下,可以看到匿名游客可以訪問,使用注解@RequiresPermissions("*:*:*")提示沒有token,因為在shiroFilter過濾器里過濾了的,認證用戶訪問的接口提示未登錄

2.登錄的情況下訪問接口(前端不攜帶token)

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

可以看到登錄后,想要登出都不可能,因為登出也是需要token認證的,加了注解的四個接口也是沒有token,只有特殊的認證訪問顯示的是未登錄,因為這種情況是這個接口是不經過shiroFilter過濾器的,但是進了自定義的session會話ShiroSessionManager,在這里他從http請求的header里沒拿到token,因此ssesionId是null,即使調用了父類方法,也是沒有的,于是重定向了接口,在這里重定向到了/shiro-redirect/index 如果后端重啟,在這里也是同樣的狀態(tài),因此不測試了。

將前端請求的token注釋取消,就是前端請求后端接口攜帶token

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

1.不登錄的情況下訪問接口(這是正式環(huán)境不會出現的情況)

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

可以看到前端報錯了,那是因為在前端緩存里token不存在,直接return了,不是return config,所以不請求后端數據。

2.登錄的情況下訪問接口(前端攜帶token,正常情況)

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

這種整套操作流程,登錄和登出都沒問題

3.前端登錄后,后端程序重啟后訪問認證頁面(因為后端使用的內存,內存里沒了shiro相關的會話)

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

可以看到報了400錯誤,那個shiro處理跳轉了登錄頁面,分離的前端無法處理的,一般我們碰不到這個錯誤的

4.使用只有部分權限的賬號登錄

shiro前后端分離實現,SpringBoot+Demo,shiro框架,shiro安全框架,ssm整合shiro,spring整合shiro,shiro權限管理,Powered by 金山文檔

可以看到加了注解的接口只有查詢用戶可以訪問,其他的訪問權限不夠。

這里的SpringBoot+Shiro和Vue實現前后端分離的Demo實際上使用到了Session的sessionId作為token來操作,如果要對這個token要求嚴格,那么可以使用對sessionId二次加密,例如jwt方式,這樣在session重寫的會話管理里先對token解密后在放入會話里。文章來源地址http://www.zghlxwxcb.cn/news/detail-738872.html

到了這里,關于SpringBoot+Shiro框架整合實現前后端分離的權限管理基礎Demo的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉載,請注明出處: 如若內容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 基于springboot + vue實現的前后端分離-酒店管理系統

    基于springboot + vue實現的前后端分離-酒店管理系統

    項目介紹 ????????基于springboot + vue實現的酒店管理系統一共有酒店管理員和用戶這兩種角色。 管理員功能 登錄:管理員可以通過登錄功能進入系統,確保只有授權人員可以訪問系統。 用戶管理:管理員可以添加、編輯和刪除酒店的用戶,包括前臺員工、服務員等。他們

    2024年02月22日
    瀏覽(21)
  • SpringBoot + Vue前后端分離項目實戰(zhàn) || 四:用戶管理功能實現

    SpringBoot + Vue前后端分離項目實戰(zhàn) || 四:用戶管理功能實現

    系列文章: SpringBoot + Vue前后端分離項目實戰(zhàn) || 一:Vue前端設計 SpringBoot + Vue前后端分離項目實戰(zhàn) || 二:Spring Boot后端與數據庫連接 SpringBoot + Vue前后端分離項目實戰(zhàn) || 三:Spring Boot后端與Vue前端連接 SpringBoot + Vue前后端分離項目實戰(zhàn) || 四:用戶管理功能實現 SpringBoot + Vue前后

    2024年02月11日
    瀏覽(38)
  • 基于Java+SpringBoot+Vue前后端分離學生信息管理設計實現

    基于Java+SpringBoot+Vue前后端分離學生信息管理設計實現

    博主介紹 : ? 全網粉絲30W+,csdn特邀作者、博客專家、CSDN新星計劃導師、java領域優(yōu)質創(chuàng)作者,博客之星、掘金/華為云/阿里云/InfoQ等平臺優(yōu)質作者、專注于Java技術領域和畢業(yè)項目實戰(zhàn) ? ?? 文末獲取源碼聯系 ?? ?????精彩專欄 推薦訂閱 ?????不然下次找不到喲 ?java項目

    2024年02月05日
    瀏覽(91)
  • 基于Java+SpringBoot+Vue前后端分離倉庫管理系統設計實現

    基于Java+SpringBoot+Vue前后端分離倉庫管理系統設計實現

    博主介紹 : ? 全網粉絲30W+,csdn特邀作者、博客專家、CSDN新星計劃導師、java領域優(yōu)質創(chuàng)作者,博客之星、掘金/華為云/阿里云/InfoQ等平臺優(yōu)質作者、專注于Java技術領域和畢業(yè)項目實戰(zhàn) ? ?? 文末獲取源碼聯系 ?? ?????精彩專欄 推薦訂閱 ?????不然下次找不到喲 基于Jav

    2023年04月09日
    瀏覽(1145)
  • 基于Java+SpringBoot+vue前后端分離企業(yè)客戶管理系統設計實現

    基于Java+SpringBoot+vue前后端分離企業(yè)客戶管理系統設計實現

    博主介紹 : ? 全網粉絲30W+,csdn特邀作者、博客專家、CSDN新星計劃導師、Java領域優(yōu)質創(chuàng)作者,博客之星、掘金/華為云/阿里云/InfoQ等平臺優(yōu)質作者、專注于Java技術領域和畢業(yè)項目實戰(zhàn) ? ?? 文末獲取源碼聯系 ?? ?????精彩專欄 推薦訂閱 ?????不然下次找不到喲 2022-2024年

    2024年02月13日
    瀏覽(232)
  • 基于Java+SpringBoot+Vue實現前后端分離美術館管理系統

    基于Java+SpringBoot+Vue實現前后端分離美術館管理系統

    ?博主介紹 : ? 全網粉絲20W+,csdn特邀作者、博客專家、CSDN新星計劃導師、java領域優(yōu)質創(chuàng)作者,博客之星、掘金/華為云/阿里云/InfoQ等平臺優(yōu)質作者、專注于Java技術領域和畢業(yè)項目實戰(zhàn) ? ?? 文末獲取源碼聯系 ?? ?????精彩專欄 推薦訂閱 ?????不然下次找不到喲 ?java項

    2024年02月07日
    瀏覽(96)
  • 基于Java+SpringBoot+Vue前后端分離圖書管理系統設計和實現

    基于Java+SpringBoot+Vue前后端分離圖書管理系統設計和實現

    博主介紹 : ? 全網粉絲30W+,csdn特邀作者、博客專家、CSDN新星計劃導師、Java領域優(yōu)質創(chuàng)作者,博客之星、掘金/華為云/阿里云/InfoQ等平臺優(yōu)質作者、專注于Java技術領域和畢業(yè)項目實戰(zhàn) ? ?? 文末獲取源碼聯系 ?? ?????精彩專欄 推薦訂閱 ?????不然下次找不到喲 2022-2024年

    2024年02月10日
    瀏覽(302)
  • 一個基于SpringBoot+Vue前后端分離學生宿舍管理系統詳細設計實現

    一個基于SpringBoot+Vue前后端分離學生宿舍管理系統詳細設計實現

    博主介紹 : ? 全網粉絲30W+,csdn特邀作者、博客專家、CSDN新星計劃導師、Java領域優(yōu)質創(chuàng)作者,博客之星、掘金/華為云/阿里云/InfoQ等平臺優(yōu)質作者、專注于Java技術領域和畢業(yè)項目實戰(zhàn) ? ?? 文末獲取源碼聯系 ?? ?????精彩專欄 推薦訂閱 ?????不然下次找不到喲 2022-2024年

    2024年02月07日
    瀏覽(166)
  • SpringBoot+mysql+vue實現大學生健康檔案管理系統前后端分離

    SpringBoot+mysql+vue實現大學生健康檔案管理系統前后端分離

    本項目是一套基于SpringBoot實現大學生健康檔案管理系統,主要針對計算機相關專業(yè)的正在做bishe的學生和需要項目實戰(zhàn)練習的Java學習者。 包含:項目源碼、數據庫腳本等,該項目可以直接作為bishe使用。 項目都經過嚴格調試,確保可以運行! 項目是采用SpringBoot + Mybatis + S

    2024年02月05日
    瀏覽(27)
  • 基于Java+SpringBoot+Vue前后端分離倉庫管理系統詳細設計和實現

    基于Java+SpringBoot+Vue前后端分離倉庫管理系統詳細設計和實現

    博主介紹 : ? 全網粉絲30W+,csdn特邀作者、博客專家、CSDN新星計劃導師、Java領域優(yōu)質創(chuàng)作者,博客之星、掘金/華為云/阿里云/InfoQ等平臺優(yōu)質作者、專注于Java技術領域和畢業(yè)項目實戰(zhàn) ? ?? 文末獲取源碼聯系 ?? ?????精彩專欄 推薦訂閱 ?????不然下次找不到喲 2022-2024年

    2024年02月16日
    瀏覽(100)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包