jwt+redis实现登录认证

文章介绍了在SpringBoot项目中如何使用JWT进行用户身份验证,同时利用Redis作为缓存存储JWT以实现会话管理。文章详细描述了JWT工具类、登录验证、MD5加密以及自定义登录拦截器的实现过程。
摘要由CSDN通过智能技术生成

项目环境:spring boot项目

pom.xml引入jwt和redis

        <!-- jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.3.0</version>
        </dependency>


        <!-- redis坐标-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

application.yml配置文件中,对redis进行配置

  data:
    redis:
      port: 6379
      host: localhost

JwtUtil工具类

package com.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtil {

    /*生成jwt字符串时用到的秘钥*/
    private static final String KEY = "itheima";

	//接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 )) //失效时间 单位毫秒 当前值为1天
                .sign(Algorithm.HMAC256(KEY));
    }

	//接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }

}
Md5Util工具类(因为登录时有对密码进行md5加密的逻辑)
package com.utils;


import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Md5Util {
    /**
     * 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
     */
    protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    protected static MessageDigest messagedigest = null;

    static {
        try {
            messagedigest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException nsaex) {
            System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
            nsaex.printStackTrace();
        }
    }

    /**
     * 生成字符串的md5校验值
     *
     * @param s
     * @return
     */
    public static String getMD5String(String s) {
        return getMD5String(s.getBytes());
    }

    /**
     * 判断字符串的md5校验码是否与一个已知的md5码相匹配
     *
     * @param password  要校验的字符串
     * @param md5PwdStr 已知的md5校验码
     * @return
     */
    public static boolean checkPassword(String password, String md5PwdStr) {
        String s = getMD5String(password);
        return s.equals(md5PwdStr);
    }


    public static String getMD5String(byte[] bytes) {
        messagedigest.update(bytes);
        return bufferToHex(messagedigest.digest());
    }

    private static String bufferToHex(byte bytes[]) {
        return bufferToHex(bytes, 0, bytes.length);
    }

    private static String bufferToHex(byte bytes[], int m, int n) {
        StringBuffer stringbuffer = new StringBuffer(2 * n);
        int k = m + n;
        for (int l = m; l < k; l++) {
            appendHexPair(bytes[l], stringbuffer);
        }
        return stringbuffer.toString();
    }

    private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
        char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
        // 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
        char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
        stringbuffer.append(c0);
        stringbuffer.append(c1);
    }

}

登录的控制器

package com.controller;

import com.pojo.Result;
import com.pojo.User;
import com.service.UserService;
import com.utils.JwtUtil;
import com.utils.Md5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user")
@Validated
@CrossOrigin
public class UserController1 {
    @Autowired
    private UserService userService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @PostMapping("/login")
    public Result login(String username, String password) {
        //查询用户
        String md5Password = Md5Util.getMD5String(password);
        User user = userService.login(username, md5Password);
        if (user == null) {//用户名或密码错误
            return Result.error("用户名或密码错误");
        } else {
            //登录成功
            Map<String, Object> claims = new HashMap<>();
            claims.put("id", user.getId());
            claims.put("username", user.getUsername());
            String jwtToken = JwtUtil.genToken(claims);

            //往Redis中存储一个键值对
            ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
            operations.set(jwtToken, jwtToken, 1, TimeUnit.DAYS);

            return Result.success(jwtToken);
        }
    }

}

在前端发起登录请求,用户名和密码验证通过后,生成jwt,把jwt字符长的值,存放在redis缓存中,然后把jwt字符串返回到前端。

前端拿到jwt字符串后存在一个变量中,在之后的每次请求中,都在请求头中携带上jwt,后端再根据jwt的内容进行验证,判断是否是已登录的正常请求,如果是已经登录后的请求,就放行,否则就返回401(未认证)。

后端是通过声明一个拦截器,对请求进行判断的,代码如下:

LoginInterceptor.java
package com.interceptor;
import com.utils.JwtUtil;
import com.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.List;
import java.util.Map;

