Bootstrap

Spring Boot 集成 Shiro:会话管理、加密与登录次数限制

在当今的 Web 应用开发中,Spring Boot 以其便捷的开发体验被广泛应用,而安全问题始终是重中之重。Shiro 与 Spring Boot 的结合能够为应用提供强大的安全防护。本文将详细阐述如何在 Spring Boot 环境下实现 Shiro 的会话管理、加密以及登录次数限制功能。

一、Spring Boot 与 Shiro 集成准备

(一)项目搭建与依赖引入

使用 Spring Initializr 创建一个 Spring Boot 项目,在 pom.xml 文件中引入 Shiro 的相关依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.7.1</version>
</dependency>

(二)Shiro 基本配置

创建一个 Shiro 配置类,用于配置 Shiro 的核心组件。首先,定义 SecurityManager

@Configuration
public class ShiroConfig {

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm());
        return securityManager;
    }

    @Bean
    public Realm myRealm() {
        return new MyRealm();
    }
}

这里的 MyRealm 是我们自定义的 Realm,用于从数据源获取用户认证和授权信息。

二、会话管理

(一)会话超时设置

在 Spring Boot 中,通过在 application.properties 或 application.yml 文件中配置 Shiro 会话超时时间:

application.yml 配置示例:

shiro:
  session:
    globalSessionTimeout: 1800000 # 30分钟,单位毫秒

(二)会话验证调度器与监听

在 Shiro 配置类中配置会话验证调度器和会话监听器:

@Bean
public DefaultWebSessionManager sessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionValidationSchedulerEnabled(true);
    sessionManager.setSessionIdCookieEnabled(true);
    // 配置会话监听器
    Collection<SessionListener> listeners = new ArrayList<>();
    listeners.add(new MySessionListener());
    sessionManager.setSessionListeners(listeners);
    return sessionManager;
}

public class MySessionListener implements SessionListener {
    @Override
    public void onStart(Session session) {
        // 会话创建时的逻辑,如记录日志等
    }

    @Override
    public void onStop(Session session) {
        // 会话停止时的逻辑
    }

    @Override
    public void onExpiration(Session session) {
        // 会话过期时的逻辑,比如清理相关资源
    }
}

三、加密

(一)选择加密算法并配置

在 Shiro 配置类中配置加密算法和相关参数:

@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    matcher.setHashAlgorithmName("md5");
    matcher.setHashIterations(2);
    return matcher;
}

(二)在 Realm 中应用加密

在自定义的 MyRealm 类中,实现认证方法时使用加密逻辑:

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        User user = userService.getUserByUsername(username);
        if (user == null) {
            throw new UnknownAccountException();
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                user.getUsername(),
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()), // 假设用户有盐值,用于加密
                getName());
        return info;
    }
}

这里假设用户实体类 User 中有密码和盐值相关的字段,Shiro 会根据配置的加密算法和盐值进行密码验证。

四、登录次数限制

(一)计数器存储与缓存配置

在 Spring Boot 中,可以使用内置的缓存机制或者集成其他缓存框架(如 Redis)来存储登录失败次数。以使用 Spring Boot 内置缓存为例:

在 pom.xml 中添加缓存依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

在 application.yml 中启用缓存:

spring:
  cache:
    type: simple

(二)登录次数限制逻辑

在 MyRealm 的认证方法中添加登录次数限制逻辑:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String username = (String) token.getPrincipal();
    Integer loginFailCount = (Integer) cacheManager().getCache("loginFailCache").get(username, Integer.class);
    if (loginFailCount!= null && loginFailCount >= 5) {
        throw new LockedAccountException("用户已被锁定,请稍后再试");
    }
    try {
        User user = userService.getUserByUsername(username);
        if (user == null) {
            throw new UnknownAccountException();
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                user.getUsername(),
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()),
                getName());
        return info;
    } catch (AuthenticationException e) {
        if (loginFailCount == null) {
            loginFailCount = 1;
        } else {
            loginFailCount++;
        }
        cacheManager().getCache("loginFailCache").put(username, loginFailCount);
        throw e;
    }
}

通过上述步骤,我们在 Spring Boot 集成 Shiro 的项目中成功实现了会话管理、加密和登录次数限制功能,为应用的安全保驾护航。同时,这些配置可以根据实际业务需求进行进一步的调整和优化。

 

五、安全增强与优化

(一)会话并发控制

在 Spring Boot 集成 Shiro 的环境下,实现会话并发控制可以通过自定义会话管理器来完成。

@Bean
public DefaultWebSessionManager sessionManager() {
    CustomSessionManager customSessionManager = new CustomSessionManager();
    customSessionManager.setSessionValidationSchedulerEnabled(true);
    customSessionManager.setSessionIdCookieEnabled(true);
    return customSessionManager;
}

