Spring Boot结合Redis和Token来实现同一个账户同时只能有一处在线的方案

Spring Boot结合Redis和Token来实现同一个账户同时只能有一处在线的方案

以下具体步骤:

步骤1:添加依赖

首先,在你的pom.xml中添加必要的依赖。


<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Boot Starter Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- Spring Boot Starter Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <!-- Redisson -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.13.6</version>
    </dependency>
</dependencies>

步骤 2: 配置Redis

application.properties中配置Redis连接:


# application.properties 
spring.redis.host=localhost  
spring.redis.port=6379  
# 根据需要添加其他配置,如密码、数据库索引等

步骤 3: 创建JWT工具类

这个类将负责生成和验证JWT。

import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
import java.util.Date;  
import java.util.HashMap;  
import java.util.Map;  

public class JwtTokenUtil {  

    private String secretKey;  

    // 构造函数,初始化密钥  
    public JwtTokenUtil(String secretKey) {  
        this.secretKey = secretKey;  
    }  

    // 生成JWT的方法  
    public String generateToken(Map<String, Object> claims) {  
        return Jwts.builder()  
                .setClaims(claims) // 设置claims  
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // 设置1小时后过期  
                .signWith(SignatureAlgorithm.HS512, secretKey) // 使用HS512算法和密钥签名  
                .compact(); // 生成token  
    }  

    // 验证JWT的方法  
    public boolean validateToken(String token, String username) {  
        try {  
            final Claims claims = extractClaims(token);  
            return claims != null && claims.getSubject().equals(username) && !isTokenExpired(claims.getExpiration());  
        } catch (Exception e) {  
            // 如果token无效,会抛出异常  
            return false;  
        }  
    }  

    // 提取JWT中的Claims的方法  
    public Claims extractClaims(String token) {  
        return Jwts.parser()  
                .setSigningKey(secretKey)  
                .parseClaimsJws(token)  
                .getBody();  
    }  

    // 辅助方法:检查token是否过期  
    private boolean isTokenExpired(Date expiration) {  
        return expiration.before(new Date());  
    }  

}

步骤 4: 配置Spring Security

你需要配置Spring Security以使用JWT进行身份验证,并添加自定义的过滤器来处理JWT。

@Configuration  
@EnableWebSecurity  
public class SecurityConfig extends WebSecurityConfigurerAdapter {  

    @Autowired  
    private JwtTokenUtil jwtTokenUtil;  

    // 其他Bean和方法...  

    @Override  
    protected void configure(HttpSecurity http) throws Exception {  
        http  
            .csrf().disable()  
            .authorizeRequests()  
            .antMatchers("/login", "/register").permitAll()  
            .anyRequest().authenticated()  
            .and()  
            .addFilterBefore(new JwtAuthenticationTokenFilter(jwtTokenUtil), UsernamePasswordAuthenticationFilter.class);  

        // 其他配置...  
    }  
}

步骤 5: 创建JWT认证过滤器

这个过滤器将检查每个请求中的JWT,并设置Spring Security的SecurityContextHolder


import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.core.Authentication;  
import org.springframework.security.core.context.SecurityContext;  
import org.springframework.security.core.context.SecurityContextHolder;  
import org.springframework.web.filter.OncePerRequestFilter;  

import javax.servlet.FilterChain;  
import javax.servlet.ServletException;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  

public class JwtAuthenticationFilter extends OncePerRequestFilter {  

    private final JwtTokenUtil jwtTokenUtil;  

    public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil) {  
        this.jwtTokenUtil = jwtTokenUtil;  
    }  

    @Override  
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)  
            throws ServletException, IOException {  

        // 从请求头中提取JWT  
        String jwt = extractJwtFromRequestHeader(request);  

        // 验证JWT  
        if (jwt != null && jwtTokenUtil.validateToken(jwt)) {  
            // 如果JWT有效,则提取用户名  
            String username = jwtTokenUtil.extractUsernameFromToken(jwt);  

            // 创建一个UsernamePasswordAuthenticationToken(虽然这里没有密码,但我们可以将其视为预认证的用户名)  
            // 注意:在实际应用中,你可能需要自定义Authentication类来存储更多信息  
            Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, null);  

            // 设置SecurityContextHolder  
            SecurityContextHolder.getContext().setAuthentication(authentication);  
        }  

        // 继续过滤器链  
        chain.doFilter(request, response);  
    }  

    // 辅助方法:从请求头中提取JWT  
    private String extractJwtFromRequestHeader(HttpServletRequest request) {  
        String header = request.getHeader("Authorization");  
        if (header == null || !header.startsWith("Bearer ")) {  
            return null;  
        }  
        return header.substring(7);  
    }  
 
}

步骤 6: 创建会话管理器

这个管理器将使用Redis来跟踪用户的会话。

@Component  
public class SessionManager {  

    @Autowired  
    private RedisTemplate<String, String> redisTemplate;  

    public void login(String username, String tokenId) {  
        // 将username和tokenId存储到Redis中  
        // ...  
    }  

