Apache Shiro

新的聊天

用戶:

你知道Apache Shiro么?

ChatGPT:

是的,我了解Apache Shiro。它是一個功能強(qiáng)大且易于使用的Java安全框架,主要用于身份認(rèn)證、授權(quán)、加密和會話管理。以下是Shiro的主要特點(diǎn)和核心概念:

  1. 主要功能:
  • 身份認(rèn)證(Authentication) - 驗(yàn)證用戶身份
  • 授權(quán)(Authorization) - 訪問控制
  • 會話管理(Session Management)
  • 加密(Cryptography)
  • Web支持
  • 緩存支持
  • 并發(fā)支持
  1. 核心概念:
  • Subject: 當(dāng)前操作的用戶
  • SecurityManager: 安全管理器,Shiro的核心組件
  • Realm: 用于連接Shiro和實(shí)際的安全數(shù)據(jù)源
  • Authentication: 身份驗(yàn)證
  • Authorization: 授權(quán)
  • Session: 會話管理
  1. 常用注解:
@RequiresAuthentication  // 需要登錄
@RequiresPermissions    // 需要權(quán)限
@RequiresRoles         // 需要角色
@RequiresGuest        // 需要是游客
@RequiresUser        // 需要是用戶
  1. 身份認(rèn)證流程:
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        currentUser.login(token);
    } catch (AuthenticationException ae) {
        //處理登錄失敗
    }
}
  1. 權(quán)限檢查示例:
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("admin")) {
    //有admin角色
}
if (currentUser.isPermitted("user:create")) {
    //有創(chuàng)建用戶的權(quán)限
}
  1. 配置示例:
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        
        Map<String, String> filterChainDefinitionMap = new HashMap<>();
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
}
  1. 主要優(yōu)點(diǎn):
  • 使用簡單,容易上手
  • 靈活性強(qiáng),可自定義程度高
  • 安全可靠
  • 支持多種驗(yàn)證方式
  • 支持分布式應(yīng)用
  1. 常見整合:
  • Spring Boot
  • Redis(用于Session管理)
  • JWT(Json Web Token)
  • Ehcache(緩存)
  1. 使用建議:
  • 建議使用最新版本
  • 注意合理配置緩存
  • 謹(jǐn)慎處理密碼等敏感信息
  • 合理規(guī)劃權(quán)限體系

Shiro是一個成熟的安全框架,特別適合需要靈活權(quán)限管理的Java Web應(yīng)用。它可以很好地與Spring Boot等框架集成,提供完整的安全解決方案。

用戶:

我目前只對java開發(fā)有一些了解,以前更多的用的是go語言開發(fā),我之前登錄注冊都是自己寫的,token用的是jwt,權(quán)限這塊用的RBAC設(shè)計(jì),新建表,然后弄了一個權(quán)限管理相關(guān)的接口用于后臺管理一些權(quán)限。我想知道用了Apache Shiro,比我之前的方案有哪些不同

ChatGPT:

讓我從幾個方面來比較一下:

  1. 開發(fā)效率:
  • 你之前的方案:需要自己實(shí)現(xiàn)認(rèn)證、授權(quán)邏輯,設(shè)計(jì)數(shù)據(jù)庫表,開發(fā)管理接口
  • 使用Shiro:提供了現(xiàn)成的認(rèn)證、授權(quán)框架,只需要實(shí)現(xiàn)Realm接口對接數(shù)據(jù)源,配置相關(guān)規(guī)則即可
  1. 安全性:
  • 你之前的方案:安全性依賴于自己的實(shí)現(xiàn)
  • 使用Shiro:經(jīng)過大量實(shí)踐驗(yàn)證的安全框架,內(nèi)置多種安全機(jī)制
  1. 功能完整性:
    Shiro額外提供了:
  • 會話管理
  • 緩存管理
  • 加密服務(wù)
  • 并發(fā)特性
  • Remember Me功能
  • 多種認(rèn)證策略
  1. 代碼示例對比:

你之前的方案可能是這樣:

// 登錄
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
    // 1. 驗(yàn)證用戶名密碼
    User user = userService.findByUsername(loginDTO.getUsername());
    if (user == null || !passwordMatch(loginDTO.getPassword(), user.getPassword())) {
        return Result.fail("用戶名或密碼錯誤");
    }
    
    // 2. 生成JWT
    String token = JWTUtil.generateToken(user);
    
    return Result.success(token);
}

// 權(quán)限校驗(yàn)
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/data")
public Result getAdminData() {
    // 業(yè)務(wù)邏輯
}

使用Shiro的方案:

// 登錄
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(loginDTO.getUsername(), loginDTO.getPassword());
    
    try {
        // Shiro會自動處理認(rèn)證邏輯
        subject.login(token);
        return Result.success("登錄成功");
    } catch (AuthenticationException e) {
        return Result.fail("登錄失敗");
    }
}

