JWT登录的简单案例

jwt登录的简单实现案例

token构成

token=base64(header).base4(payload).hmacsha256(base64(header) + "." + base4(payload), secret)

一、JWT登录校验后台实现

package com.strap.mydemo.service.impl;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.digest.BCrypt;
import cn.hutool.http.Header;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.strap.mydemo.constants.MyDemoConstants;
import com.strap.mydemo.entity.UserDetail;
import com.strap.mydemo.enums.BaseResultType;
import com.strap.mydemo.exceptions.BaseResultException;
import com.strap.mydemo.mapper.UserDetailMapper;
import com.strap.mydemo.service.IUserDetailService;
import lombok.extern.log4j.Log4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.HashMap;

/**
 * <p>
 * jwt登录验证服务实现类
 * </p>
 *
 * @author strap
 */
@Service
@Log4j
public class UserDetailServiceImpl extends ServiceImpl<UserDetailMapper, UserDetail> implements IUserDetailService {

    /**
     * jwt登录失效时间
     */
    private static final Long LOGIN_EXPIRE_TIME = DateUnit.HOUR.getMillis();

    /**
     * 私钥失效时间
     */
    private static final Long PRI_KEY_EXPIRE_TIME = 30 * DateUnit.MINUTE.getMillis();

    /**
     * 私钥缓存前缀
     */
    private static final String PRI_KEY_PREFIX_NAME = "$LOGIN_PRI_KEY@";

    private static final String TOKEN_HEADER_PREFIX = "Bearer ";

    private RedisTemplate<String, Object> redisTemplate;

    private UserDetailMapper userDetailMapper;

    @Resource
    public void setUserDetailMapper(UserDetailMapper userDetailMapper) {
        this.userDetailMapper = userDetailMapper;
    }

    @Resource
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean login(UserDetail userDetail, String pwd) throws Exception {
        // 获取私钥解密密码
        String privateKeyBase64 = Convert.toStr(redisTemplate.opsForValue().get(PRI_KEY_PREFIX_NAME + userDetail.getUsername()));
        BaseResultType.FAILED.isEmpty(privateKeyBase64, () -> "public key is invalid");
        byte[] decrypt;
        try {
            RSA rsa = new RSA(privateKeyBase64, null);
            // 获取原始密码
            decrypt = rsa.decrypt(Base64.decode(pwd), KeyType.PrivateKey);
        } catch (Exception e) {
            log.error("rsa.decrypt failed, \npwd:" + pwd + "\nprivateKeyBase64:" + privateKeyBase64, e);
            throw new BaseResultException(BaseResultType.FAILED, () -> "the entered password is incorrect");
        }
        // 将原始密码使用用户盐+BCrypt加密后与数据库比对
        String encryptPwd = encryptPwd(StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8), userDetail.getSalt());
        // 不要简单使用string的equals对密码进行比较,可能产生计时攻击
        return MessageDigest.isEqual(encryptPwd.getBytes(CharsetUtil.CHARSET_UTF_8), userDetail.getPassword().getBytes(CharsetUtil.CHARSET_UTF_8));
    }

    @Override
    public UserDetail queryUserByUserName(String userName) throws Exception {
        // 获取用户信息
        UserDetail user = userDetailMapper.selectOne(
                new LambdaQueryWrapper<UserDetail>().eq(
                        UserDetail::getUsername, userName
                )
        );
        BaseResultType.FAILED.isNull(user, () -> "user not exists");
        return user;
    }

    @Override
    public String encryptPwd(String sourcePwd, String salt) throws Exception {
        return BCrypt.hashpw(sourcePwd, BCrypt.gensalt(10, new SecureRandom(salt.getBytes())));
    }

    @Override
    public String getPublicKeyBase64(String userName) throws Exception {
        RSA rsa = new RSA();
        redisTemplate.opsForValue().set(PRI_KEY_PREFIX_NAME + userName, rsa.getPrivateKeyBase64(), Duration.ofMillis(PRI_KEY_EXPIRE_TIME));
        return rsa.getPublicKeyBase64();
    }

    @Override
    public String generateJwtToken(UserDetail userDetail) throws Exception {
        return JWTUtil.createToken(
                MapUtil.builder(new HashMap<String, Object>(3))
                        .put("userName", userDetail.getUsername())
                        .put(MyDemoConstants.UserConstants.JWT_EXPIRE_KEY, System.currentTimeMillis() + LOGIN_EXPIRE_TIME)
                        .build(),
                JWTSignerUtil.hs256(getLoginSecretStr())
        );
    }

    @Override
    public byte[] getLoginSecretStr() throws Exception {
        // TODO 服务密钥
        return "ajk1234562laskasfdj$lajsldas%asdaf".getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public boolean verify(HttpServletRequest request) throws Exception {
        String token = StrUtil.removeAll(request.getHeader(Header.AUTHORIZATION.getValue()), TOKEN_HEADER_PREFIX);
        BaseResultType.ILLEGAL_ARG.isEmpty(token, () -> "token cannot be empty");
        JWT jwt = JWT.of(token);
        BaseResultType.TOKEN_EXPIRE.isError(Convert.toLong(jwt.getPayload(MyDemoConstants.UserConstants.JWT_EXPIRE_KEY), 0L) < System.currentTimeMillis(),
                () -> "token has expired");
        BaseResultType.FAILED.isError(!jwt.verify(JWTSignerUtil.hs256(getLoginSecretStr())), () -> "invalid token");
        return true;
    }
}

