JWT〖二〗Springboot与JWT单点登录实现
一. 单点登录
如图,下图为运用token来实现的登录系统,客户端第一次请求使用密码与账户来服务端换取token,拿到token后存在浏览器缓存中,然后每次请求新的资源携带上当前的token来验证是否为已登录的用户,JWT在上一篇博客中已经介绍很详细了,家人们不懂的话可以去看看JWT博客
有人问我为什么要单点登录,不会吧不会吧,真就敢数据罗奔啊,当你用户数据被篡改了,你学生数据库出现一个迪迦奥特曼你就开心了~
二. 废话不多说,上代码
首先通过Springboot的AOP切面来实现两个注解,分别代表接口需要Token验证,或者不要Token验证
AOP不会还有人不知道吧,面试官说你自己走还是我帮你
面向切面编程,举一个简单的例子,你服务是不是需要记录日志,或者就是验证登录,你不可能每个controller的接口都整个验证程序吧,每个controller都传一个token参数,这谁看到代码不恶心死啊~
2.1 AOP代码
PassToken注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
UserLoginToken注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
重头戏在AuthJWT 的handler类
package com.fehead.diseaseCare.aop;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.fehead.diseaseCare.controller.BaseController;
import com.fehead.diseaseCare.error.BusinessException;
import com.fehead.diseaseCare.error.EmBusinessError;
import com.fehead.diseaseCare.service.IUserService;
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Component
@Aspect
@Slf4j
public class AuthJWT {
//定义切面,所有的controller层都会监控 家人们切面表达式可得多学学了
@Pointcut("execution(* com.fehead.diseaseCare.controller..*.*(..))")
public void doHander() { }
@Around("doHander()")
public Object exception(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//进入controller层前
beforePoint(joinPoint);
//放行
Object result = joinPoint.proceed();
//返回数据前
afterPoint(joinPoint, result);
return result;
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
throw new BusinessException(EmBusinessError.COMMON_ERROR,e.getMessage());
}
}
private Boolean beforePoint(ProceedingJoinPoint joinPoint) throws Exception {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
// 从 http 请求头中取出 token
String token = request.getHeader("token");
//得到要进入的是哪个controller方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//检查是否有passtoken注释,有则跳过认证,所以在controller层加了@Passtoken注解,这里我就不校验
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//加了@UserLoginToken注解的我要进行校验
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (StringUtils.isEmpty(token)) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"token为空" );
}
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("xxxxx4234{}d]]fxxxxdsadasdasdsad")).build();
try {
//验证token
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new BusinessException(EmBusinessError.USER_AUTH_ERROR);
}
return true;
}
}
return true;
}
private void afterPoint(ProceedingJoinPoint joinPoint, Object result) throws Exception {
}
}
理一理逻辑(AOP注解版需要自己掌握)
- 首先@Component加入Sping容器 @Aspect变成一个切面类
- @Pointcut(“execution(* com.fehead.diseaseCare.controller….(…))”) 通过这个方法定义切入点来整合切面表达式,我们这里是设置了全部controller
- @Around(“doHander()”) 环绕通知,通过 joinPoint 执行 joinPoint.proceed(); 方法时控制在这个方法前面或者后面或者异常时做什么,这块就把你想要在其他方法执行前,可以先执行你自定义的方法
- 拿到方法上的自定义注解 method.isAnnotationPresent(UserLoginToken.class) 去验证token
- 从header获取token,然后 jwtVerifier.verify(token); 验证token
大致流程就是这样,简化版的小单点登录,但是这里就又有一个问题了,那我们之前不是说可以在token里设置一些不重要的信息嘛,例如userId这种,那我们在后端是不是可以不用再传一个userId了,这里有几种方法
- 1. 使用threadLocal存储从Token中获取的UserId,通过静态方法从threadLocal中获取UserId,当方法结束后,再手动清除threadlocal就行,这种方法非常简便,但是需要及时清空,不然会造成内存泄漏,因为TreadLocal类似于一个线程Map,但是key是弱引用,一旦GCkey就会清空,但Value是强引用不会清楚,就~~
- 方法二,手动去再次获取Token,然后封装成静态方法去解析UserId,这种方法是麻烦了点,就是多做一次获取验证token
package com.fehead.diseaseCare.utility;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fehead.diseaseCare.entities.User;
import com.fehead.diseaseCare.entities.model.UserIdRoleInfo;
import com.fehead.diseaseCare.error.BusinessException;
import com.fehead.diseaseCare.error.EmBusinessError;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
public class JwtUtil {
public static String makeToken(User user) {
//withAudience()存入需要保存在token的信息,这里我把用户ID存入token中
String token="";
Date date = new Date(System.currentTimeMillis()+1000*60*2*24 *30);
token= JWT.create()
.withClaim("userId",user.getId())
.withClaim("role",user.getRole())
.withExpiresAt(date)
.sign(Algorithm.HMAC256("xxxxx4234{}d]]fxxxxdsadasdasdsad"));
return token;
}
public static UserIdRoleInfo getUserIdByToken(){
ServletRequestAttributes requestAttributes = ServletRequestAttributes.class.
cast(RequestContextHolder.getRequestAttributes());
HttpServletRequest req = requestAttributes.getRequest();
String token = req.getHeader("token");
UserIdRoleInfo userIdRoleInfo=new UserIdRoleInfo();
JWTVerifier build = JWT.require(Algorithm.HMAC256("xxxxx4234{}d]]fxxxxdsadasdasdsad")).build();
DecodedJWT jwt=null;
try {
jwt = build.verify(token);
}catch (Exception e){
throw new BusinessException(EmBusinessError.USER_AUTH_ERROR);
}
int userId = jwt.getClaim("userId").asInt();
int role =jwt.getClaim("role").asInt();
userIdRoleInfo.setUserId(userId);
userIdRoleInfo.setRole(role);
return userIdRoleInfo;
}
}
- withClaim(“userId”,user.getId()) 设置token的明文数据
- withExpiresAt(date) 设置过期时间
- sign(Algorithm.HMAC256(“xxxxx4234{}d]]fxxxxdsadasdasdsad”)) 设置密钥加解密,密钥就是中间的字符串,也可以使用用户密码作为密钥
- 使用JwtUtil封装静态求情
至此,整个项目大概结构就介绍完了,家人们不会都没长脑袋吧
我拿这篇博客我不点赞,我收藏,唉就是玩儿~