class CustomSessionManager extends DefaultWebSessionManager {

    private Map<String, Set<String>> userSessionMap = new ConcurrentHashMap<>();

    @Override
    protected Serializable createSessionKey(ServletRequest request, ServletResponse response) {
        Serializable sessionId = super.createSessionKey(request, response);
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            String username = (String) subject.getPrincipal();
            Set<String> sessionIds = userSessionMap.get(username);
            if (sessionIds == null) {
                sessionIds = new HashSet<>();
                userSessionMap.put(username, sessionIds);
            }
            sessionIds.add(sessionId.toString());
            if (sessionIds.size() > 2) { // 假设允许每个用户最多2个并发会话
                // 找到要踢出的会话ID,这里简单示例为最早创建的会话
                String sessionToKick = sessionIds.stream().findFirst().get();
                Session session = getSession(sessionToKick);
                if (session!= null) {
                    session.stop();
                }
                sessionIds.remove(sessionToKick);
            }
        }
        return sessionId;
    }
}

(二)加密密钥动态加载

对于加密密钥,为了提高安全性,可以从配置中心或环境变量中动态加载。

例如,使用 Spring Cloud Config 从配置中心获取加密密钥。首先,在 pom.xml 中添加相关依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>

然后,在启动类上添加 @EnableConfigServer 注解(如果是配置中心客户端)。在 Shiro 配置类中修改加密算法的密钥设置:

@Value("${shiro.encryption.key}")
private String encryptionKey;

@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    matcher.setHashAlgorithmName("md5");
    matcher.setHashIterations(2);
    matcher.setHashSalted(true);
    matcher.setPrivateSalt(new SimpleByteSource(encryptionKey));
    return matcher;
}

在配置中心设置 shiro.encryption.key 的值,这样可以方便地更新密钥而无需重新部署应用。

(三)登录失败后的安全措施扩展

除了简单的登录次数限制和验证码机制,还可以在用户多次登录失败后记录相关的登录信息,如登录 IP、时间等,以便进行安全审计。

在 MyRealm 类中,当登录失败时:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //... 前面的登录次数限制逻辑

    try {
        //... 认证逻辑
    } catch (AuthenticationException e) {
        //... 登录次数增加逻辑

        // 记录登录失败信息
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getRemoteAddr();
        LoginFailureRecord record = new LoginFailureRecord(username, ip, new Date());
        loginFailureRecordService.save(record);

        throw e;
    }
}

同时,可以创建一个定时任务,定期清理过期的登录失败记录,以避免数据量过大。

六、与 Spring Security 在 Spring Boot 中的对比及选择

(一)功能特性对比

  • 认证方式多样性:Spring Security 在 OAuth、OpenID 等现代认证协议的支持上更为完善,适合需要与第三方认证平台集成的应用。Shiro 则在传统的用户名 / 密码认证以及基于表单的认证方面表现出色,且配置相对简洁。
  • 授权模型:Spring Security 的授权模型与 Spring 框架的整合度高,特别是在基于方法级别的权限控制上,通过注解等方式可以很方便地实现。Shiro 的授权模型更侧重于基于角色和权限字符串的控制,对于简单的权限管理场景更加直观。
  • 会话管理:两者都有完善的会话管理功能。Spring Security 的会话管理与 Spring Session 等项目结合紧密,适用于分布式环境下的会话管理。Shiro 的会话管理在独立应用或简单集群环境中也能很好地满足需求。

(二)性能与资源占用

  • 在轻量级应用场景下,Shiro 的资源占用相对较低,启动速度可能更快。它的架构设计使得在小型项目中能够快速响应认证和授权请求。
  • Spring Security 由于其功能的丰富性和与 Spring 生态的深度集成,在大型项目中,经过合理配置后,也能提供高效的安全服务,但在资源占用上可能会稍高一些,尤其是在启动时需要加载更多的配置和模块。

(三)开发复杂度与社区支持

  • Shiro 的配置相对简单直接,对于熟悉简单认证授权逻辑的开发者来说,学习成本较低,能够快速上手。其文档和社区资源虽然不如 Spring Security 丰富,但对于基本功能的实现已经足够。
  • Spring Security 的配置较为复杂,尤其是在处理复杂的安全需求时,需要对其众多的配置类和概念有深入的理解。然而,它拥有庞大的社区支持和丰富的文档,遇到问题时更容易找到解决方案,并且在与 Spring 其他组件协同工作时,能够提供更统一的开发体验。

综上所述,在选择 Spring Boot 安全框架时,需要根据项目的规模、安全需求的复杂程度、是否需要与第三方认证集成以及对性能和开发复杂度的权衡来决定使用 Shiro 还是 Spring Security。

;