// 權(quán)限校驗(yàn)
@RequiresRoles("admin")
@GetMapping("/admin/data")
public Result getAdminData() {
    // 業(yè)務(wù)邏輯
}
  1. 集成難度:
  • 你之前的方案:完全自主可控,但需要自己處理各種細(xì)節(jié)
  • 使用Shiro:需要學(xué)習(xí)Shiro的概念和API,但集成相對簡單
  1. 靈活性:
  • 你之前的方案:完全自定義,可以根據(jù)需求隨意調(diào)整
  • 使用Shiro:需要在Shiro的框架內(nèi)實(shí)現(xiàn),但也提供了足夠的擴(kuò)展點(diǎn)
  1. JWT集成:
  • 你之前的方案:直接使用JWT
  • 使用Shiro:需要自定義JWTToken和Realm,稍微復(fù)雜一些
  1. 維護(hù)成本:
  • 你之前的方案:需要自己維護(hù)所有代碼
  • 使用Shiro:框架部分由Apache維護(hù),你只需要維護(hù)業(yè)務(wù)相關(guān)代碼

建議:

  1. 如果是小型項(xiàng)目,你之前的方案夠用

  2. 如果是大型項(xiàng)目,建議使用Shiro:

    • 更完善的安全特性
    • 更好的可維護(hù)性
    • 社區(qū)支持
    • 與其他框架的良好集成
  3. 如果主要考慮輕量級解決方案,可以考慮Spring Security,它與Spring生態(tài)系統(tǒng)集成更好

  4. 如果你習(xí)慣了之前的方案,也可以繼續(xù)使用,但建議:

    • 增加更多的安全特性
    • 完善異常處理
    • 添加更多的日志記錄
    • 考慮添加緩存機(jī)制

用戶:

Remember Me功能是什么?

ChatGPT:

Remember Me(記住我)功能是一種持久化的身份認(rèn)證機(jī)制,允許用戶在關(guān)閉瀏覽器后再次訪問網(wǎng)站時無需重新登錄。

工作原理:

  1. 當(dāng)用戶登錄時勾選"記住我"選項(xiàng)
  2. 服務(wù)器生成一個持久化的令牌(token)
  3. 將令牌存儲在客戶端(通常是cookie)和服務(wù)器端
  4. 下次用戶訪問時,即使session已過期,系統(tǒng)也能通過這個令牌識別用戶

Shiro中使用示例:

// 登錄時啟用Remember Me
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(
        loginDTO.getUsername(), 
        loginDTO.getPassword()
    );
    
    // 設(shè)置Remember Me
    token.setRememberMe(true);
    
    try {
        subject.login(token);
        return Result.success("登錄成功");
    } catch (AuthenticationException e) {
        return Result.fail("登錄失敗");
    }
}

// 配置Remember Me
@Bean
public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    
    // 配置Remember Me管理器
    CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
    // 設(shè)置cookie
    SimpleCookie cookie = new SimpleCookie("rememberMe");
    cookie.setMaxAge(259200); // 3天
    rememberMeManager.setCookie(cookie);
    
    securityManager.setRememberMeManager(rememberMeManager);
    return securityManager;
}

安全考慮:

  1. Remember Me令牌應(yīng)該有過期時間
  2. 令牌應(yīng)該是加密的
  3. 用戶應(yīng)該能夠主動清除Remember Me狀態(tài)
  4. 敏感操作仍需要重新驗(yàn)證

使用場景:

  • 普通網(wǎng)站的用戶便利性提升
  • 非敏感系統(tǒng)的自動登錄
  • 移動應(yīng)用的持久登錄狀態(tài)

不建議使用的場景:

  • 銀行等金融系統(tǒng)
  • 需要高安全性的管理后臺
  • 含有敏感信息的系統(tǒng)

Remember Me本質(zhì)上是用便利性換取部分安全性,使用時需要根據(jù)實(shí)際場景權(quán)衡。

用戶:

上面提到的

Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(loginDTO.getUsername(), loginDTO.getPassword());
    
    try {
        // Shiro會自動處理認(rèn)證邏輯
        subject.login(token);
        return Result.success("登錄成功");
    } catch (AuthenticationException e) {
        return Result.fail("登錄失敗");
    }

這個實(shí)際執(zhí)行了什么操作呢?

ChatGPT:

這段代碼背后的執(zhí)行流程比較復(fù)雜,我來詳細(xì)解釋一下:

  1. 首先,SecurityUtils.getSubject() 獲取當(dāng)前用戶主體:
Subject subject = SecurityUtils.getSubject();
  • 這會從SecurityManager中獲取當(dāng)前用戶的Subject實(shí)例
  • Subject代表當(dāng)前用戶,可以是已認(rèn)證的用戶,也可以是匿名用戶
  1. 創(chuàng)建認(rèn)證令牌:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  • 這是一個封裝了用戶名和密碼的認(rèn)證令牌
  • 密碼會被轉(zhuǎn)換為字符數(shù)組,提高安全性
  1. 當(dāng)執(zhí)行 subject.login(token) 時,會觸發(fā)以下流程:
public void login(AuthenticationToken token) throws AuthenticationException {
    // 1. 獲取SecurityManager
    SecurityManager securityManager = getSecurityManager();
    
    // 2. 執(zhí)行實(shí)際的登錄邏輯
    securityManager.login(this, token);
}
  1. SecurityManager會調(diào)用Authenticator進(jìn)行認(rèn)證:
// 在SecurityManager內(nèi)部
public void authenticate(AuthenticationToken token) throws AuthenticationException {
    // 1. 獲取Realm列表
    Collection<Realm> realms = getRealms();
    
    // 2. 遍歷所有配置的Realm
    for (Realm realm : realms) {
        // 3. 如果Realm支持此類型的token
        if (realm.supports(token)) {
            // 4. 調(diào)用Realm進(jìn)行認(rèn)證
            AuthenticationInfo info = realm.getAuthenticationInfo(token);
            // 5. 驗(yàn)證返回結(jié)果
            // ...
        }
    }
}
  1. 在自定義的Realm中進(jìn)行實(shí)際的認(rèn)證:
public class CustomRealm extends AuthorizingRealm {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        // 1. 轉(zhuǎn)換token
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        
        // 2. 從數(shù)據(jù)庫查詢用戶
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UnknownAccountException();
        }
        
        // 3. 返回認(rèn)證信息
        return new SimpleAuthenticationInfo(
            user,                     // principal(身份)
            user.getPassword(),       // hashedCredentials(密碼)
            ByteSource.Util.bytes(user.getSalt()), // salt(鹽值)
            getName()                 // realmName
        );
    }
}
  1. 密碼匹配過程:
// 在CredentialsMatcher中
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    // 1. 獲取用戶輸入的密碼
    char[] submittedPassword = getSubmittedPassword(token);
    
    // 2. 獲取存儲的密碼
    Object storedCredentials = getStoredPassword(info);
    
    // 3. 進(jìn)行密碼匹配(可能包含加密、hash等操作)
    return equals(submittedPassword, storedCredentials);
}
  1. 如果認(rèn)證成功:
  • 創(chuàng)建新的會話(Session)
  • 緩存認(rèn)證信息
  • 執(zhí)行記住我(如果啟用)
  • 觸發(fā)認(rèn)證成功事件
  1. 如果認(rèn)證失?。?/li>
  • 拋出相應(yīng)的異常:
    • UnknownAccountException(賬號不存在)
    • IncorrectCredentialsException(密碼錯誤)
    • LockedAccountException(賬號鎖定)
    • ExcessiveAttemptsException(嘗試次數(shù)過多)
      等等

完整的調(diào)用鏈大致是:

Controller 
    → Subject.login() 
    → SecurityManager 
    → Authenticator 
    → Realm 
    → UserService/DAO 
    → 數(shù)據(jù)庫

這個過程的優(yōu)點(diǎn)是:

  1. 解耦了認(rèn)證邏輯
  2. 支持多種認(rèn)證方式
  3. 提供了完整的異常體系
  4. 可以方便地?cái)U(kuò)展和定制

使用時需要注意:

  1. 正確配置Realm
  2. 合理處理異常
  3. 考慮添加密碼加密
  4. 可以配置多個Realm實(shí)現(xiàn)多種認(rèn)證方式

用戶:

如果用了這個框架,我還需要單獨(dú)實(shí)現(xiàn)RBAC,還需要創(chuàng)建那些管理接口么?

ChatGPT:

使用Shiro時,你仍然需要實(shí)現(xiàn)RBAC的數(shù)據(jù)模型和管理接口,但是權(quán)限驗(yàn)證的邏輯可以交給Shiro處理。我來詳細(xì)說明:

  1. 數(shù)據(jù)庫設(shè)計(jì):
    仍然需要基本的RBAC表結(jié)構(gòu):
-- 用戶表
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(100),
    password VARCHAR(100),
    -- 其他字段
);

-- 角色表
CREATE TABLE role (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    description VARCHAR(200)
);

-- 權(quán)限表
CREATE TABLE permission (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    description VARCHAR(200)
);

-- 用戶-角色關(guān)聯(lián)表
CREATE TABLE user_role (
    user_id BIGINT,
    role_id BIGINT
);

-- 角色-權(quán)限關(guān)聯(lián)表
CREATE TABLE role_permission (
    role_id BIGINT,
    permission_id BIGINT
);
  1. 自定義Realm實(shí)現(xiàn):
@Component
public class CustomRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    // 認(rèn)證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        // ... 認(rèn)證邏輯
    }

    // 授權(quán)
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        
        // 從數(shù)據(jù)庫獲取用戶角色和權(quán)限
        Set<String> roles = userService.findRoles(user.getId());
        Set<String> permissions = userService.findPermissions(user.getId());
        
        info.setRoles(roles);
        info.setStringPermissions(permissions);
        
        return info;
    }
}
  1. 管理接口示例:
@RestController
@RequestMapping("/admin")
public class RbacController {
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermissionService permissionService;

    // 創(chuàng)建角色
    @RequiresPermissions("role:create")
    @PostMapping("/role")
    public Result createRole(@RequestBody Role role) {
        roleService.create(role);
        return Result.success();
    }

    // 分配角色權(quán)限
    @RequiresPermissions("role:assign")
    @PostMapping("/role/{roleId}/permissions")
    public Result assignPermissions(@PathVariable Long roleId, 
                                  @RequestBody List<Long> permissionIds) {
        roleService.assignPermissions(roleId, permissionIds);
        return Result.success();
    }

    // 分配用戶角色
    @RequiresPermissions("user:assign")
    @PostMapping("/user/{userId}/roles")
    public Result assignRoles(@PathVariable Long userId, 
                            @RequestBody List<Long> roleIds) {
        userService.assignRoles(userId, roleIds);
        return Result.success();
    }
}
  1. 服務(wù)層示例:
@Service
public class RoleServiceImpl implements RoleService {
    @Autowired
    private RoleMapper roleMapper;
    
    @Override
    @Transactional
    public void assignPermissions(Long roleId, List<Long> permissionIds) {
        // 1. 刪除原有權(quán)限
        roleMapper.deleteRolePermissions(roleId);
        
        // 2. 添加新權(quán)限
        roleMapper.insertRolePermissions(roleId, permissionIds);
        
        // 3. 清除相關(guān)的權(quán)限緩存
        clearCache(roleId);
    }
}
  1. 權(quán)限使用示例:
// 注解方式
@RequiresPermissions("user:view")
@GetMapping("/users")
public Result listUsers() {
    // ...
}

// 編程方式
Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("user:edit")) {
    // ...
}
  1. 緩存配置(可選):
@Configuration
public class ShiroConfig {
    @Bean
    public CacheManager cacheManager() {
        // 使用EhCache
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }
}

主要區(qū)別:

  1. 不需要自己實(shí)現(xiàn)權(quán)限驗(yàn)證邏輯
  2. 可以使用Shiro的注解或API進(jìn)行權(quán)限控制
  3. 可以利用Shiro的緩存機(jī)制
  4. 權(quán)限管理更加標(biāo)準(zhǔn)化

仍然需要:

  1. 設(shè)計(jì)和實(shí)現(xiàn)RBAC數(shù)據(jù)模型
  2. 實(shí)現(xiàn)權(quán)限管理接口
  3. 維護(hù)角色和權(quán)限數(shù)據(jù)
  4. 實(shí)現(xiàn)用戶-角色-權(quán)限的分配邏輯

建議:

  1. 使用緩存來提高性能
  2. 實(shí)現(xiàn)細(xì)粒度的權(quán)限控制
  3. 做好權(quán)限變更后的緩存清理
  4. 考慮權(quán)限的層級結(jié)構(gòu)
  5. 實(shí)現(xiàn)完善的權(quán)限管理界面

總的來說,Shiro幫你處理了權(quán)限驗(yàn)證的邏輯,但具體的RBAC實(shí)現(xiàn)和管理還是需要你自己完成。這樣的分工可以讓你專注于業(yè)務(wù)邏輯,而將通用的安全控制交給框架處理。

用戶:

Subject subject = SecurityUtils.getSubject();

這一行我不太懂,我之前看如果用戶訪問一個接口的話,如果這個接口需要驗(yàn)證token,都是在攔截器里面自己解析的,解析出來得到user_id和其他信息,然后再做權(quán)限校驗(yàn),根據(jù)情況選擇是否拋出異常,上面這一行是自動做了這些事情么?還是我理解的不對?

ChatGPT:

你說得對,這里確實(shí)需要解釋清楚。SecurityUtils.getSubject() 并不會自動完成 token 的解析,它需要配合 Shiro 的過濾器鏈來工作。

讓我用一個完整的例子來說明:

  1. 首先,需要自定義一個 JWT Token:
public class JWTToken implements AuthenticationToken {
    private String token;

    public JWTToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}
  1. 創(chuàng)建一個處理 JWT 的過濾器:
public class JWTFilter extends BasicHttpAuthenticationFilter {
    
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Authorization");
        
        if (StringUtils.isEmpty(token)) {
            throw new AuthenticationException("Token不能為空");
        }

        // 創(chuàng)建 JWTToken
        JWTToken jwtToken = new JWTToken(token);
        
        // 提交給 realm 進(jìn)行登入,如果錯誤他會拋出異常并被捕獲
        try {
            // 這里就是調(diào)用 Subject,但是是在過濾器中調(diào)用的
            getSubject(request, response).login(jwtToken);
            return true;
        } catch (AuthenticationException e) {
            return false;
        }
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 判斷請求的請求頭是否帶上 token
        if (isLoginAttempt(request, response)) {
            try {
                // 執(zhí)行登錄
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                // token 錯誤
                return false;
            }
        }
        // 如果請求頭不存在 token,則可能是執(zhí)行登陸操作或者是游客狀態(tài)訪問
        return true;
    }
}
  1. 自定義 Realm 來處理 JWT:
public class JWTRealm extends AuthorizingRealm {
    
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        
        // 解析 JWT token
        String userId = JWTUtil.getUserId(token);
        if (userId == null) {
            throw new AuthenticationException("token invalid");
        }
        
        // 查詢用戶信息
        User user = userService.findById(userId);
        if (user == null) {
            throw new AuthenticationException("User didn't existed!");
        }
        
        return new SimpleAuthenticationInfo(user, token, getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        
        // 設(shè)置用戶角色和權(quán)限
        info.setRoles(userService.getUserRoles(user.getId()));
        info.setStringPermissions(userService.getUserPermissions(user.getId()));
        
        return info;
    }
}
  1. 配置 Shiro 過濾器鏈:
@Configuration
public class ShiroConfig {
    
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);

        // 添加自定義過濾器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);

        // 設(shè)置過濾器鏈
        Map<String, String> filterRuleMap = new HashMap<>();
        filterRuleMap.put("/login", "anon"); // 登錄接口放行
        filterRuleMap.put("/**", "jwt");     // 其他所有請求都需要JWT驗(yàn)證
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        
        return factoryBean;
    }
}

工作流程:

  1. 請求進(jìn)入系統(tǒng)
  2. 經(jīng)過 JWTFilter
  3. JWTFilter 從請求頭獲取 token
  4. JWTFilter 創(chuàng)建 JWTToken 并調(diào)用 subject.login()
  5. Shiro 將認(rèn)證請求轉(zhuǎn)發(fā)給 Realm
  6. Realm 解析 token,驗(yàn)證有效性
  7. 認(rèn)證成功后,用戶信息會存儲在 Subject 中

然后在 Controller 中:

@GetMapping("/user/info")
@RequiresPermissions("user:view")
public Result getUserInfo() {
    // 這時候調(diào)用 getSubject() 就能獲取到當(dāng)前用戶信息了
    Subject subject = SecurityUtils.getSubject();
    User user = (User) subject.getPrincipal();
    return Result.success(user);
}

所以你說得對,SecurityUtils.getSubject() 本身并不會解析 token,它只是獲取當(dāng)前用戶的上下文信息。真正的 token 解析和驗(yàn)證是在過濾器和 Realm 中完成的。

這個流程和你之前的實(shí)現(xiàn)方式類似,主要區(qū)別是:

  1. 把驗(yàn)證邏輯封裝在了 Realm 中
  2. 使用 Shiro 的過濾器鏈來管理請求
  3. 可以利用 Shiro 提供的注解和 API 來做權(quán)限控制

你之前的實(shí)現(xiàn)方式也是完全可行的,使用 Shiro 主要是為了:

  1. 提供統(tǒng)一的安全框架
  2. 更好的可擴(kuò)展性
  3. 更多的內(nèi)置功能
  4. 更規(guī)范的實(shí)現(xiàn)方式

用戶:

我看你說支持多種認(rèn)證方式,這個是什么意思?

ChatGPT:

支持多種認(rèn)證方式意味著你可以在同一個系統(tǒng)中配置多個 Realm,每個 Realm 負(fù)責(zé)不同的認(rèn)證方式。我來舉幾個例子:

  1. 同時支持用戶名密碼和JWT認(rèn)證:
// 用戶名密碼認(rèn)證的 Realm
public class UsernamePasswordRealm extends AuthorizingRealm {
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        // 驗(yàn)證用戶名密碼
        // ...
    }
}

// JWT認(rèn)證的 Realm
public class JWTRealm extends AuthorizingRealm {
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        JWTToken jwtToken = (JWTToken) token;
        // 驗(yàn)證JWT
        // ...
    }
}

// 配置多個Realm
@Configuration
public class ShiroConfig {
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        
        // 創(chuàng)建認(rèn)證器
        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        // 設(shè)置認(rèn)證策略,ALL必須所有Realm都通過,ANY只需要其中一個通過
        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
        
        // 設(shè)置多個Realm
        List<Realm> realms = new ArrayList<>();
        realms.add(new UsernamePasswordRealm());
        realms.add(new JWTRealm());
        securityManager.setRealms(realms);
        
        return securityManager;
    }
}
  1. 支持多種第三方登錄:
// 微信登錄的 Realm
public class WeChatRealm extends AuthorizingRealm {
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof WeChatToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        WeChatToken weChatToken = (WeChatToken) token;
        // 驗(yàn)證微信登錄
        // ...
    }
}

// GitHub登錄的 Realm
public class GitHubRealm extends AuthorizingRealm {
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof GitHubToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        GitHubToken githubToken = (GitHubToken) token;
        // 驗(yàn)證GitHub登錄
        // ...
    }
}
  1. 同時支持?jǐn)?shù)據(jù)庫和LDAP認(rèn)證:
// 數(shù)據(jù)庫認(rèn)證的 Realm
public class DatabaseRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        // 從數(shù)據(jù)庫驗(yàn)證用戶
        // ...
    }
}

// LDAP認(rèn)證的 Realm
public class LdapRealm extends AuthorizingRealm {
    @Autowired
    private LdapTemplate ldapTemplate;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        // 通過LDAP驗(yàn)證用戶
        // ...
    }
}
  1. 在Controller中使用不同的認(rèn)證方式:
@RestController
public class AuthController {
    
    // 用戶名密碼登錄
    @PostMapping("/login/password")
    public Result loginByPassword(@RequestBody LoginDTO loginDTO) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(
            loginDTO.getUsername(), 
            loginDTO.getPassword()
        );
        subject.login(token);
        return Result.success();
    }
    
    // 微信登錄
    @PostMapping("/login/wechat")
    public Result loginByWeChat(@RequestBody WeChatLoginDTO loginDTO) {
        Subject subject = SecurityUtils.getSubject();
        WeChatToken token = new WeChatToken(loginDTO.getCode());
        subject.login(token);
        return Result.success();
    }
    
    // GitHub登錄
    @PostMapping("/login/github")
    public Result loginByGitHub(@RequestBody GitHubLoginDTO loginDTO) {
        Subject subject = SecurityUtils.getSubject();
        GitHubToken token = new GitHubToken(loginDTO.getCode());
        subject.login(token);
        return Result.success();
    }
}

