Apache Shiro

新的聊天

用户:

你知道Apache Shiro么?

ChatGPT:

是的,我了解Apache Shiro。它是一个功能强大且易于使用的Java安全框架,主要用于身份认证、授权、加密和会话管理。以下是Shiro的主要特点和核心概念:

  1. 主要功能:
  • 身份认证(Authentication) - 验证用户身份
  • 授权(Authorization) - 访问控制
  • 会话管理(Session Management)
  • 加密(Cryptography)
  • Web支持
  • 缓存支持
  • 并发支持
  1. 核心概念:
  • Subject: 当前操作的用户
  • SecurityManager: 安全管理器,Shiro的核心组件
  • Realm: 用于连接Shiro和实际的安全数据源
  • Authentication: 身份验证
  • Authorization: 授权
  • Session: 会话管理
  1. 常用注解:
@RequiresAuthentication  // 需要登录
@RequiresPermissions    // 需要权限
@RequiresRoles         // 需要角色
@RequiresGuest        // 需要是游客
@RequiresUser        // 需要是用户
  1. 身份认证流程:
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        currentUser.login(token);
    } catch (AuthenticationException ae) {
        //处理登录失败
    }
}
  1. 权限检查示例:
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("admin")) {
    //有admin角色
}
if (currentUser.isPermitted("user:create")) {
    //有创建用户的权限
}
  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. 主要优点:
  • 使用简单,容易上手
  • 灵活性强,可自定义程度高
  • 安全可靠
  • 支持多种验证方式
  • 支持分布式应用
  1. 常见整合:
  • Spring Boot
  • Redis(用于Session管理)
  • JWT(Json Web Token)
  • Ehcache(缓存)
  1. 使用建议:
  • 建议使用最新版本
  • 注意合理配置缓存
  • 谨慎处理密码等敏感信息
  • 合理规划权限体系

Shiro是一个成熟的安全框架,特别适合需要灵活权限管理的Java Web应用。它可以很好地与Spring Boot等框架集成,提供完整的安全解决方案。

用户:

我目前只对java开发有一些了解,以前更多的用的是go语言开发,我之前登录注册都是自己写的,token用的是jwt,权限这块用的RBAC设计,新建表,然后弄了一个权限管理相关的接口用于后台管理一些权限。我想知道用了Apache Shiro,比我之前的方案有哪些不同

ChatGPT:

让我从几个方面来比较一下:

  1. 开发效率:
  • 你之前的方案:需要自己实现认证、授权逻辑,设计数据库表,开发管理接口
  • 使用Shiro:提供了现成的认证、授权框架,只需要实现Realm接口对接数据源,配置相关规则即可
  1. 安全性:
  • 你之前的方案:安全性依赖于自己的实现
  • 使用Shiro:经过大量实践验证的安全框架,内置多种安全机制
  1. 功能完整性:
    Shiro额外提供了:
  • 会话管理
  • 缓存管理
  • 加密服务
  • 并发特性
  • Remember Me功能
  • 多种认证策略
  1. 代码示例对比:

你之前的方案可能是这样:

// 登录
@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() {
    // 业务逻辑
}
  1. 集成难度:
  • 你之前的方案:完全自主可控,但需要自己处理各种细节
  • 使用Shiro:需要学习Shiro的概念和API,但集成相对简单
  1. 灵活性:
  • 你之前的方案:完全自定义,可以根据需求随意调整
  • 使用Shiro:需要在Shiro的框架内实现,但也提供了足够的扩展点
  1. JWT集成:
  • 你之前的方案:直接使用JWT
  • 使用Shiro:需要自定义JWTToken和Realm,稍微复杂一些
  1. 维护成本:
  • 你之前的方案:需要自己维护所有代码
  • 使用Shiro:框架部分由Apache维护,你只需要维护业务相关代码

