码神之路项目之JWT登录+redis存储token及用户信息获取

1,292 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情

JWT登录+redis存储token及用户信息获取

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

#JWT

JWT就是一个加密的字符串,作为验证信息在计算机之间传递,只有可以访问持有对应正确的加密密钥的计算机才能对其进行解密,从而验证携带这个令牌(Token)的请求是否合法。

登录使用JWT技术:jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。

#JWT的组成:

  • head头部,唯一在头部里面要包含的是 alg 这个属性,如果是加密的 JWT,这个属性的值就是使用的签名或者解密用的算法。如果是未加密的 JWT,这个属性的值要设置成 none。{"type":"JWT","alg":"HS256"} 固定,这部分是可以被解密的。
  • payload(数据),存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息
  • signature(签证),由head、payload加上密钥加密而成,密钥不丢失的情况下,可以认为是安全的。

JWT验证主要验证signature部分是否合法。

#Springboot的JWT工具类

Step1: 导入依赖包

  <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>

Step2: 工具类生成token与测试

public class JWTUtils {

    private static final String jwtToken = "123456Mszlu!@#$$";

    public static String createToken(Long userId){
        Map<String,Object> claims = new HashMap<>();
        claims.put("userId",userId);
        JwtBuilder jwtBuilder = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法(head部分),秘钥为jwtToken
                .setClaims(claims) // body数据,要唯一,自行设置。payload部分数据
                .setIssuedAt(new Date()) // 设置签发时间:保证每次生成的token不同
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
        String token = jwtBuilder.compact();
        return token;
    }
//token解析
    public static Map<String, Object> checkToken(String token){
        try {
            Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
            return (Map<String, Object>) parse.getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

#登录

#登录流程与逻辑分析

客户使用账号密码发起登录请求,服务端收到请求后会做验证。项目中,没有直接对user表的pojo进行操作,而是将账号、密码、昵称三个字段封装成了一个登录参数类。

当收到客户端请求,后端会先去验证参数是否合法(账号密码是否有缺失),然后对密码进行MD5盐加密,如password = DigestUtils.md5Hex(password + slat);(到这里能确定登录参数合法)然后根据用户名和密码到user表中做查询,如果存在就去生成token,返回给前端。

值得一提的是,项目中,查询user表设置了只查询需要的字段并只查询到一条数据就不再查了,提升了性能。另外,还将token放入了redis中(token:user信息),并设置了过期时间。**登录认证的时候先认证token是否合法,去redis认证是否存在。

@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String,String> redisTemplate;

private static final String slat = "mszlu!@#";

@Override
public Result login(LoginParam loginParam) {
    /**
     * 1. 检查参数是否合法
     * 2. 根据用户名和密码去user表中查询 是否存在
     * 3. 如果不存在 登录失败
     * 4. 如果存在 ,使用jwt 生成token 返回给前端
     * 5. token放入redis当中,redis  token:user信息 设置过期时间
     *  (登录认证的时候 先认证token字符串是否合法,去redis认证是否存在)
     */
    String account = loginParam.getAccount();
    String password = loginParam.getPassword();
    if (StringUtils.isBlank(account) || StringUtils.isBlank(password)){
        return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
    }
    password = DigestUtils.md5Hex(password + slat);
    SysUser sysUser = sysUserService.findUser(account,password);
    if (sysUser == null){
        return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
    }
    String token = JWTUtils.createToken(sysUser.getId());

    redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
    return Result.success(token);
}
@Override
public SysUser findUser(String account, String password) {
    LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(SysUser::getAccount,account);
    queryWrapper.eq(SysUser::getPassword,password);
    queryWrapper.select(SysUser::getAccount,SysUser::getId,SysUser::getAvatar,SysUser::getNickname);
    queryWrapper.last("limit 1");

    return sysUserMapper.selectOne(queryWrapper);
}

#获取用户信息

前端:Authorization将String类型的token信息存在头部。

@GetMapping("currentUser")
public Result currentUser(@RequestHeader("Authorization") String token){
    return sysUserService.findUserByToken(token);
}

#获取逻辑

Token解析

@Override
public SysUser checkToken(String token) {
    if (StringUtils.isBlank(token)){
        return null;
    }
    Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
    if (stringObjectMap == null){
        return null;
    }
    String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
    if (StringUtils.isBlank(userJson)){
        return null;
    }
    SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
    return sysUser;
}

后端会先将获取到的token进行解析,看解析结果是否存在。如果成功我们会获取到账号、ID、昵称等信息。在项目中,我们将这部分用户信息封装在了loginUserVo实体中。

@Override
public Result findUserByToken(String token) {
    /**
     * 1. token合法性校验
     *    是否为空,解析是否成功 redis是否存在
     * 2. 如果校验失败 返回错误
     * 3. 如果成功,返回对应的结果 LoginUserVo
     */
    SysUser sysUser = loginService.checkToken(token);
    if (sysUser == null){
        return Result.fail(ErrorCode.TOKEN_ERROR.getCode(),ErrorCode.TOKEN_ERROR.getMsg());
    }
    LoginUserVo loginUserVo = new LoginUserVo();
    loginUserVo.setId(String.valueOf(sysUser.getId()));
    loginUserVo.setNickname(sysUser.getNickname());
    loginUserVo.setAvatar(sysUser.getAvatar());
    loginUserVo.setAccount(sysUser.getAccount());
    return Result.success(loginUserVo);
}