    public boolean checkSessionExists(String username, String tokenId) {  
        // 检查Redis中是否已存在该用户的session  
        // ...  
    }  
}

步骤 7: 整合JWT认证和会话管理

JwtAuthenticationTokenFilter中,除了验证JWT之外,还需要调用SessionManager来检查会话是否已存在。

// 在doFilterInternal方法中  
// ...  
String jwt = extractToken(request);  
if (jwt != null && jwtTokenUtil.validateToken(jwt)) {  
    Claims claims = jwtTokenUtil.extractClaims(jwt);  
    String username = claims.getSubject();  
    if (!sessionManager.checkSessionExists(username, jwt)) {  
        // 处理会话冲突  
        // ...  
    }

步骤 8: 处理会话冲突

JwtAuthenticationTokenFilterdoFilterInternal方法中,当检测到会话冲突时(即同一个用户名已有一个活动的会话),你可以决定是踢出旧会话还是拒绝新会话。以下是一个简单的示例,展示了如何拒绝新会话并返回一个错误响应给客户端。


@Override  
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)  
        throws ServletException, IOException {  
    String jwt = extractTokenFromRequest(request);  
    if (jwt != null && jwtTokenUtil.validateToken(jwt)) {  
        Claims claims = jwtTokenUtil.extractClaims(jwt);  
        String username = claims.getSubject();  

        // 检查会话是否已存在  
        if (!sessionManager.checkSessionExists(username, jwt)) {  
            // 标记当前会话为有效  
            sessionManager.login(username, jwt);  

            // 设置认证信息到SecurityContextHolder  
            // ...  

            chain.doFilter(request, response);  
        } else {  
            // 会话冲突处理  
            response.setStatus(HttpServletResponse.SC_CONFLICT);  
            response.getWriter().write("Session conflict: User is already logged in from another session.");  
            return;  
        }  
    }  

    // 如果没有JWT或JWT无效,可能需要进行其他处理(如重定向到登录页面)  
    // ...  

    chain.doFilter(request, response);  
}  

// 辅助方法,用于从请求中提取JWT  
private String extractTokenFromRequest(HttpServletRequest request) {  
    // 从请求头、Cookie或URL参数中提取JWT  
    // 示例:从HTTP头Authorization字段中提取  
    String bearerToken = request.getHeader("Authorization");  
    if (bearerToken != null && bearerToken.startsWith("Bearer ")) {  
        return bearerToken.substring(7);  
    }  
    return null;  
}

步骤 9: 登出处理

当用户登出时,你需要从Redis中删除该用户的会话记录。这可以通过在登出控制器中调用SessionManager的相应方法来实现。

@RestController  
@RequestMapping("/api/auth")  
public class AuthController {  

    @Autowired  
    private SessionManager sessionManager;  

    // 其他方法...  

    @PostMapping("/logout")  
    public ResponseEntity<?> logout(@RequestHeader(value = "Authorization", required = false) String jwt) {  
        if (jwt != null && jwt.startsWith("Bearer ")) {  
            jwt = jwt.substring(7);  
            Claims claims = jwtTokenUtil.extractClaims(jwt);  
            String username = claims.getSubject();  

            // 假设你有一个方法来获取与JWT关联的特定标识符(如tokenId)  
            // String tokenId = ...; // 从JWT的Claims中获取  

            // 从Redis中删除会话  
            sessionManager.logout(username, /* tokenId 如果需要的话 */);  

            // 返回成功响应  
            return ResponseEntity.ok("Logged out successfully");  
        }  

        // 如果没有提供JWT或其他错误情况  
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid JWT for logout");  
    }  

    // 注意:上面的logout方法假设你可以从JWT中提取出足够的信息来唯一标识会话。
    // 在实际应用中,你可能需要更复杂的逻辑来确保正确地跟踪和识别会话。
}

总结下设计思路

1. JWT生成与验证
  • 生成JWT:在用户登录时,服务器验证用户名和密码后,生成一个包含用户信息的JWT,并将其发送给客户端。JWT中通常包含用户ID、过期时间等信息。

  • 验证JWT:客户端在每次请求时,将JWT放在请求头中发送给服务器。服务器使用JWT密钥验证JWT的有效性和完整性。

2. Redis会话管理
  • 存储Token标识:在服务器验证JWT后,可以提取JWT中的用户ID(或其他唯一标识符),并将这个标识符与当前JWT的Token(或其哈希值)作为键值对存储在Redis中。

  • 这个键可以设置为用户的唯一会话标识符,如user:session:userId

  • 检查会话唯一性:在每次请求验证JWT后,检查Redis中是否已存在该用户ID的其他Token。如果存在,则说明该用户已在其他地方登录,此时可以选择:

    • 强制登出旧会话(删除Redis中的旧Token)。

    • 拒绝新会话的请求,提示用户已在其他地方登录。

  • 更新Redis:如果当前会话是有效的,更新Redis中的Token为最新的,以确保会话的时效性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值