優(yōu)點(diǎn):

  1. 可以靈活組合不同的認(rèn)證方式
  2. 統(tǒng)一的認(rèn)證框架
  3. 代碼復(fù)用性好
  4. 易于擴(kuò)展

使用場景:

  1. 支持多種登錄方式的系統(tǒng)
  2. 需要兼容新舊認(rèn)證方式的系統(tǒng)
  3. 企業(yè)級應(yīng)用需要同時支持多種認(rèn)證方式
  4. 需要集成第三方認(rèn)證的系統(tǒng)

注意事項(xiàng):

  1. 合理配置認(rèn)證策略
  2. 處理好多種認(rèn)證方式的優(yōu)先級
  3. 注意不同認(rèn)證方式的安全級別
  4. 做好用戶身份的統(tǒng)一管理

用戶:

AuthorizingRealm是一個什么都沒有實(shí)現(xiàn)的類么,我發(fā)現(xiàn)上面全是繼承這個的?

ChatGPT:

AuthorizingRealm 是一個抽象類,它實(shí)現(xiàn)了 Realm 接口,并提供了認(rèn)證(Authentication)和授權(quán)(Authorization)的基礎(chǔ)框架。它不是一個空的類,而是提供了很多基礎(chǔ)實(shí)現(xiàn)。

讓我們看看它的主要結(jié)構(gòu):

public abstract class AuthorizingRealm extends AuthenticatingRealm {
    
    // 緩存管理
    private AuthorizationCache<Object, AuthorizationInfo> authorizationCache;
    
    // 主要方法
    public AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
        // 1. 檢查參數(shù)
        if (principals == null) {
            return null;
        }

        // 2. 嘗試從緩存獲取
        if (authorizationCache != null) {
            AuthorizationInfo cached = authorizationCache.get(principals);
            if (cached != null) {
                return cached;
            }
        }

        // 3. 調(diào)用子類實(shí)現(xiàn)的方法獲取授權(quán)信息
        AuthorizationInfo info = doGetAuthorizationInfo(principals);
        
        // 4. 緩存結(jié)果
        if (info != null && authorizationCache != null) {
            authorizationCache.put(principals, info);
        }

        return info;
    }

    // 子類需要實(shí)現(xiàn)的抽象方法
    protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
}

// AuthenticatingRealm(父類)也提供了很多實(shí)現(xiàn)
public abstract class AuthenticatingRealm implements Realm {
    
    // 憑證匹配器
    private CredentialsMatcher credentialsMatcher;
    
    // 緩存管理
    private Cache<Object, AuthenticationInfo> authenticationCache;
    
    // 主要方法
    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        
        // 1. 檢查緩存
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info != null) {
            return info;
        }

        // 2. 調(diào)用子類實(shí)現(xiàn)的方法獲取認(rèn)證信息
        info = doGetAuthenticationInfo(token);
        
        // 3. 緩存結(jié)果
        if (info != null) {
            cacheAuthenticationInfo(token, info);
        }
        
        return info;
    }

    // 子類需要實(shí)現(xiàn)的抽象方法
    protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException;
}

AuthorizingRealm 提供的功能包括:

  1. 緩存支持:
// 使用緩存
public class CustomRealm extends AuthorizingRealm {
    @Override
    public void setCacheManager(CacheManager cacheManager) {
        super.setCacheManager(cacheManager);
    }
}
  1. 憑證匹配:
public class CustomRealm extends AuthorizingRealm {
    public CustomRealm() {
        // 設(shè)置密碼匹配器
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(1024);
        setCredentialsMatcher(matcher);
    }
}
  1. 權(quán)限字符串解析:
public class CustomRealm extends AuthorizingRealm {
    @Override
    public boolean isPermitted(PrincipalCollection principals, String permission) {
        // AuthorizingRealm已經(jīng)實(shí)現(xiàn)了權(quán)限字符串的解析
        return super.isPermitted(principals, permission);
    }
}
  1. 角色檢查:
public class CustomRealm extends AuthorizingRealm {
    @Override
    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        // AuthorizingRealm已經(jīng)實(shí)現(xiàn)了角色檢查的邏輯
        return super.hasRole(principals, roleIdentifier);
    }
}

我們只需要實(shí)現(xiàn)兩個核心方法:

  1. doGetAuthenticationInfo:處理認(rèn)證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
        throws AuthenticationException {
    String username = (String) token.getPrincipal();
    User user = userService.findByUsername(username);
    
    if (user == null) {
        throw new UnknownAccountException();
    }
    
    return new SimpleAuthenticationInfo(
        user,                     // principal(用戶對象)
        user.getPassword(),       // credentials(密碼)
        ByteSource.Util.bytes(user.getSalt()), // salt(鹽值)
        getName()                 // realmName
    );
}
  1. doGetAuthorizationInfo:處理授權(quán)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    User user = (User) principals.getPrimaryPrincipal();
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    
    // 設(shè)置角色和權(quán)限
    info.setRoles(userService.getRoles(user.getId()));
    info.setStringPermissions(userService.getPermissions(user.getId()));
    
    return info;
}

