新的聊天
用户:
你知道Apache Shiro么?
ChatGPT:
是的,我了解Apache Shiro。它是一个功能强大且易于使用的Java安全框架,主要用于身份认证、授权、加密和会话管理。以下是Shiro的主要特点和核心概念:
- 主要功能:
- 身份认证(Authentication) - 验证用户身份
- 授权(Authorization) - 访问控制
- 会话管理(Session Management)
- 加密(Cryptography)
- Web支持
- 缓存支持
- 并发支持
- 核心概念:
- Subject: 当前操作的用户
- SecurityManager: 安全管理器,Shiro的核心组件
- Realm: 用于连接Shiro和实际的安全数据源
- Authentication: 身份验证
- Authorization: 授权
- Session: 会话管理
- 常用注解:
@RequiresAuthentication // 需要登录
@RequiresPermissions // 需要权限
@RequiresRoles // 需要角色
@RequiresGuest // 需要是游客
@RequiresUser // 需要是用户
- 身份认证流程:
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
currentUser.login(token);
} catch (AuthenticationException ae) {
//处理登录失败
}
}
- 权限检查示例:
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("admin")) {
//有admin角色
}
if (currentUser.isPermitted("user:create")) {
//有创建用户的权限
}
- 配置示例:
@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;
}
}
- 主要优点:
- 使用简单,容易上手
- 灵活性强,可自定义程度高
- 安全可靠
- 支持多种验证方式
- 支持分布式应用
- 常见整合:
- Spring Boot
- Redis(用于Session管理)
- JWT(Json Web Token)
- Ehcache(缓存)
- 使用建议:
- 建议使用最新版本
- 注意合理配置缓存
- 谨慎处理密码等敏感信息
- 合理规划权限体系
Shiro是一个成熟的安全框架,特别适合需要灵活权限管理的Java Web应用。它可以很好地与Spring Boot等框架集成,提供完整的安全解决方案。
用户:
我目前只对java开发有一些了解,以前更多的用的是go语言开发,我之前登录注册都是自己写的,token用的是jwt,权限这块用的RBAC设计,新建表,然后弄了一个权限管理相关的接口用于后台管理一些权限。我想知道用了Apache Shiro,比我之前的方案有哪些不同
ChatGPT:
让我从几个方面来比较一下:
- 开发效率:
- 你之前的方案:需要自己实现认证、授权逻辑,设计数据库表,开发管理接口
- 使用Shiro:提供了现成的认证、授权框架,只需要实现Realm接口对接数据源,配置相关规则即可
- 安全性:
- 你之前的方案:安全性依赖于自己的实现
- 使用Shiro:经过大量实践验证的安全框架,内置多种安全机制
- 功能完整性:
Shiro额外提供了:
- 会话管理
- 缓存管理
- 加密服务
- 并发特性
- Remember Me功能
- 多种认证策略
- 代码示例对比:
你之前的方案可能是这样:
// 登录
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
// 1. 验证用户名密码
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);
}
// 权限校验
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/data")
public Result getAdminData() {
// 业务逻辑
}
使用Shiro的方案:
// 登录
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginDTO.getUsername(), loginDTO.getPassword());
try {
// Shiro会自动处理认证逻辑
subject.login(token);
return Result.success("登录成功");
} catch (AuthenticationException e) {
return Result.fail("登录失败");
}
}
// 权限校验
@RequiresRoles("admin")
@GetMapping("/admin/data")
public Result getAdminData() {
// 业务逻辑
}
- 集成难度:
- 你之前的方案:完全自主可控,但需要自己处理各种细节
- 使用Shiro:需要学习Shiro的概念和API,但集成相对简单
- 灵活性:
- 你之前的方案:完全自定义,可以根据需求随意调整
- 使用Shiro:需要在Shiro的框架内实现,但也提供了足够的扩展点
- JWT集成:
- 你之前的方案:直接使用JWT
- 使用Shiro:需要自定义JWTToken和Realm,稍微复杂一些
- 维护成本:
- 你之前的方案:需要自己维护所有代码
- 使用Shiro:框架部分由Apache维护,你只需要维护业务相关代码
建议:
如果是小型项目,你之前的方案够用
-
如果是大型项目,建议使用Shiro:
- 更完善的安全特性
- 更好的可维护性
- 社区支持
- 与其他框架的良好集成
如果主要考虑轻量级解决方案,可以考虑Spring Security,它与Spring生态系统集成更好
-
如果你习惯了之前的方案,也可以继续使用,但建议:
- 增加更多的安全特性
- 完善异常处理
- 添加更多的日志记录
- 考虑添加缓存机制
用户:
Remember Me功能是什么?
ChatGPT:
Remember Me(记住我)功能是一种持久化的身份认证机制,允许用户在关闭浏览器后再次访问网站时无需重新登录。
工作原理:
- 当用户登录时勾选"记住我"选项
- 服务器生成一个持久化的令牌(token)
- 将令牌存储在客户端(通常是cookie)和服务器端
- 下次用户访问时,即使session已过期,系统也能通过这个令牌识别用户
Shiro中使用示例:
// 登录时启用Remember Me
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(
loginDTO.getUsername(),
loginDTO.getPassword()
);
// 设置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();
// 设置cookie
SimpleCookie cookie = new SimpleCookie("rememberMe");
cookie.setMaxAge(259200); // 3天
rememberMeManager.setCookie(cookie);
securityManager.setRememberMeManager(rememberMeManager);
return securityManager;
}
安全考虑:
- Remember Me令牌应该有过期时间
- 令牌应该是加密的
- 用户应该能够主动清除Remember Me状态
- 敏感操作仍需要重新验证
使用场景:
- 普通网站的用户便利性提升
- 非敏感系统的自动登录
- 移动应用的持久登录状态
不建议使用的场景:
- 银行等金融系统
- 需要高安全性的管理后台
- 含有敏感信息的系统
Remember Me本质上是用便利性换取部分安全性,使用时需要根据实际场景权衡。
用户:
上面提到的
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginDTO.getUsername(), loginDTO.getPassword());
try {
// Shiro会自动处理认证逻辑
subject.login(token);
return Result.success("登录成功");
} catch (AuthenticationException e) {
return Result.fail("登录失败");
}
这个实际执行了什么操作呢?
ChatGPT:
这段代码背后的执行流程比较复杂,我来详细解释一下:
- 首先,
SecurityUtils.getSubject()
获取当前用户主体:
Subject subject = SecurityUtils.getSubject();
- 这会从SecurityManager中获取当前用户的Subject实例
- Subject代表当前用户,可以是已认证的用户,也可以是匿名用户
- 创建认证令牌:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
- 这是一个封装了用户名和密码的认证令牌
- 密码会被转换为字符数组,提高安全性
- 当执行
subject.login(token)
时,会触发以下流程:
public void login(AuthenticationToken token) throws AuthenticationException {
// 1. 获取SecurityManager
SecurityManager securityManager = getSecurityManager();
// 2. 执行实际的登录逻辑
securityManager.login(this, token);
}
- SecurityManager会调用Authenticator进行认证:
// 在SecurityManager内部
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. 调用Realm进行认证
AuthenticationInfo info = realm.getAuthenticationInfo(token);
// 5. 验证返回结果
// ...
}
}
}
- 在自定义的Realm中进行实际的认证:
public class CustomRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
// 1. 转换token
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// 2. 从数据库查询用户
User user = userService.findByUsername(username);
if (user == null) {
throw new UnknownAccountException();
}
// 3. 返回认证信息
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. 进行密码匹配(可能包含加密、hash等操作)
return equals(submittedPassword, storedCredentials);
}
- 如果认证成功:
- 创建新的会话(Session)
- 缓存认证信息
- 执行记住我(如果启用)
- 触发认证成功事件
- 如果认证失败:
- 抛出相应的异常:
- UnknownAccountException(账号不存在)
- IncorrectCredentialsException(密码错误)
- LockedAccountException(账号锁定)
- ExcessiveAttemptsException(尝试次数过多)
等等
完整的调用链大致是:
Controller
→ Subject.login()
→ SecurityManager
→ Authenticator
→ Realm
→ UserService/DAO
→ 数据库
这个过程的优点是:
- 解耦了认证逻辑
- 支持多种认证方式
- 提供了完整的异常体系
- 可以方便地扩展和定制
使用时需要注意:
- 正确配置Realm
- 合理处理异常
- 考虑添加密码加密
- 可以配置多个Realm实现多种认证方式
用户:
如果用了这个框架,我还需要单独实现RBAC,还需要创建那些管理接口么?
ChatGPT:
使用Shiro时,你仍然需要实现RBAC的数据模型和管理接口,但是权限验证的逻辑可以交给Shiro处理。我来详细说明:
- 数据库设计:
仍然需要基本的RBAC表结构:
-- 用户表
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)
);
-- 权限表
CREATE TABLE permission (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
description VARCHAR(200)
);
-- 用户-角色关联表
CREATE TABLE user_role (
user_id BIGINT,
role_id BIGINT
);
-- 角色-权限关联表
CREATE TABLE role_permission (
role_id BIGINT,
permission_id BIGINT
);
- 自定义Realm实现:
@Component
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
// ... 认证逻辑
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 从数据库获取用户角色和权限
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;
// 创建角色
@RequiresPermissions("role:create")
@PostMapping("/role")
public Result createRole(@RequestBody Role role) {
roleService.create(role);
return Result.success();
}
// 分配角色权限
@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();
}
}
- 服务层示例:
@Service
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleMapper roleMapper;
@Override
@Transactional
public void assignPermissions(Long roleId, List<Long> permissionIds) {
// 1. 删除原有权限
roleMapper.deleteRolePermissions(roleId);
// 2. 添加新权限
roleMapper.insertRolePermissions(roleId, permissionIds);
// 3. 清除相关的权限缓存
clearCache(roleId);
}
}
- 权限使用示例:
// 注解方式
@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;
}
}
主要区别:
- 不需要自己实现权限验证逻辑
- 可以使用Shiro的注解或API进行权限控制
- 可以利用Shiro的缓存机制
- 权限管理更加标准化
仍然需要:
- 设计和实现RBAC数据模型
- 实现权限管理接口
- 维护角色和权限数据
- 实现用户-角色-权限的分配逻辑
建议:
- 使用缓存来提高性能
- 实现细粒度的权限控制
- 做好权限变更后的缓存清理
- 考虑权限的层级结构
- 实现完善的权限管理界面
总的来说,Shiro帮你处理了权限验证的逻辑,但具体的RBAC实现和管理还是需要你自己完成。这样的分工可以让你专注于业务逻辑,而将通用的安全控制交给框架处理。
用户:
Subject subject = SecurityUtils.getSubject();
这一行我不太懂,我之前看如果用户访问一个接口的话,如果这个接口需要验证token,都是在拦截器里面自己解析的,解析出来得到user_id和其他信息,然后再做权限校验,根据情况选择是否抛出异常,上面这一行是自动做了这些事情么?还是我理解的不对?
ChatGPT:
你说得对,这里确实需要解释清楚。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;
}
}
- 创建一个处理 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不能为空");
}
// 创建 JWTToken
JWTToken jwtToken = new JWTToken(token);
// 提交给 realm 进行登入,如果错误他会抛出异常并被捕获
try {
// 这里就是调用 Subject,但是是在过滤器中调用的
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 {
// 执行登录
executeLogin(request, response);
return true;
} catch (Exception e) {
// token 错误
return false;
}
}
// 如果请求头不存在 token,则可能是执行登陆操作或者是游客状态访问
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();
// 设置用户角色和权限
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);
// 设置过滤器链
Map<String, String> filterRuleMap = new HashMap<>();
filterRuleMap.put("/login", "anon"); // 登录接口放行
filterRuleMap.put("/**", "jwt"); // 其他所有请求都需要JWT验证
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
}
工作流程:
- 请求进入系统
- 经过 JWTFilter
- JWTFilter 从请求头获取 token
- JWTFilter 创建 JWTToken 并调用
subject.login()
- Shiro 将认证请求转发给 Realm
- Realm 解析 token,验证有效性
- 认证成功后,用户信息会存储在 Subject 中
然后在 Controller 中:
@GetMapping("/user/info")
@RequiresPermissions("user:view")
public Result getUserInfo() {
// 这时候调用 getSubject() 就能获取到当前用户信息了
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
return Result.success(user);
}
所以你说得对,SecurityUtils.getSubject()
本身并不会解析 token,它只是获取当前用户的上下文信息。真正的 token 解析和验证是在过滤器和 Realm 中完成的。
这个流程和你之前的实现方式类似,主要区别是:
- 把验证逻辑封装在了 Realm 中
- 使用 Shiro 的过滤器链来管理请求
- 可以利用 Shiro 提供的注解和 API 来做权限控制
你之前的实现方式也是完全可行的,使用 Shiro 主要是为了:
- 提供统一的安全框架
- 更好的可扩展性
- 更多的内置功能
- 更规范的实现方式
用户:
我看你说支持多种认证方式,这个是什么意思?
ChatGPT:
支持多种认证方式意味着你可以在同一个系统中配置多个 Realm,每个 Realm 负责不同的认证方式。我来举几个例子:
- 同时支持用户名密码和JWT认证:
// 用户名密码认证的 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();
// 验证用户名密码
// ...
}
}
// JWT认证的 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;
// 验证JWT
// ...
}
}
// 配置多个Realm
@Configuration
public class ShiroConfig {
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 创建认证器
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
// 设置认证策略,ALL必须所有Realm都通过,ANY只需要其中一个通过
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
// 设置多个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;
// 验证微信登录
// ...
}
}
// 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;
// 验证GitHub登录
// ...
}
}
- 同时支持数据库和LDAP认证:
// 数据库认证的 Realm
public class DatabaseRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
// 从数据库验证用户
// ...
}
}
// LDAP认证的 Realm
public class LdapRealm extends AuthorizingRealm {
@Autowired
private LdapTemplate ldapTemplate;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
// 通过LDAP验证用户
// ...
}
}
- 在Controller中使用不同的认证方式:
@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();
}
}
优点:
- 可以灵活组合不同的认证方式
- 统一的认证框架
- 代码复用性好
- 易于扩展
使用场景:
- 支持多种登录方式的系统
- 需要兼容新旧认证方式的系统
- 企业级应用需要同时支持多种认证方式
- 需要集成第三方认证的系统
注意事项:
- 合理配置认证策略
- 处理好多种认证方式的优先级
- 注意不同认证方式的安全级别
- 做好用户身份的统一管理
用户:
AuthorizingRealm是一个什么都没有实现的类么,我发现上面全是继承这个的?
ChatGPT:
AuthorizingRealm 是一个抽象类,它实现了 Realm 接口,并提供了认证(Authentication)和授权(Authorization)的基础框架。它不是一个空的类,而是提供了很多基础实现。
让我们看看它的主要结构:
public abstract class AuthorizingRealm extends AuthenticatingRealm {
// 缓存管理
private AuthorizationCache<Object, AuthorizationInfo> authorizationCache;
// 主要方法
public AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
// 1. 检查参数
if (principals == null) {
return null;
}
// 2. 尝试从缓存获取
if (authorizationCache != null) {
AuthorizationInfo cached = authorizationCache.get(principals);
if (cached != null) {
return cached;
}
}
// 3. 调用子类实现的方法获取授权信息
AuthorizationInfo info = doGetAuthorizationInfo(principals);
// 4. 缓存结果
if (info != null && authorizationCache != null) {
authorizationCache.put(principals, info);
}
return info;
}
// 子类需要实现的抽象方法
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
}
// AuthenticatingRealm(父类)也提供了很多实现
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. 调用子类实现的方法获取认证信息
info = doGetAuthenticationInfo(token);
// 3. 缓存结果
if (info != null) {
cacheAuthenticationInfo(token, info);
}
return info;
}
// 子类需要实现的抽象方法
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() {
// 设置密码匹配器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");
matcher.setHashIterations(1024);
setCredentialsMatcher(matcher);
}
}
- 权限字符串解析:
public class CustomRealm extends AuthorizingRealm {
@Override
public boolean isPermitted(PrincipalCollection principals, String permission) {
// AuthorizingRealm已经实现了权限字符串的解析
return super.isPermitted(principals, permission);
}
}
- 角色检查:
public class CustomRealm extends AuthorizingRealm {
@Override
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
// AuthorizingRealm已经实现了角色检查的逻辑
return super.hasRole(principals, roleIdentifier);
}
}
我们只需要实现两个核心方法:
- doGetAuthenticationInfo:处理认证
@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:处理授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置角色和权限
info.setRoles(userService.getRoles(user.getId()));
info.setStringPermissions(userService.getPermissions(user.getId()));
return info;
}
所以 AuthorizingRealm 不是空类,而是:
- 提供了完整的认证授权框架
- 实现了缓存管理
- 实现了凭证匹配
- 提供了权限检查的基础实现
- 只需要我们实现核心的认证和授权逻辑
这种设计模式是典型的模板方法模式,父类定义框架,子类实现具体细节。
用户:
我看你之前给的案例里面有一个这个注解:@RequiresRoles("admin"),这个admin是RBAC里面的角色么?
ChatGPT:
是的,@RequiresRoles("admin")
中的 "admin" 就是对应 RBAC 中角色表里的角色标识。让我用一个完整的例子来说明:
- 首先是数据库中的角色表:
CREATE TABLE role (
id BIGINT PRIMARY KEY,
code VARCHAR(100), -- 角色标识,如 "admin"
name VARCHAR(100), -- 角色名称,如 "管理员"
description VARCHAR(200)
);
-- 示例数据
INSERT INTO role (id, code, name, description) VALUES
(1, 'admin', '管理员', '系统管理员'),
(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 中关联角色:
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 从数据库获取用户角色
Set<String> roles = userService.getUserRoles(user.getId());
// 这里的 roles 可能包含 "admin", "user" 等角色标识
info.setRoles(roles);
return info;
}
}
- Service层实现:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
public Set<String> getUserRoles(Long userId) {
// 查询用户角色关联表,获取角色标识
List<Role> roles = userMapper.getUserRoles(userId);
return roles.stream()
.map(Role::getCode) // 获取角色标识
.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 角色
}
// 检查多个角色并获取结果
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) {
// 当用户没有所需角色时抛出此异常
return Result.fail("没有所需的角色权限");
}
}
注意事项:
- 角色标识(如"admin")最好使用统一的常量管理:
public class RoleConstants {
public static final String ADMIN = "admin";
public static final String USER = "user";
public static final String MANAGER = "manager";
}
- 可以配合权限使用:
@RequiresRoles("admin")
@RequiresPermissions("user:create")
@PostMapping("/user")
public Result createUser(@RequestBody User user) {
// 需要同时具有 admin 角色和创建用户的权限
return userService.createUser(user);
}
- 建议角色和权限的关系:
- 角色是权限的集合
- 通常粗粒度的控制用角色
- 细粒度的控制用权限
- 优先使用权限控制,而不是角色控制
- 角色标识的命名建议:
- 使用小写字母
- 用下划线或中划线分隔单词
- 具有业务含义
- 保持简短但清晰
例如:
// 好的命名
"admin"
"content_editor"
"system_manager"
// 不好的命名
"ADMIN"
"role1"
"r_01"
用户:
你的意思,我尽量不使用这种注解?而是直接在自定义的Realm中获取到角色对应的API权限列表,然后用代码判断么?
ChatGPT:
是的,你理解得对。使用基于权限(Permission)的控制比基于角色(Role)的控制更灵活和可维护。我来解释下原因和具体实现:
- 基于角色的控制存在的问题:
// 不够灵活
@RequiresRoles("admin")
@GetMapping("/users")
public Result listUsers() {
return userService.listUsers();
}
- 角色和权限强耦合
- 修改角色权限需要修改代码
- 难以细粒度控制
- 权限分配不够直观
- 基于权限的控制更好的方式:
// 更清晰的表达权限需求
@RequiresPermissions("user:view")
@GetMapping("/users")
public Result listUsers() {
return userService.listUsers();
}
- 在Realm中实现权限控制:
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());
// 获取所有角色对应的权限
Set<String> permissions = permissionService.getPermissionsByRoles(roles);
// 也可以获取用户直接关联的权限
permissions.addAll(permissionService.getUserPermissions(user.getId()));
// 设置权限
info.setStringPermissions(permissions);
return info;
}
}
- 权限表设计:
-- 权限表
CREATE TABLE permission (
id BIGINT PRIMARY KEY,
code VARCHAR(100), -- 权限标识,如 "user:view"
name VARCHAR(100), -- 权限名称,如 "查看用户"
type VARCHAR(50), -- 权限类型,如 "api", "menu", "button"
url VARCHAR(200), -- API路径,如 "/api/users"
method VARCHAR(10), -- HTTP方法,如 "GET"
description VARCHAR(200)
);
-- 角色权限关联表
CREATE TABLE role_permission (
role_id BIGINT,
permission_id BIGINT
);
- 权限服务实现:
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private PermissionMapper permissionMapper;
public Set<String> getPermissionsByRoles(Set<String> roles) {
// 查询这些角色关联的所有权限
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) {
// 根据URL和方法查找需要的权限
Permission permission = permissionMapper.findByUrlAndMethod(url, method);
if (permission == null) {
return false;
}
// 检查用户是否有这个权限
return userPermissions.contains(permission.getCode());
}
}
- 可以自定义过滤器进行权限检查:
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();
// 获取用户权限
SimpleAuthorizationInfo info = (SimpleAuthorizationInfo) subject.getSession()
.getAttribute("authorizationInfo");
Set<String> permissions = info.getStringPermissions();
// 检查权限
return permissionService.checkPermission(url, method, permissions);
}
}
- 权限标识的命名规范:
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";
// ...
}
这种方式的优点:
- 解耦了角色和权限
- 可以动态调整权限
- 支持细粒度控制
- 便于维护和扩展
- 权限分配更直观
- 支持动态权限验证
使用建议:
- 设计合理的权限粒度
- 建立清晰的权限命名规范
- 做好权限的分类管理
- 考虑权限的缓存策略
- 实现完善的权限管理界面
这样的实现更适合企业级应用,特别是需要灵活权限控制的系统。