二、拦截方式

方式一:通过拦截器的方式拦截无效请求

  1. 增加配置类
package com.strap.mydemo.config;

import com.strap.mydemo.interceptors.LoginInterceptor;
import com.strap.mydemo.service.IUserDetailService;
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;

/**
 * <p></p>
 *
 * @author strap
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private IUserDetailService userDetailService;

    @Autowired
    public void setUserDetailService(IUserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(userDetailService))
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/login/publicKey/get", "/test*");
    }
}

  1. 增加拦截器类
package com.strap.mydemo.interceptors;

import com.strap.mydemo.service.IUserDetailService;
import lombok.extern.log4j.Log4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * <p></p>
 *
 * @author strap
 */
@Log4j
public class LoginInterceptor implements HandlerInterceptor {

    private final IUserDetailService userDetailService;

    public LoginInterceptor(IUserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }

    @Override
    public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, Object handler) throws Exception {
        return userDetailService.verify(request);
    }


}


方式二:通过AOP切面方式拦截无效请求

  1. 引入AOP依赖
      <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
  1. 新增权限控制注解
package com.strap.mydemo.annotations;

import java.lang.annotation.*;

/**
 * <p>自定义权限注解</p>
 *
 * @author strap
 */
@Target(value = ElementType.METHOD)
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
public @interface MyDemoAuth {

    /**
     * 是否校验token
     */
    boolean checkToken() default true;

}

  1. 增加切面类
package com.strap.mydemo.components;

import com.strap.mydemo.annotations.MyDemoAuth;
import com.strap.mydemo.service.IUserDetailService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>切面鉴权</p>
 *
 * @author strap
 */
@Component
@Aspect
public class AuthAspect {

    private final HttpServletRequest request;

    private final IUserDetailService userDetailService;

    public AuthAspect(HttpServletRequest request, IUserDetailService userDetailService) {
        this.request = request;
        this.userDetailService = userDetailService;
    }

    @Pointcut("@annotation(com.strap.mydemo.annotations.MyDemoAuth)")
    private void authPointcut() {
    }

    @Around("authPointcut()")
    public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取目标对象对应的字节码对象
        //Class<?> targetCls=joinPoint.getTarget().getClass();
        //获取方法签名信息从而获取方法名和参数类型
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        //获取目标方法对象上注解中的属性值
        MyDemoAuth auth = ms.getMethod().getAnnotation(MyDemoAuth.class);
        // 校验签名
        if (auth.checkToken()) {
            userDetailService.verify(request);
        }
        return joinPoint.proceed();
    }
}

  1. demo
    @MyDemoAuth(checkToken = false)
    @GetMapping(path = "/showPage")
    public String showPage() throws Exception {
        return "test1.html";
    }

    @MyDemoAuth()
    @GetMapping(path = "/queryData")
    public String quueryData() throws Exception {
        return "查询数据";
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值