所以 AuthorizingRealm 不是空類,而是:

  1. 提供了完整的認(rèn)證授權(quán)框架
  2. 實(shí)現(xiàn)了緩存管理
  3. 實(shí)現(xiàn)了憑證匹配
  4. 提供了權(quán)限檢查的基礎(chǔ)實(shí)現(xiàn)
  5. 只需要我們實(shí)現(xiàn)核心的認(rèn)證和授權(quán)邏輯

這種設(shè)計(jì)模式是典型的模板方法模式,父類定義框架,子類實(shí)現(xiàn)具體細(xì)節(jié)。

用戶:

我看你之前給的案例里面有一個這個注解:@RequiresRoles("admin"),這個admin是RBAC里面的角色么?

ChatGPT:

是的,@RequiresRoles("admin") 中的 "admin" 就是對應(yīng) RBAC 中角色表里的角色標(biāo)識。讓我用一個完整的例子來說明:

  1. 首先是數(shù)據(jù)庫中的角色表:
CREATE TABLE role (
    id BIGINT PRIMARY KEY,
    code VARCHAR(100),  -- 角色標(biāo)識,如 "admin"
    name VARCHAR(100),  -- 角色名稱,如 "管理員"
    description VARCHAR(200)
);

-- 示例數(shù)據(jù)
INSERT INTO role (id, code, name, description) VALUES 
(1, 'admin', '管理員', '系統(tǒng)管理員'),
(2, 'user', '普通用戶', '普通用戶');
  1. 在代碼中使用注解:
@RestController
@RequestMapping("/admin")
public class AdminController {
    
    // 需要 admin 角色才能訪問
    @RequiresRoles("admin")
    @GetMapping("/users")
    public Result listUsers() {
        return userService.listUsers();
    }

    // 需要多個角色才能訪問
    @RequiresRoles({"admin", "manager"})
    @GetMapping("/sensitive")
    public Result sensitiveOperation() {
        return someService.doSomething();
    }

    // 需要任意一個角色即可訪問
    @RequiresRoles(value = {"admin", "manager"}, logical = Logical.OR)
    @GetMapping("/report")
    public Result viewReport() {
        return reportService.generateReport();
    }
}
  1. 在 Realm 中關(guān)聯(lián)角色:
public class CustomRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        
        // 從數(shù)據(jù)庫獲取用戶角色
        Set<String> roles = userService.getUserRoles(user.getId());
        // 這里的 roles 可能包含 "admin", "user" 等角色標(biāo)識
        info.setRoles(roles);
        
        return info;
    }
}
  1. Service層實(shí)現(xiàn):
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    public Set<String> getUserRoles(Long userId) {
        // 查詢用戶角色關(guān)聯(lián)表,獲取角色標(biāo)識
        List<Role> roles = userMapper.getUserRoles(userId);
        return roles.stream()
                   .map(Role::getCode)  // 獲取角色標(biāo)識
                   .collect(Collectors.toSet());
    }
}
  1. 也可以通過編程方式檢查角色:
@Service
public class SomeService {
    
    public void doSomething() {
        Subject subject = SecurityUtils.getSubject();
        
        // 檢查是否有某個角色
        if (subject.hasRole("admin")) {
            // 有 admin 角色
        }
        
        // 檢查是否有多個角色
        if (subject.hasAllRoles(Arrays.asList("admin", "manager"))) {
            // 同時具有 admin 和 manager 角色
        }
        
        // 檢查多個角色并獲取結(jié)果
        boolean[] result = subject.hasRoles(Arrays.asList("admin", "user", "guest"));
        // result[0] 表示是否有 admin 角色
        // result[1] 表示是否有 user 角色
        // result[2] 表示是否有 guest 角色
    }
}
  1. 異常處理:
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(UnauthorizedException.class)
    public Result handleUnauthorizedException(UnauthorizedException e) {
        // 當(dāng)用戶沒有所需角色時拋出此異常
        return Result.fail("沒有所需的角色權(quán)限");
    }
}

注意事項(xiàng):

  1. 角色標(biāo)識(如"admin")最好使用統(tǒng)一的常量管理:
public class RoleConstants {
    public static final String ADMIN = "admin";
    public static final String USER = "user";
    public static final String MANAGER = "manager";
}
  1. 可以配合權(quán)限使用:
@RequiresRoles("admin")
@RequiresPermissions("user:create")
@PostMapping("/user")
public Result createUser(@RequestBody User user) {
    // 需要同時具有 admin 角色和創(chuàng)建用戶的權(quán)限
    return userService.createUser(user);
}
  1. 建議角色和權(quán)限的關(guān)系:
  • 角色是權(quán)限的集合
  • 通常粗粒度的控制用角色
  • 細(xì)粒度的控制用權(quán)限
  • 優(yōu)先使用權(quán)限控制,而不是角色控制
  1. 角色標(biāo)識的命名建議:
  • 使用小寫字母
  • 用下劃線或中劃線分隔單詞
  • 具有業(yè)務(wù)含義
  • 保持簡短但清晰

例如:

// 好的命名
"admin"
"content_editor"
"system_manager"

// 不好的命名
"ADMIN"
"role1"
"r_01"

用戶:

你的意思,我盡量不使用這種注解?而是直接在自定義的Realm中獲取到角色對應(yīng)的API權(quán)限列表,然后用代碼判斷么?

ChatGPT:

是的,你理解得對。使用基于權(quán)限(Permission)的控制比基于角色(Role)的控制更靈活和可維護(hù)。我來解釋下原因和具體實(shí)現(xiàn):

  1. 基于角色的控制存在的問題:
// 不夠靈活
@RequiresRoles("admin")
@GetMapping("/users")
public Result listUsers() {
    return userService.listUsers();
}
  • 角色和權(quán)限強(qiáng)耦合
  • 修改角色權(quán)限需要修改代碼
  • 難以細(xì)粒度控制
  • 權(quán)限分配不夠直觀
  1. 基于權(quán)限的控制更好的方式:
// 更清晰的表達(dá)權(quán)限需求
@RequiresPermissions("user:view")
@GetMapping("/users")
public Result listUsers() {
    return userService.listUsers();
}
  1. 在Realm中實(shí)現(xiàn)權(quán)限控制:
public class CustomRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private PermissionService permissionService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        
        // 獲取用戶所有角色
        Set<String> roles = userService.getUserRoles(user.getId());
        
        // 獲取所有角色對應(yīng)的權(quán)限
        Set<String> permissions = permissionService.getPermissionsByRoles(roles);
        
        // 也可以獲取用戶直接關(guān)聯(lián)的權(quán)限
        permissions.addAll(permissionService.getUserPermissions(user.getId()));
        
        // 設(shè)置權(quán)限
        info.setStringPermissions(permissions);
        
        return info;
    }
}
  1. 權(quán)限表設(shè)計(jì):
-- 權(quán)限表
CREATE TABLE permission (
    id BIGINT PRIMARY KEY,
    code VARCHAR(100),       -- 權(quán)限標(biāo)識,如 "user:view"
    name VARCHAR(100),       -- 權(quán)限名稱,如 "查看用戶"
    type VARCHAR(50),        -- 權(quán)限類型,如 "api", "menu", "button"
    url VARCHAR(200),        -- API路徑,如 "/api/users"
    method VARCHAR(10),      -- HTTP方法,如 "GET"
    description VARCHAR(200)
);

-- 角色權(quán)限關(guān)聯(lián)表
CREATE TABLE role_permission (
    role_id BIGINT,
    permission_id BIGINT
);
  1. 權(quán)限服務(wù)實(shí)現(xiàn):
@Service
public class PermissionServiceImpl implements PermissionService {
    @Autowired
    private PermissionMapper permissionMapper;
    
    public Set<String> getPermissionsByRoles(Set<String> roles) {
        // 查詢這些角色關(guān)聯(lián)的所有權(quán)限
        List<Permission> permissions = permissionMapper.findByRoles(roles);
        return permissions.stream()
                         .map(Permission::getCode)
                         .collect(Collectors.toSet());
    }
    
    public boolean checkPermission(String url, String method, Set<String> userPermissions) {
        // 根據(jù)URL和方法查找需要的權(quán)限
        Permission permission = permissionMapper.findByUrlAndMethod(url, method);
        if (permission == null) {
            return false;
        }
        // 檢查用戶是否有這個權(quán)限
        return userPermissions.contains(permission.getCode());
    }
}
  1. 可以自定義過濾器進(jìn)行權(quán)限檢查:
public class PermissionFilter extends AccessControlFilter {
    @Autowired
    private PermissionService permissionService;

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 獲取請求的URL和方法
        String url = httpRequest.getRequestURI();
        String method = httpRequest.getMethod();
        
        // 獲取用戶權(quán)限
        SimpleAuthorizationInfo info = (SimpleAuthorizationInfo) subject.getSession()
                                                                      .getAttribute("authorizationInfo");
        Set<String> permissions = info.getStringPermissions();
        
        // 檢查權(quán)限
        return permissionService.checkPermission(url, method, permissions);
    }
}
  1. 權(quán)限標(biāo)識的命名規(guī)范:
public class PermissionConstants {
    // 用戶模塊
    public static final String USER_VIEW = "user:view";
    public static final String USER_CREATE = "user:create";
    public static final String USER_UPDATE = "user:update";
    public static final String USER_DELETE = "user:delete";
    
    // 訂單模塊
    public static final String ORDER_VIEW = "order:view";
    public static final String ORDER_CREATE = "order:create";
    // ...
}

這種方式的優(yōu)點(diǎn):

  1. 解耦了角色和權(quán)限
  2. 可以動態(tài)調(diào)整權(quán)限
  3. 支持細(xì)粒度控制
  4. 便于維護(hù)和擴(kuò)展
  5. 權(quán)限分配更直觀
  6. 支持動態(tài)權(quán)限驗(yàn)證

使用建議:

  1. 設(shè)計(jì)合理的權(quán)限粒度
  2. 建立清晰的權(quán)限命名規(guī)范
  3. 做好權(quán)限的分類管理
  4. 考慮權(quán)限的緩存策略
  5. 實(shí)現(xiàn)完善的權(quán)限管理界面

這樣的實(shí)現(xiàn)更適合企業(yè)級應(yīng)用,特別是需要靈活權(quán)限控制的系統(tǒng)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容