建议:

  1. 如果是小型项目,你之前的方案够用

  2. 如果是大型项目,建议使用Shiro:

    • 更完善的安全特性
    • 更好的可维护性
    • 社区支持
    • 与其他框架的良好集成
  3. 如果主要考虑轻量级解决方案,可以考虑Spring Security,它与Spring生态系统集成更好

  4. 如果你习惯了之前的方案,也可以继续使用,但建议:

    • 增加更多的安全特性
    • 完善异常处理
    • 添加更多的日志记录
    • 考虑添加缓存机制

用户:

Remember Me功能是什么?

ChatGPT:

Remember Me(记住我)功能是一种持久化的身份认证机制,允许用户在关闭浏览器后再次访问网站时无需重新登录。

工作原理:

  1. 当用户登录时勾选"记住我"选项
  2. 服务器生成一个持久化的令牌(token)
  3. 将令牌存储在客户端(通常是cookie)和服务器端
  4. 下次用户访问时,即使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;
}

安全考虑:

  1. Remember Me令牌应该有过期时间
  2. 令牌应该是加密的
  3. 用户应该能够主动清除Remember Me状态
  4. 敏感操作仍需要重新验证

使用场景:

  • 普通网站的用户便利性提升
  • 非敏感系统的自动登录
  • 移动应用的持久登录状态

不建议使用的场景:

  • 银行等金融系统
  • 需要高安全性的管理后台
  • 含有敏感信息的系统

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:

这段代码背后的执行流程比较复杂,我来详细解释一下:

  1. 首先,SecurityUtils.getSubject() 获取当前用户主体:
Subject subject = SecurityUtils.getSubject();
  • 这会从SecurityManager中获取当前用户的Subject实例
  • Subject代表当前用户,可以是已认证的用户,也可以是匿名用户
  1. 创建认证令牌:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  • 这是一个封装了用户名和密码的认证令牌
  • 密码会被转换为字符数组,提高安全性
  1. 当执行 subject.login(token) 时,会触发以下流程:
public void login(AuthenticationToken token) throws AuthenticationException {
    // 1. 获取SecurityManager
    SecurityManager securityManager = getSecurityManager();
    
    // 2. 执行实际的登录逻辑
    securityManager.login(this, token);
}
  1. 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. 验证返回结果
            // ...
        }
    }
}
  1. 在自定义的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
        );
    }
}
  1. 密码匹配过程:
// 在CredentialsMatcher中
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    // 1. 获取用户输入的密码
    char[] submittedPassword = getSubmittedPassword(token);
    
    // 2. 获取存储的密码
    Object storedCredentials = getStoredPassword(info);
    
    // 3. 进行密码匹配(可能包含加密、hash等操作)
    return equals(submittedPassword, storedCredentials);
}
  1. 如果认证成功:
  • 创建新的会话(Session)
  • 缓存认证信息
  • 执行记住我(如果启用)
  • 触发认证成功事件
  1. 如果认证失败:
  • 抛出相应的异常:
    • UnknownAccountException(账号不存在)
    • IncorrectCredentialsException(密码错误)
    • LockedAccountException(账号锁定)
    • ExcessiveAttemptsException(尝试次数过多)
      等等

完整的调用链大致是:

Controller 
    → Subject.login() 
    → SecurityManager 
    → Authenticator 
    → Realm 
    → UserService/DAO 
    → 数据库

这个过程的优点是:

  1. 解耦了认证逻辑
  2. 支持多种认证方式
  3. 提供了完整的异常体系
  4. 可以方便地扩展和定制

使用时需要注意:

  1. 正确配置Realm
  2. 合理处理异常
  3. 考虑添加密码加密
  4. 可以配置多个Realm实现多种认证方式

用户:

如果用了这个框架,我还需要单独实现RBAC,还需要创建那些管理接口么?

ChatGPT:

使用Shiro时,你仍然需要实现RBAC的数据模型和管理接口,但是权限验证的逻辑可以交给Shiro处理。我来详细说明:

  1. 数据库设计:
    仍然需要基本的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
);
  1. 自定义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;
    }
}
  1. 管理接口示例:
@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();
    }
}
  1. 服务层示例:
@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);
    }
}
  1. 权限使用示例:
// 注解方式
@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;
    }
}

主要区别:

  1. 不需要自己实现权限验证逻辑
  2. 可以使用Shiro的注解或API进行权限控制
  3. 可以利用Shiro的缓存机制
  4. 权限管理更加标准化

仍然需要:

  1. 设计和实现RBAC数据模型
  2. 实现权限管理接口
  3. 维护角色和权限数据
  4. 实现用户-角色-权限的分配逻辑

建议:

  1. 使用缓存来提高性能
  2. 实现细粒度的权限控制
  3. 做好权限变更后的缓存清理
  4. 考虑权限的层级结构
  5. 实现完善的权限管理界面

总的来说,Shiro帮你处理了权限验证的逻辑,但具体的RBAC实现和管理还是需要你自己完成。这样的分工可以让你专注于业务逻辑,而将通用的安全控制交给框架处理。

用户:

Subject subject = SecurityUtils.getSubject();

这一行我不太懂,我之前看如果用户访问一个接口的话,如果这个接口需要验证token,都是在拦截器里面自己解析的,解析出来得到user_id和其他信息,然后再做权限校验,根据情况选择是否抛出异常,上面这一行是自动做了这些事情么?还是我理解的不对?

ChatGPT:

你说得对,这里确实需要解释清楚。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. 创建一个处理 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;
    }
}
  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();
        
        // 设置用户角色和权限
        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);

        // 设置过滤器链
        Map<String, String> filterRuleMap = new HashMap<>();
        filterRuleMap.put("/login", "anon"); // 登录接口放行
        filterRuleMap.put("/**", "jwt");     // 其他所有请求都需要JWT验证
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        
        return factoryBean;
    }
}

工作流程:

  1. 请求进入系统
  2. 经过 JWTFilter
  3. JWTFilter 从请求头获取 token
  4. JWTFilter 创建 JWTToken 并调用 subject.login()
  5. Shiro 将认证请求转发给 Realm
  6. Realm 解析 token,验证有效性
  7. 认证成功后,用户信息会存储在 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 中完成的。

这个流程和你之前的实现方式类似,主要区别是:

  1. 把验证逻辑封装在了 Realm 中
  2. 使用 Shiro 的过滤器链来管理请求
  3. 可以利用 Shiro 提供的注解和 API 来做权限控制

你之前的实现方式也是完全可行的,使用 Shiro 主要是为了:

  1. 提供统一的安全框架
  2. 更好的可扩展性
  3. 更多的内置功能
  4. 更规范的实现方式

用户:

我看你说支持多种认证方式,这个是什么意思?

ChatGPT:

支持多种认证方式意味着你可以在同一个系统中配置多个 Realm,每个 Realm 负责不同的认证方式。我来举几个例子:

  1. 同时支持用户名密码和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;
    }
}
  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;
        // 验证微信登录
        // ...
    }
}

// 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登录
        // ...
    }
}
  1. 同时支持数据库和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验证用户
        // ...
    }
}
  1. 在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();
    }
}

优点:

  1. 可以灵活组合不同的认证方式
  2. 统一的认证框架
  3. 代码复用性好
  4. 易于扩展

使用场景:

  1. 支持多种登录方式的系统
  2. 需要兼容新旧认证方式的系统
  3. 企业级应用需要同时支持多种认证方式
  4. 需要集成第三方认证的系统

注意事项:

  1. 合理配置认证策略
  2. 处理好多种认证方式的优先级
  3. 注意不同认证方式的安全级别
  4. 做好用户身份的统一管理

用户:

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 提供的功能包括:

  1. 缓存支持:
// 使用缓存
public class CustomRealm extends AuthorizingRealm {
    @Override
    public void setCacheManager(CacheManager cacheManager) {
        super.setCacheManager(cacheManager);
    }
}
  1. 凭证匹配:
public class CustomRealm extends AuthorizingRealm {
    public CustomRealm() {
        // 设置密码匹配器
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(1024);
        setCredentialsMatcher(matcher);
    }
}
  1. 权限字符串解析:
public class CustomRealm extends AuthorizingRealm {
    @Override
    public boolean isPermitted(PrincipalCollection principals, String permission) {
        // AuthorizingRealm已经实现了权限字符串的解析
        return super.isPermitted(principals, permission);
    }
}
  1. 角色检查:
public class CustomRealm extends AuthorizingRealm {
    @Override
    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        // AuthorizingRealm已经实现了角色检查的逻辑
        return super.hasRole(principals, roleIdentifier);
    }
}

我们只需要实现两个核心方法:

  1. 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
    );
}
  1. 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 不是空类,而是:

  1. 提供了完整的认证授权框架
  2. 实现了缓存管理
  3. 实现了凭证匹配
  4. 提供了权限检查的基础实现
  5. 只需要我们实现核心的认证和授权逻辑

这种设计模式是典型的模板方法模式,父类定义框架,子类实现具体细节。

用户:

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

ChatGPT:

是的,@RequiresRoles("admin") 中的 "admin" 就是对应 RBAC 中角色表里的角色标识。让我用一个完整的例子来说明:

  1. 首先是数据库中的角色表:
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', '普通用户', '普通用户');
  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 中关联角色:
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;
    }
}
  1. 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());
    }
}
  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 角色
        }
        
        // 检查多个角色并获取结果
        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) {
        // 当用户没有所需角色时抛出此异常
        return Result.fail("没有所需的角色权限");
    }
}

注意事项:

  1. 角色标识(如"admin")最好使用统一的常量管理:
public class RoleConstants {
    public static final String ADMIN = "admin";
    public static final String USER = "user";
    public static final String MANAGER = "manager";
}
  1. 可以配合权限使用:
@RequiresRoles("admin")
@RequiresPermissions("user:create")
@PostMapping("/user")
public Result createUser(@RequestBody User user) {
    // 需要同时具有 admin 角色和创建用户的权限
    return userService.createUser(user);
}
  1. 建议角色和权限的关系:
  • 角色是权限的集合
  • 通常粗粒度的控制用角色
  • 细粒度的控制用权限
  • 优先使用权限控制,而不是角色控制
  1. 角色标识的命名建议:
  • 使用小写字母
  • 用下划线或中划线分隔单词
  • 具有业务含义
  • 保持简短但清晰

例如:

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

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

用户:

你的意思,我尽量不使用这种注解?而是直接在自定义的Realm中获取到角色对应的API权限列表,然后用代码判断么?

ChatGPT:

是的,你理解得对。使用基于权限(Permission)的控制比基于角色(Role)的控制更灵活和可维护。我来解释下原因和具体实现:

  1. 基于角色的控制存在的问题:
// 不够灵活
@RequiresRoles("admin")
@GetMapping("/users")
public Result listUsers() {
    return userService.listUsers();
}
  • 角色和权限强耦合
  • 修改角色权限需要修改代码
  • 难以细粒度控制
  • 权限分配不够直观
  1. 基于权限的控制更好的方式:
// 更清晰的表达权限需求
@RequiresPermissions("user:view")
@GetMapping("/users")
public Result listUsers() {
    return userService.listUsers();
}
  1. 在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;
    }
}
  1. 权限表设计:
-- 权限表
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
);
  1. 权限服务实现:
@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());
    }
}
  1. 可以自定义过滤器进行权限检查:
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);
    }
}
  1. 权限标识的命名规范:
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";
    // ...
}

这种方式的优点:

  1. 解耦了角色和权限
  2. 可以动态调整权限
  3. 支持细粒度控制
  4. 便于维护和扩展
  5. 权限分配更直观
  6. 支持动态权限验证

使用建议:

  1. 设计合理的权限粒度
  2. 建立清晰的权限命名规范
  3. 做好权限的分类管理
  4. 考虑权限的缓存策略
  5. 实现完善的权限管理界面

这样的实现更适合企业级应用,特别是需要灵活权限控制的系统。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容