/**
 * @projectName: big-event
 * @package: com.interceptor
 * @className: LoginInterceptor
 * @author: liangmeng
 * @description: 登录拦截器
 * @date: 2024/1/14 17:20
 * @version: 1.0
 */

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //Controller方法处理之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取到请求头的jwt字符串
        String token = request.getHeader("Authorization");
        //验证token
        try {
            //获取redis的值
            ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
            String redisToken = operations.get(token);
            if (redisToken == null) {
                //token失效 拦截请求
                throw new RuntimeException();
            } else {
                Map<String, Object> claims = JwtUtil.parseToken(token);

                //把业务数据存储到ThreadLocal中
                ThreadLocalUtil.set(claims);

                //放行请求
                return true;
            }
        } catch (Exception e) {
            //相应码401
            response.setStatus(401);
            //拦截请求
            return false;
        }
    }

    //该方法将在整个请求完成之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清空ThreadLocal中的信息 防止内存泄漏
        ThreadLocalUtil.remove();
    }

}

上面代码是自定义的登录拦截器,在请求到来后,控制器方法执行前,会进入到拦截器的preHandle方法中,在这个方法里面,或获取到请求头中的Authorization属性值,也就是jwt的值,然后再获取到redis中的值,redis中key和values是相同的,都是jwt字符串的内容,所以这里是用jwt的值作为key去获取value,如果value有值则说明该请求是可以放行的(已经登录过),否则拦截请求,返回响应码401。

afterCompletion方法是在整个请求完成之后执行,示例中是把ThreadLocal的值清空,这里根据需要,如果使用到了ThreadLocal,就把它清空,防止内存泄漏,如果没有使用到,则不需要处理。

拦截器定义好了之后,还需要把自定义的拦截器注册到全局拦截器中,使其生效。

WebConfig.java
package com.config;

import com.interceptor.CorsInterceptor;
import com.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;



@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Autowired
    private CorsInterceptor corsInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        //把自定义的拦截器注册到全局拦截器中 排除登录和注册请求
        registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
    }
}

同步把登录和注册接口的请求放行,不需要拦截。

修改密码的接口处理逻辑,当用户修改密码后,需要主动将jwt做失效操作,下面是控制器的代码:

package com.controller;

import com.pojo.Result;
import com.pojo.User;
import com.service.UserService;
import com.utils.Md5Util;
import com.utils.ThreadLocalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/user")
@Validated
@CrossOrigin
public class UserController {
    @Autowired
    private UserService userService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    //更新用户密码
    @PatchMapping("/updatePwd")
    public Result updatePwd(@RequestBody Map<String, String> params, @RequestHeader("Authorization") String jwtToken) {
        String oldPwd = params.get("oldPwd");
        String newPwd = params.get("newPwd");
        String reNewPwd = params.get("reNewPwd");
        if (!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(reNewPwd)) {
            return Result.error("缺少必要的参数");
        }

        //判断原始密码是否正确
        //获取用户名
        Map<String, Object> claims = ThreadLocalUtil.get();
        String username = (String) claims.get("username");
        //根据用户名查询用户
        User user = userService.findByUserName(username);
        String currPwd = user.getPassword();

        String oldPwdMd5 = Md5Util.getMD5String(oldPwd);
        if (!oldPwdMd5.equals(currPwd)) {
            return Result.error("原始密码不正确");
        }

        //判断输入的两次密码是否一致
        if (!newPwd.equals(reNewPwd)) {
            return Result.error("两次密码不一致");
        }

        //更新密码
        //获取用户名
        String userId = (String) claims.get("id");
        String newPwdMd5 = Md5Util.getMD5String(newPwd);

        userService.updatePwd(userId, newPwdMd5);

        //密码更新完毕,删除redis中的缓存jwtToken值,让用户重新登陆
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        operations.getOperations().delete(jwtToken);

        return Result.success();
    }


}

