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;
}
}
二、拦截方式
方式一:通过拦截器的方式拦截无效请求
- 增加配置类
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*");
}
}
- 增加拦截器类
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切面方式拦截无效请求
- 引入AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 新增权限控制注解
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;
}
- 增加切面类
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();
}
}
- 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 "查询数据";
}