新的聊天
用戶:
你知道Apache Shiro么?
ChatGPT:
是的,我了解Apache Shiro。它是一個功能強(qiáng)大且易于使用的Java安全框架,主要用于身份認(rèn)證、授權(quán)、加密和會話管理。以下是Shiro的主要特點(diǎn)和核心概念:
- 主要功能:
- 身份認(rèn)證(Authentication) - 驗(yàn)證用戶身份
- 授權(quán)(Authorization) - 訪問控制
- 會話管理(Session Management)
- 加密(Cryptography)
- Web支持
- 緩存支持
- 并發(fā)支持
- 核心概念:
- Subject: 當(dāng)前操作的用戶
- SecurityManager: 安全管理器,Shiro的核心組件
- Realm: 用于連接Shiro和實(shí)際的安全數(shù)據(jù)源
- Authentication: 身份驗(yàn)證
- Authorization: 授權(quán)
- Session: 會話管理
- 常用注解:
@RequiresAuthentication // 需要登錄
@RequiresPermissions // 需要權(quán)限
@RequiresRoles // 需要角色
@RequiresGuest // 需要是游客
@RequiresUser // 需要是用戶
- 身份認(rèn)證流程:
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
currentUser.login(token);
} catch (AuthenticationException ae) {
//處理登錄失敗
}
}
- 權(quán)限檢查示例:
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("admin")) {
//有admin角色
}
if (currentUser.isPermitted("user:create")) {
//有創(chuàng)建用戶的權(quán)限
}
- 配置示例:
@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;
}
}
- 主要優(yōu)點(diǎn):
- 使用簡單,容易上手
- 靈活性強(qiáng),可自定義程度高
- 安全可靠
- 支持多種驗(yàn)證方式
- 支持分布式應(yīng)用
- 常見整合:
- Spring Boot
- Redis(用于Session管理)
- JWT(Json Web Token)
- Ehcache(緩存)
- 使用建議:
- 建議使用最新版本
- 注意合理配置緩存
- 謹(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:
讓我從幾個方面來比較一下:
- 開發(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ī)則即可
- 安全性:
- 你之前的方案:安全性依賴于自己的實(shí)現(xiàn)
- 使用Shiro:經(jīng)過大量實(shí)踐驗(yàn)證的安全框架,內(nèi)置多種安全機(jī)制
- 功能完整性:
Shiro額外提供了:
- 會話管理
- 緩存管理
- 加密服務(wù)
- 并發(fā)特性
- Remember Me功能
- 多種認(rèn)證策略
- 代碼示例對比:
你之前的方案可能是這樣:
// 登錄
@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ù)邏輯
}
- 集成難度:
- 你之前的方案:完全自主可控,但需要自己處理各種細(xì)節(jié)
- 使用Shiro:需要學(xué)習(xí)Shiro的概念和API,但集成相對簡單
- 靈活性:
- 你之前的方案:完全自定義,可以根據(jù)需求隨意調(diào)整
- 使用Shiro:需要在Shiro的框架內(nèi)實(shí)現(xiàn),但也提供了足夠的擴(kuò)展點(diǎn)
- JWT集成:
- 你之前的方案:直接使用JWT
- 使用Shiro:需要自定義JWTToken和Realm,稍微復(fù)雜一些
- 維護(hù)成本:
- 你之前的方案:需要自己維護(hù)所有代碼
- 使用Shiro:框架部分由Apache維護(hù),你只需要維護(hù)業(yè)務(wù)相關(guān)代碼
建議:
如果是小型項(xiàng)目,你之前的方案夠用
-
如果是大型項(xiàng)目,建議使用Shiro:
- 更完善的安全特性
- 更好的可維護(hù)性
- 社區(qū)支持
- 與其他框架的良好集成
如果主要考慮輕量級解決方案,可以考慮Spring Security,它與Spring生態(tài)系統(tǒng)集成更好
-
如果你習(xí)慣了之前的方案,也可以繼續(xù)使用,但建議:
- 增加更多的安全特性
- 完善異常處理
- 添加更多的日志記錄
- 考慮添加緩存機(jī)制
用戶:
Remember Me功能是什么?
ChatGPT:
Remember Me(記住我)功能是一種持久化的身份認(rèn)證機(jī)制,允許用戶在關(guān)閉瀏覽器后再次訪問網(wǎng)站時無需重新登錄。
工作原理:
- 當(dāng)用戶登錄時勾選"記住我"選項(xiàng)
- 服務(wù)器生成一個持久化的令牌(token)
- 將令牌存儲在客戶端(通常是cookie)和服務(wù)器端
- 下次用戶訪問時,即使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;
}
安全考慮:
- Remember Me令牌應(yīng)該有過期時間
- 令牌應(yīng)該是加密的
- 用戶應(yīng)該能夠主動清除Remember Me狀態(tài)
- 敏感操作仍需要重新驗(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ì)解釋一下:
- 首先,
SecurityUtils.getSubject()獲取當(dāng)前用戶主體:
Subject subject = SecurityUtils.getSubject();
- 這會從SecurityManager中獲取當(dāng)前用戶的Subject實(shí)例
- Subject代表當(dāng)前用戶,可以是已認(rèn)證的用戶,也可以是匿名用戶
- 創(chuàng)建認(rèn)證令牌:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
- 這是一個封裝了用戶名和密碼的認(rèn)證令牌
- 密碼會被轉(zhuǎn)換為字符數(shù)組,提高安全性
- 當(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);
}
- 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é)果
// ...
}
}
}
- 在自定義的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
);
}
}
- 密碼匹配過程:
// 在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);
}
- 如果認(rèn)證成功:
- 創(chuàng)建新的會話(Session)
- 緩存認(rèn)證信息
- 執(zhí)行記住我(如果啟用)
- 觸發(fā)認(rèn)證成功事件
- 如果認(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)是:
- 解耦了認(rèn)證邏輯
- 支持多種認(rèn)證方式
- 提供了完整的異常體系
- 可以方便地?cái)U(kuò)展和定制
使用時需要注意:
- 正確配置Realm
- 合理處理異常
- 考慮添加密碼加密
- 可以配置多個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ì)說明:
- 數(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
);
- 自定義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;
}
}
- 管理接口示例:
@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();
}
}
- 服務(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);
}
}
- 權(quán)限使用示例:
// 注解方式
@RequiresPermissions("user:view")
@GetMapping("/users")
public Result listUsers() {
// ...
}
// 編程方式
Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("user:edit")) {
// ...
}
- 緩存配置(可選):
@Configuration
public class ShiroConfig {
@Bean
public CacheManager cacheManager() {
// 使用EhCache
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
}
主要區(qū)別:
- 不需要自己實(shí)現(xiàn)權(quán)限驗(yàn)證邏輯
- 可以使用Shiro的注解或API進(jìn)行權(quán)限控制
- 可以利用Shiro的緩存機(jī)制
- 權(quán)限管理更加標(biāo)準(zhǔn)化
仍然需要:
- 設(shè)計(jì)和實(shí)現(xiàn)RBAC數(shù)據(jù)模型
- 實(shí)現(xiàn)權(quán)限管理接口
- 維護(hù)角色和權(quán)限數(shù)據(jù)
- 實(shí)現(xiàn)用戶-角色-權(quán)限的分配邏輯
建議:
- 使用緩存來提高性能
- 實(shí)現(xiàn)細(xì)粒度的權(quán)限控制
- 做好權(quán)限變更后的緩存清理
- 考慮權(quán)限的層級結(jié)構(gòu)
- 實(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 的過濾器鏈來工作。
讓我用一個完整的例子來說明:
- 首先,需要自定義一個 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;
}
}
- 創(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;
}
}
- 自定義 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;
}
}
- 配置 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;
}
}
工作流程:
- 請求進(jìn)入系統(tǒng)
- 經(jīng)過 JWTFilter
- JWTFilter 從請求頭獲取 token
- JWTFilter 創(chuàng)建 JWTToken 并調(diào)用
subject.login() - Shiro 將認(rèn)證請求轉(zhuǎn)發(fā)給 Realm
- Realm 解析 token,驗(yàn)證有效性
- 認(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ū)別是:
- 把驗(yàn)證邏輯封裝在了 Realm 中
- 使用 Shiro 的過濾器鏈來管理請求
- 可以利用 Shiro 提供的注解和 API 來做權(quán)限控制
你之前的實(shí)現(xiàn)方式也是完全可行的,使用 Shiro 主要是為了:
- 提供統(tǒng)一的安全框架
- 更好的可擴(kuò)展性
- 更多的內(nèi)置功能
- 更規(guī)范的實(shí)現(xiàn)方式
用戶:
我看你說支持多種認(rèn)證方式,這個是什么意思?
ChatGPT:
支持多種認(rèn)證方式意味著你可以在同一個系統(tǒng)中配置多個 Realm,每個 Realm 負(fù)責(zé)不同的認(rèn)證方式。我來舉幾個例子:
- 同時支持用戶名密碼和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;
}
}
- 支持多種第三方登錄:
// 微信登錄的 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登錄
// ...
}
}
- 同時支持?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)證用戶
// ...
}
}
- 在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):
- 可以靈活組合不同的認(rèn)證方式
- 統(tǒng)一的認(rèn)證框架
- 代碼復(fù)用性好
- 易于擴(kuò)展
使用場景:
- 支持多種登錄方式的系統(tǒng)
- 需要兼容新舊認(rèn)證方式的系統(tǒng)
- 企業(yè)級應(yīng)用需要同時支持多種認(rèn)證方式
- 需要集成第三方認(rèn)證的系統(tǒng)
注意事項(xiàng):
- 合理配置認(rèn)證策略
- 處理好多種認(rèn)證方式的優(yōu)先級
- 注意不同認(rèn)證方式的安全級別
- 做好用戶身份的統(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 提供的功能包括:
- 緩存支持:
// 使用緩存
public class CustomRealm extends AuthorizingRealm {
@Override
public void setCacheManager(CacheManager cacheManager) {
super.setCacheManager(cacheManager);
}
}
- 憑證匹配:
public class CustomRealm extends AuthorizingRealm {
public CustomRealm() {
// 設(shè)置密碼匹配器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");
matcher.setHashIterations(1024);
setCredentialsMatcher(matcher);
}
}
- 權(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);
}
}
- 角色檢查:
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)兩個核心方法:
- 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
);
}
- 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 不是空類,而是:
- 提供了完整的認(rèn)證授權(quán)框架
- 實(shí)現(xiàn)了緩存管理
- 實(shí)現(xiàn)了憑證匹配
- 提供了權(quán)限檢查的基礎(chǔ)實(shí)現(xiàn)
- 只需要我們實(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)識。讓我用一個完整的例子來說明:
- 首先是數(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', '普通用戶', '普通用戶');
- 在代碼中使用注解:
@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();
}
}
- 在 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;
}
}
- 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());
}
}
- 也可以通過編程方式檢查角色:
@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 角色
}
}
- 異常處理:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UnauthorizedException.class)
public Result handleUnauthorizedException(UnauthorizedException e) {
// 當(dāng)用戶沒有所需角色時拋出此異常
return Result.fail("沒有所需的角色權(quán)限");
}
}
注意事項(xiàng):
- 角色標(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";
}
- 可以配合權(quán)限使用:
@RequiresRoles("admin")
@RequiresPermissions("user:create")
@PostMapping("/user")
public Result createUser(@RequestBody User user) {
// 需要同時具有 admin 角色和創(chuàng)建用戶的權(quán)限
return userService.createUser(user);
}
- 建議角色和權(quán)限的關(guān)系:
- 角色是權(quán)限的集合
- 通常粗粒度的控制用角色
- 細(xì)粒度的控制用權(quán)限
- 優(yōu)先使用權(quán)限控制,而不是角色控制
- 角色標(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):
- 基于角色的控制存在的問題:
// 不夠靈活
@RequiresRoles("admin")
@GetMapping("/users")
public Result listUsers() {
return userService.listUsers();
}
- 角色和權(quán)限強(qiáng)耦合
- 修改角色權(quán)限需要修改代碼
- 難以細(xì)粒度控制
- 權(quán)限分配不夠直觀
- 基于權(quán)限的控制更好的方式:
// 更清晰的表達(dá)權(quán)限需求
@RequiresPermissions("user:view")
@GetMapping("/users")
public Result listUsers() {
return userService.listUsers();
}
- 在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;
}
}
- 權(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
);
- 權(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());
}
}
- 可以自定義過濾器進(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);
}
}
- 權(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):
- 解耦了角色和權(quán)限
- 可以動態(tài)調(diào)整權(quán)限
- 支持細(xì)粒度控制
- 便于維護(hù)和擴(kuò)展
- 權(quán)限分配更直觀
- 支持動態(tài)權(quán)限驗(yàn)證
使用建議:
- 設(shè)計(jì)合理的權(quán)限粒度
- 建立清晰的權(quán)限命名規(guī)范
- 做好權(quán)限的分類管理
- 考慮權(quán)限的緩存策略
- 實(shí)現(xiàn)完善的權(quán)限管理界面
這樣的實(shí)現(xiàn)更適合企業(yè)級應(yīng)用,特別是需要靈活權(quán)限控制的系統(tǒng)。