在密码修改完毕后,根据请求头中的Authorization属性值来删除redis中存储的数据。这样原来的jwt就失效了,无法使用原来的jwt进行登录认证。

总结:因为http请求是无状态请求,使用jwt可以解决这一问题,登录成功后,在一段时间内就可以直接向后端发送请求,这样不需要重复进行登录操作。

因为jwt数据在后端没有进行存储,所以会出现一个问题,无法使其失效。这时候就引入了redis缓存,在redis缓存中将jwt的数据存下来,当用户修改密码、或退出登录后,将缓存的数据删除,然后在请求处理时(拦截器中)判断当前jwt是否有效,有效则正常处理请求,否则拦截请求,从而完成请求的权限控制。

一般项目还会使用sessionID作为请求控制的方式,其逻辑如下图所示:

<think>好的,我现在需要帮助用户了解如何使用Shiro、JWTRedis实现SSO单点登录。首先,我需要回顾一下用户提供的引用内容,看看有哪些现有的资料可以利用。引用1到4提到了基于Spring和Shiro的实现,结合了JWTRedis,还有一些具体的配置和依赖信息。用户的需求是得到示例教程,所以需要结构清晰,分步骤讲解。 首先,我应该明确SSO的基本概念,确保用户理解单点登录的核心机制。根据引用2,SSO允许用户一次登录即可访问多个信任系统。接下来,需要考虑各个组件的作用:Shiro负责权限控制,JWT用于生成和验证令牌Redis用来存储令牌信息,实现会话共享。 接下来,我需要规划教程的结构。通常,教程会从环境搭建开始,包括依赖导入和配置。用户提供的引用4中有相关的依赖项,比如shiro-spring和java-jwt,需要确保这些依赖正确添加到项目中。然后,可能需要配置Shiro的核心组件,如自定义Realm和过滤器,这部分在引用3中提到过。 然后,需要考虑JWT的生成和验证流程。用户可能需要生成Token,并在各个子系统间传递。同时,Redis的作用是存储有效Token,以便各子系统可以验证Token的有效性。需要设计一个JwtFilter,作为Shiro的过滤器,拦截请求并验证Token,这部分在引用3的JwtFilter中有所涉及。 另外,单点登录的关键是共享会话状态,Redis在这里作为中央存储,确保各个服务都能访问到同一个会话信息。需要详细说明如何将Token存入Redis,并在验证时进行查询。同时,需要考虑Token的刷新和过期机制,以增强安全性。 还要注意跨域问题,因为子系统可能部署在不同域名下,需要处理CORS配置。在示例代码中,可能需要添加跨域支持,确保前后端能够正常通信。 在编写代码示例时,需要确保语法正确,比如Shiro的配置类、自定义Realm的实现,以及JWT工具类的编写。同时,要说明如何在不同服务中集成这些配置,确保SSO的流程顺畅。 最后,需要生成相关问题,帮助用户进一步理解和深入学习。可能的问题包括如何优化Token的安全性、如何处理Token失效的情况,或者如何扩展支持更多的子系统。 整个过程中,需要确保步骤清晰,代码示例准确,并且引用提供的参考资料来支持各个部分的实现,比如引用3中的模块化开发和引用4中的依赖配置。同时,要使用用户指定的格式,行内数学表达式用$...$,独立公式用$$...$$,但在这个主题中可能数学公式较少,更多是代码和配置的展示。 需要验证每个步骤是否正确,比如Shiro的过滤器链配置是否正确,JWT的签名和验证是否有效,Redis的存储和查询是否正常。可能还需要考虑异常处理,比如Token过期或无效时的响应。 总结来说,教程应该分为以下几个部分:SSO原理概述、环境准备与依赖、Shiro核心配置、JWT工具类实现Redis集成、单点登录流程实现,以及跨域处理。每个部分都需要详细的说明和代码示例,确保用户能够跟随操作并实现功能。</think>### 如何使用 Shiro、JWTRedis 实现 SSO 单点登录 #### 一、SSO 单点登录原理 SSO(Single Sign On)允许用户通过一次登录访问多个相互信任的子系统。核心流程如下: 1. **认证中心**:用户首次登录时,由认证中心验证身份并生成 Token,存储到 Redis[^2]。 2. **Token 传递**:子系统通过 Token认证中心验证用户权限[^3]。 3. **会话共享**:Redis 集中管理 Token 状态,确保各子系统共享登录状态[^1]。 --- #### 二、环境准备 ##### 1. 依赖配置(Spring Boot) ```xml <!-- Shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.8.0</version> </dependency> <!-- JWT --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.2</version> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 引用自 Shiro 和 JWT 的典型依赖配置[^4]。 --- #### 三、核心实现步骤 ##### 1. 生成和验证 JWT Token ```java public class JwtUtil { // 生成 Token(用户ID + 密钥 + 过期时间) public static String generateToken(String userId, String secret, long expire) { return JWT.create() .withClaim("userId", userId) .withExpiresAt(new Date(System.currentTimeMillis() + expire)) .sign(Algorithm.HMAC256(secret)); } // 验证 Token 有效性 public static boolean verifyToken(String token, String secret) { try { JWT.require(Algorithm.HMAC256(secret)).build().verify(token); return true; } catch (Exception e) { return false; } } } ``` ##### 2. 自定义 Shiro Realm 和 Filter ```java public class JwtRealm extends AuthorizingRealm { @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; // 仅处理 JWT Token } // 授权逻辑 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 添加角色和权限(从数据库或缓存查询) return info; } // 认证逻辑 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) { String token = (String) authToken.getCredentials(); String userId = JwtUtil.parseUserId(token); // 解析用户ID if (!RedisUtil.exists(userId)) { // 检查 Redis 中是否存在有效 Token throw new AuthenticationException("Token 已过期"); } return new SimpleAuthenticationInfo(userId, token, getName()); } } // 自定义 JWT 过滤器 public class JwtFilter extends AccessControlFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest httpRequest = (HttpServletRequest) request; String token = httpRequest.getHeader("Authorization"); if (token == null) return false; return executeLogin(request, response); // 触发 Shiro 的登录验证 } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) { sendChallenge(response, 401); // 返回 401 未授权 return false; } } ``` ##### 3. Shiro 配置类 ```java @Configuration public class ShiroConfig { @Bean public SecurityManager securityManager(JwtRealm jwtRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(jwtRealm); return manager; } @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); Map<String, Filter> filters = new HashMap<>(); filters.put("jwt", new JwtFilter()); factory.setFilters(filters); // 设置拦截规则 Map<String, String> filterChainMap = new LinkedHashMap<>(); filterChainMap.put("/login", "anon"); // 登录接口放行 filterChainMap.put("/**", "jwt"); // 其他请求需验证 JWT factory.setFilterChainDefinitionMap(filterChainMap); return factory; } } ``` ##### 4. Redis 管理 Token ```java public class RedisUtil { @Autowired private RedisTemplate<String, String> redisTemplate; // 存储 Token(用户ID为键,Token为值) public void saveToken(String userId, String token, long expire) { redisTemplate.opsForValue().set(userId, token, expire, TimeUnit.SECONDS); } // 检查 Token 是否存在 public boolean exists(String userId) { return redisTemplate.hasKey(userId); } } ``` --- #### 四、单点登录流程 1. **用户登录** - 子系统 A 重定向到认证中心登录页。 - 认证成功生成 JWT Token,存储到 Redis,并返回给用户[^1]。 2. **访问其他子系统** - 用户携带 Token 访问子系统 B。 - 子系统 B 向认证中心验证 Token 有效性(通过 Redis 检查)[^3]。 3. **注销登录** - 从 Redis 中删除 Token,所有子系统同步失效。 --- #### 五、跨域处理(可选) 若子系统部署在不同域名下,需在 Spring Boot 中添加跨域配置: ```java @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "POST") .allowedHeaders("Authorization"); } } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值