个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道
如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充
前言
随着分布式的普及,session的成本正变得越来越高,因而一种不需要session,而直接将身份信息放在token中的方案应运而生--JWT
请留意,本文主要整理相关思路,为方便理解,示例代码并不完整,更谈不上严谨
若需要实际使用的demo,请直接查看整理后的代码,完整demo会传到github或码云
1.介绍
1.1.什么是JWT
JWT全程为Json web token,是一种开放标准(RFC 7519),定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。
这种数字签名的设计,紧密且安全,特别适用与分布式登录和单点登录的场景。
JWT一般用于验证身份,也可以根据业务,添加其他业务逻辑所需要的的信息,如权限。
JWT可以直接用于认证,也可以进行加密。
1.2.架构
1.3.与传统session认证的区别
打个比方:一个用户在权限森严的地区活动
- session认证:登记时告知用户一个编号,用户到一个地方就报出自己的编号,然后工作人员去查询这个编号能去哪不能去哪,以此来判断是否放行
- JWT认证:登记时发给用户一个证件,用户到一个地方就出示证件,工作人员确认证件是自己家的,然后直接看证件上写了能去哪,以此判断是否放行
两者的利弊显而易见,
传统session认证的方案:传统session认证,一般仅在前后端传递cookie,作为session的关键词,后端再根据cookie查询对应的session,从而确认登陆者的身份和权限等信息。session通常存于缓存、数据库或者redis等中间件,redis最为常见。
-
传统session认证的弊端:无论将session存于何处,用户登录的时候都必须存储认证信息,且大部分请求都执行一次session查询,因而
- 随着用户越来越多,服务器的开销必然越来越大,认证速度也必然受到影响。
- 每次查询session都必须请求其存储的服务器,无疑限制了分布式中负载均衡的能力
JWT的方案:JWT不需要将认证信息进行保存,直接将其加密后在前后端传递,后端进行解密即可获取身份和其他声明信息
-
JWT的优势:JWT的优势即解决了session认证的弊端,JWT将认证信息在前后端传递,而后端本身不存储信息,因而
- 后端不存储信息,故服务器开销极低,且认证速度不会受用户数量影响
- 后端直接解析token,无需请求其他的服务器,不会给负载均衡带来影响,易于扩展
- 业务json的通用性,JWT也拥有了跨平台的能力,在Java、JS、NodeJS、PHP等语言均可使用
- jwt一般存放于请求头中,结构简单且数据量很小,非常便于传输
JWT的弊端:有点多,留在文末说,不然可能会打消你继续看下去的想法。。。综合考虑其实我不建议JWT替代session认证
2.结构
JWT由三段信息组成:头部(header)、载荷(payload)、签证(signature)
https://jwt.io/可以模拟JWT的生成和解码
2.1.header
头部包含两部分信息:
- 声明类型(typ):默认值即
JWT
, - 加密算法(alg):默认值为
HS256
,也可选择其他加密算法
示例:
{
'typ': 'JWT',
'alg': 'HS256'
}
对应base64UrlEncode
编码为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2.2.payload
载荷即存放有效数据的地方,但是因为==可被解码,不建议存放敏感信息==
包括三个部分:
- 标准中注册的声明:建议但不强制使用,存放JWT相关的数据
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
- 公共的声明:可存放任何信息,一般添加用户信息和相关业务的数据
- 私有的声明:提供者和消费者所共同定义的声明
示例:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
对应base64UrlEncode
编码为:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
2.3.signature
签证实际上就是将header和payload进行base64编码,再通过秘钥加密后的密文,用于保证jwt不会被伪造或人为修改,生成方式如下
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
因此秘钥非常重要,必须保证其不会被泄露或破解,否则整个验证系统将如同虚设
示例:
将前面两个示例信息,使用秘钥echo
进行加密的结果为QrtQpbkSSLyGt1qRQ4nZ3K0OcyO7CCv0HxIdsvYYSFU
3.使用流程
3.1.前端使用
前端只需在登录成功后保存返回的token,在发起其他请求的时候,向请求头中加入字段Authorization
,并加上Bearer标注即可
fetch('api/user/info', {
headers: {
'Authorization': 'Bearer ' + token
}
})
发起的请求头如下所示
3.2.后端使用
登录:
- 接收登录请求,验证账号密码等信息,确认其身份验证无误
- 查询其他业务相关信息,如身份权限等,组装成payload数据
- 通过预设规则,生成JWT
- 通过http的response返回JWT给前端,结束请求
其余请求:
- 接收请求,取出头字段
Authorization
,即认证信息 - 根据预设规则,解码获得明文信息,对JWT进行验证
- 获取JWT中的业务相关信息,并处理此请求相关业务
- 返回业务结果给前端,结束请求
4.集成(基于Java+SpringBoot+AOP)
还有其他集成方法,有兴趣的可以自行查阅
4.1.添加依赖
com.auth0
java-jwt
3.10.3
添加一个依赖就行了,其余的没必要
# JWT
# 发行者
spring.jwt.name=echo
# 密钥
spring.jwt.base64Secret=23333
# jwt中过期时间设置(分)
spring.jwt.jwtExpires=120
秘钥是最核心的数据,是保证令牌不被伪造的唯一防线,无论如何也不能泄露,安全起见建议每个项目都生成随机的字符串作为秘钥
4.2.自定义注解
注解用于标记需要认证的目标,当然也可以标记不需要认证的目标,最好使用AOP自定义注解拦截,来应对不同业务需求
-
注解
@JwtCheck
,用于标记接口需要验证,只需要@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface JwtCheck { boolean required() default true; }
其余注解请根据实际业务添加
4.3.JWT工具类
核心就是生成和解析token,还有验证token
- 生成token使用
JWT.create()
再组装需要的参数即可,因为方法都是限定好的,比较简单,几乎不可能出错吧。。。- 解析token直接解析请求头
Authorization
的内容就行了,自行去除最前面的Bearer
,剩余内容使用base64解码即可- 验证token是最关键的一步,也最好理解,重复一遍加密过程,然后把加密结果与签名对比,两者匹配即保证令牌不是伪造的
@Component
public class JwtUtils {
/**
* gson对象,提前初始化
*/
private final static Gson gson = new Gson();
/**
* jwt秘钥
*/
private static String jwtSecret;
/**
* jwt有效时间
*/
private static Long jwtExpires;
/**
* jwt发行者名称
*/
private static String jwtName;
@Value("${spring.jwt.base64Secret}")
public void setJwtSecret(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
@Value("${spring.jwt.jwtExpires}")
public void setJwtExpires(Long jwtExpires) {
this.jwtExpires = jwtExpires;
}
@Value("${spring.jwt.name}")
public void setJwtName(String jwtName) {
this.jwtName = jwtName;
}
/**
* 获取token字符串
*
* @param data 对象
* @return token字符串
*/
public static String getToken(Object data) {
String token = "";
// 计算时间
Date expiredDate = new Date(System.currentTimeMillis() + jwtExpires * 1000L);
Date issuedDate = new Date();
// 创建jwt
token = JWT
.create()
.withAudience(gson.toJson(data))
.withIssuer(jwtName)
.withIssuedAt(issuedDate)
.withExpiresAt(expiredDate)
.sign(Algorithm.HMAC256(jwtSecret));
return token;
}
/**
* gson解码
*
* @param encoded json字符串
* @return 解码后的对象
*/
public static UserInfo decode(String encoded) {
return gson.fromJson(encoded, UserInfo.class);
}
/**
* 验证token
*
* @param token token字符串
* @throws Exception
*/
public static void verifyToken(String token) throws Exception {
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecret)).build();
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new BaseException("身份验证失败");
}
}
/**
* 从http请求中解析token
*
* @param request http请求
* @throws Exception 解析异常
* @returntoken字符串
*/
public static String getToken(HttpServletRequest request) throws Exception {
String authorization = Strings.nullToEmpty(request.getHeader("Authorization"));
if (!authorization.startsWith("Bearer")) {
throw new BaseException("Token非JWT标准");
}
return authorization.substring(7);
}
}
4.4.数据查询
为方便测试仅做模拟查询,实际应用请移步从数据库查询,可参考文末整理后的代码
@Service
public class UserService {
/**
* 模拟数据库的数据
*/
private List userInfoList;
public UserService() {
userInfoList = new ArrayList<>();
userInfoList.add(new UserInfo(1, "user1", "pwd1", 1));
userInfoList.add(new UserInfo(2, "user2", "pwd2", 2));
userInfoList.add(new UserInfo(3, "user3", "pwd3", 3));
}
/**
* 根据id查询用户
*
* @param id 用户id
* @return 用户信息
*/
public UserInfo getUserById(Long id) {
for (UserInfo item : userInfoList) {
if (Objects.equals(item.getId(), id)) {
return item;
}
}
return null;
}
/**
* 校验账号
*
* @param username 账号
* @param password 密码
* @return 若查询到账号则返回账号信息,否则返回null
*/
public UserInfo checkUser(String username, String password) {
for (UserInfo item : userInfoList) {
if (Objects.equals(item.getUsername(), username) && Objects.equals(item.getPassword(), password)) {
return item;
}
}
return null;
}
}
4.5.切面层拦截器
这里应该是最关键的地方,用于拦截注解标记的方法,在这里进行身份验证,失败则不再继续,成功则回到切入点
- 使用
@Pointcut
设置切入点的条件- 在使用
@Around
设定切入的方法,若中途验证失败,则抛出异常,若成功则继续执行原有逻辑
@Aspect
@Component
@Slf4j
public class JwtInterceptAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.yezi_tool.demo_basic.controller..*(..))&&@annotation(com.yezi_tool.demo_basic.jwt.JwtCheck)")
public void controllerAspect() {
}
/**
* 切入方法
*/
@Around("controllerAspect() ")
public Object aroundMethod(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = null;
UserInfo user = null;
JwtCheck jwtCheck = ((MethodSignature) point.getSignature()).getMethod().getAnnotation(JwtCheck.class);
if (jwtCheck.required()) {
request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String token = JwtUtils.getToken(request);
//获取token的userid
List audience = JWT.decode(token).getAudience();
user = JwtUtils.decode(audience.get(0));
if (user == null) {
throw new BaseException("身份验证失败");
}
//验证token,秘钥为用户的密码
JwtUtils.verifyToken(token);
}
Object[] args = point.getArgs();
UserInfo finalUser = user;
args = Arrays.stream(args).map(arg -> {
if (Objects.nonNull(arg) && UserInfo.class.isAssignableFrom(arg.getClass()))
arg = finalUser;
return arg;
}).toArray();
return point.proceed(args);
}
}
4.6.控制层
简单的写两个方法,一个用于登录,一个用于测试登录结果
@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {
/**
* 用户信息业务层
*/
private final UserService userService;
public LoginController(UserService userService) {
this.userService = userService;
}
@Data
private static class LoginRequest {
private String username;
private String password;
private Boolean rememberMe;
}
@PostMapping("/login")
@ResponseBody
public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
UserInfo userInfo = userService.checkUser(loginRequest.getUsername(), loginRequest.getPassword());
if (userInfo == null) {
throw new BaseException("账号或密码不正确");
}
Map data = new HashMap<>();
data.put("id", userInfo.getId());
data.put("username", userInfo.getUsername());
returnMsg.setData(JwtUtils.getToken(data));
return returnMsg;
}
@PostMapping("/test")
@ResponseBody
@JwtCheck
public ReturnMsg test(Integer mark) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
if (mark == null) {
throw new BaseException("缺少参数");
}
returnMsg.setData(userInfo);
return returnMsg;
}
}
执行结果
-
执行登录接口,若账号密码正确则返回token字符串
-
执行测试接口,将token放在请求头里
5.JWT的弊端
以下均不考虑查询数据库或者缓存,否则就相当于放弃自己仅有的优势
5.1.安全性
- JWT令牌使用base64编码,对于前端如同明文(http://jwt.calebb.net/不信你试试?),那么敏感信息不适合放在令牌里,但如果从数据库获取便放弃了自己的优势
- 秘钥一旦泄露,客户端便可以自己签发令牌,除非修改秘钥,那么所有令牌全部失效
5.2.不可修改
- 如果令牌相关的内容被修改,如账号,身份,姓名等,只能让用户重新登录
- 令牌无法续签,到时间即失效,除非将时间设定的极长
5.3.不可销毁
如果需要强制下线,或者报废旧的令牌,JWT完全无法做到
就算下发新的令牌,旧的令牌在时效内依然有效
5.4.性能
JWT的payload内容越多,令牌便越大,开销也会越来越大,甚至超出cookie的长度限制(cookie一般限制在4k,而redis是512M。。),请求带的参数往往只有几个,本末倒置了吧。。。
以上问题在session认证均不会出现!!!
6.真正适合JWT的场景
6.1.一次性验证
如用户注册后发送一封激活邮件,包括一个链接,需要用户在时限内点击,超时即失效,
那么需要的以下条件:
- 能标记用户,一般是id或者账号进行标记
- 有时效性,通常只有几个小时时效,超时便失效
- 不可被修改,用于其他账号或者用途
JWT的payload、expires、签名都完美契合以上条件
6.2.restful api 的无状态认证
jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。
7.整理后代码
7.1.整理内容
优化代码结构
可对类或者方法标记需要或跳过认证,并指定认证的身份
统一使用AOP切面拦截请求,进行身份认证,并在认证成功后将身份信息插入切入点
允许不同身份的用户登录,并使用不同格式的令牌
密码使用盐和MD5加密
使用策略设计模式
7.2.核心源码
自定义注解
package com.yezi_tool.demo_basic.jwt;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Echo_Ye
* @title 注解-jwt验证
* @description 用于标记接口jwt验证
* @date 2020/8/26 13:55
* @email [email protected]
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
Class> value();
}
package com.yezi_tool.demo_basic.jwt;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Echo_Ye
* @title 注解-jwt不进行验证
* @description 用于标记接口jwt不进行验证
* @date 2020/8/26 13:55
* @email [email protected]
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoAuth {
}
自定义认证实体
package com.yezi_tool.demo_basic.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.hash.Hashing;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @title JWT认证虚拟类
* @description
* @author Echo_Ye
* @date 2020/9/3 17:11
* @email [email protected]
*/
@SuppressWarnings("UnstableApiUsage")
@Slf4j
public abstract class AbstractAuthByJWT {
/**
* 抽象方法-获取类型
*/
public abstract byte getUserType();
/**
* 抽象方法-解码
*/
protected abstract T decode(String encoded);
/**
* 抽象方法-编码
*/
protected abstract String encode(T bean);
/**
* 抽象方法-获取bean对象
*/
protected abstract T getBean(HttpServletRequest request) throws Exception;
/**
* 抽象方法-获取默认时间
*/
protected abstract Duration getDefaultDuration();
/**
* 相关钩子-暂不使用
*/
protected void hookOnCreate(JWTCreator.Builder builder) {
}
protected void hookOnAddHeader(Map header) {
}
protected void hookOnVerify(DecodedJWT jwt) {
}
/**
* 加密算法
*/
private final Algorithm algorithm;
/**
* jwt验证器
*/
private final JWTVerifier verifier;
/**
* 发布者,取当前class
*/
private final String identity = this.getClass().getName();
/**
* 初始化
*
* @param secret 秘钥
*/
public AbstractAuthByJWT(String secret) {
algorithm = Algorithm.HMAC256(Hashing.sha256().hashBytes(secret.getBytes()).asBytes());
verifier = JWT.require(algorithm).build();
}
/**
* 创建token
*/
public String create(T auth) throws Exception {
return create(auth, getDefaultDuration());
}
/**
* 创建token
*/
public String create(T auth, Duration duration) throws Exception {
try {
// 计算时间
Date expiredDate = new Date(System.currentTimeMillis() + duration.getSeconds() * 1000L);
Date issuedDate = new Date();
// 序列化主数据
String data = encode(auth);
Preconditions.checkState(data.length() > 0);
// 添加头部信息
HashMap header = Maps.newHashMap();
hookOnAddHeader(header);
JWTCreator.Builder builder = JWT.create()
.withHeader(header)
.withClaim(JwtConstants.JWT_REQUEST_CLAIM_KEY, data)
.withIssuer(identity)
.withIssuedAt(issuedDate)
.withExpiresAt(expiredDate);
hookOnCreate(builder);
return builder.sign(algorithm);
} catch (Throwable e) {
throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CREATE_TOKEN));
}
}
/**
* 验证request
*
* @param request request请求
* @return 返回泛型对象
* @throws Exception
*/
protected T verify(HttpServletRequest request) throws Exception {
return verify(JwtUtils.getTokenFromHttpRequest(request));
}
/**
* 验证request
*
* @param token token字符串
* @return 返回泛型对象
*/
public T verify(String token) {
DecodedJWT jwt = verifier.verify(token);
//校验签发者
Preconditions.checkState(0 == jwt.getIssuer().compareTo(identity));
//校验数据
Claim data = jwt.getClaim(JwtConstants.JWT_REQUEST_CLAIM_KEY);
Preconditions.checkNotNull(data);
//钩子
hookOnVerify(jwt);
//解析
T bean = decode(Strings.nullToEmpty(data.asString()));
Preconditions.checkNotNull(bean);
return bean;
}
}
package com.yezi_tool.demo_basic.jwt;
import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.List;
/**
* @author Echo_Ye
* @title jwt学生验证实体
* @description jwt用于学生身份验证的实体
* @date 2020/8/28 13:47
* @email [email protected]
*/
@Component
public class StudentAuthByJWT extends AbstractAuthByJWT {
/**
* 用户类型
*/
@Getter
private final byte userType = CustomConstants.USER_TYPE_STUDENT;
/**
* 初始化
*
* @param secret 秘钥
*/
public StudentAuthByJWT(@Value("${spring.jwt.base64Secret}") String secret) {
super(secret);
}
@Getter
public static class Instance extends BaseAuthInstance {
/**
* 学号
*/
private String num;
/**
* 年级id
*/
private Long gradeId;
public Instance(Integer id, List permissionList, String userName, Byte type, Integer personId, String name, Byte gender, String num, Long gradeId) {
super(id, permissionList, userName, type, personId, name, gender);
this.num = num;
this.gradeId = gradeId;
}
}
@Bean("studentAuthByJWTInstance")
@Scope(value = WebApplicationContext.SCOPE_REQUEST) // 该bean仅在本次http request内有效
@Override
protected Instance getBean(HttpServletRequest request) throws Exception {
return super.verify(request);
}
@Override
protected Instance decode(String encoded) {
return JwtUtils.decode(encoded, Instance.class);
}
@Override
protected String encode(Instance bean) {
return JwtUtils.encode(bean);
}
@Override
protected Duration getDefaultDuration() {
return Duration.ofDays(365);
}
}
package com.yezi_tool.demo_basic.jwt;
import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import com.yezi_tool.demo_basic.entity.UserInfo;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.List;
/**
* @author Echo_Ye
* @title jwt教师验证实体
* @description jwt用于教师身份验证的实体
* @date 2020/8/28 13:47
* @email [email protected]
*/
@Component
public class TeacherAuthByJWT extends AbstractAuthByJWT {
/**
* 用户类型
*/
@Getter
private final byte userType= CustomConstants.USER_TYPE_TEACHER;
/**
* 初始化
*
* @param secret 秘钥
*/
public TeacherAuthByJWT(@Value("${spring.jwt.base64Secret}") String secret) {
super(secret);
}
@Getter
public static class Instance extends BaseAuthInstance {
/**
* 学院id
*/
private Long collegeId;
public Instance(Integer id, List permissionList, String userName, Byte type, Integer personId, String name, Byte gender, Long collegeId) {
super(id, permissionList, userName, type, personId, name, gender);
this.collegeId = collegeId;
}
}
@Bean("teacherAuthByJWTInstance")
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
@Override
protected Instance getBean(HttpServletRequest request) throws Exception {
return super.verify(request);
}
@Override
protected Instance decode(String encoded) {
return JwtUtils.decode(encoded, Instance.class);
}
@Override
protected String encode(Instance bean) {
return JwtUtils.encode(bean);
}
@Override
protected Duration getDefaultDuration() {
return Duration.ofDays(365);
}
}
package com.yezi_tool.demo_basic.jwt;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* @author Echo_Ye
* @title jwt基础instance
* @description 基础instance
* @date 2020/8/28 17:54
* @email [email protected]
*/
@Getter
@AllArgsConstructor
public class BaseAuthInstance {
/**
* id
*/
private Integer id;
/**
* 权限列表
*/
private List permissionList;
/**
* 用户名
*/
private String userName;
/**
* 用户类型
*/
private Byte type;
/**
* 信息表id
*/
private Integer personId;
/**
* 用户姓名
*/
private String name;
/**
* 用户性别
*/
private Byte gender;
}
自定义切面拦截器
package com.yezi_tool.demo_basic.jwt;
import com.google.common.base.Preconditions;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.SpringContextHolder;
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.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Objects;
/**
* @author Echo_Ye
* @title 身份AOP拦截器
* @description 使用AOP拦截身份
* @date 2020/9/3 15:28
* @email [email protected]
*/
@Slf4j
@Aspect
@Component
public class AuthInterceptor {
/**
* AOP切入点
*/
@Pointcut("(@annotation(com.yezi_tool.demo_basic.jwt.Auth) || @within(com.yezi_tool.demo_basic.jwt.Auth)) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
public void authPointcut() {
}
/**
* AOP方法切入点
*/
@Pointcut("@annotation(com.yezi_tool.demo_basic.jwt.Auth) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
public void authAnnotationPointcut() {
}
/**
* AOP对象切入点
*/
@Pointcut("@within(com.yezi_tool.demo_basic.jwt.Auth) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
public void authWithinPointcut() {
}
/**
* AOP对象切入内容
*/
@Around("authWithinPointcut() && @within(auth)")
public Object checkWithinAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
return checkAuth(joinPoint, auth);
}
/**
* AOP方法切入内容
*/
@Around("authAnnotationPointcut() && @annotation(auth)")
public Object checkAnnotationAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
return checkAuth(joinPoint, auth);
}
/**
* AOP切入内容
*
* @param joinPoint 切入点
* @param auth 切入注解
* @return
* @throws Exception 抛出异常,验证失败
*/
public Object checkAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
try {
//认证实体
Object authBean = SpringContextHolder.getBean(auth.value());
Preconditions.checkNotNull(authBean);
//插入数据到切入点
Object[] args = joinPoint.getArgs();
args = Arrays.stream(args).map(arg -> {
if (Objects.nonNull(arg) && arg.getClass().isAssignableFrom(auth.value()))
// if (Objects.nonNull(arg) && arg instanceof BaseAuthInstance)//效果一样,但上面的更利于扩展
arg = authBean;
return arg;
}).toArray();
return joinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CHECK_AUTH));
}
}
}
jwt服务
package com.yezi_tool.demo_basic.service;
import com.yezi_tool.demo_basic.jwt.AbstractAuthByJWT;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Echo_Ye
* @title jwt服务
* @description jwt服务,待扩充
* @date 2020/9/3 17:17
* @email [email protected]
*/
@Service
public class JwtService {
Map jwtAuthMap = new HashMap<>();
public JwtService(List abstractAuthByJWTList) {
for (AbstractAuthByJWT auth : abstractAuthByJWTList) {
jwtAuthMap.put(auth.getUserType(), auth);
}
}
public AbstractAuthByJWT getAuth(byte type) {
return jwtAuthMap.get(type);
}
}
服务层实现类
package com.yezi_tool.demo_basic.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yezi_tool.demo_basic.commons.constants.CommonConstants;
import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.constants.ResponseConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import com.yezi_tool.demo_basic.entity.PermissionInfo;
import com.yezi_tool.demo_basic.entity.StudentInfo;
import com.yezi_tool.demo_basic.entity.TeacherInfo;
import com.yezi_tool.demo_basic.entity.UserInfo;
import com.yezi_tool.demo_basic.jwt.StudentAuthByJWT;
import com.yezi_tool.demo_basic.jwt.TeacherAuthByJWT;
import com.yezi_tool.demo_basic.mapper.PermissionInfoMapper;
import com.yezi_tool.demo_basic.mapper.StudentInfoMapper;
import com.yezi_tool.demo_basic.mapper.TeacherInfoMapper;
import com.yezi_tool.demo_basic.mapper.UserInfoMapper;
import com.yezi_tool.demo_basic.service.IUserInfoService;
import com.yezi_tool.demo_basic.service.JwtService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
/**
* @title 用户信息服务层
* @description
* @author Echo_Ye
* @date 2020/9/3 17:18
* @email [email protected]
*/
@Slf4j
@Service("userInfoService")
public class UserInfoServiceImpl extends BaseServiceImpl implements IUserInfoService {
private final UserInfoMapper userInfoMapper;
private final PermissionInfoMapper permissionInfoMapper;
private final StudentInfoMapper studentInfoMapper;
private final TeacherInfoMapper teacherInfoMapper;
private final JwtService jwtService;
public UserInfoServiceImpl(UserInfoMapper userInfoMapper, PermissionInfoMapper permissionInfoMapper, StudentInfoMapper studentInfoMapper, TeacherInfoMapper teacherInfoMapper, JwtService jwtService) {
this.userInfoMapper = userInfoMapper;
this.permissionInfoMapper = permissionInfoMapper;
this.studentInfoMapper = studentInfoMapper;
this.teacherInfoMapper = teacherInfoMapper;
this.jwtService = jwtService;
}
@Override
public UserInfo selectByUserName(String userName) {
return userInfoMapper.selectOne(new QueryWrapper().eq(UserInfo.COL_USERNAME, userName));
}
@Override
public UserInfo selectByMobile(String mobile) {
return userInfoMapper.selectOne(new QueryWrapper().eq(UserInfo.COL_MOBILE, mobile));
}
@Override
public List queryPermission(String username) {
List
jwt相关工具类
package com.yezi_tool.demo_basic.commons.utils;
import com.google.common.base.Strings;
import com.google.common.hash.Hashing;
import com.google.gson.Gson;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @author Echo_Ye
* @title jwt工具
* @description 封装jwt相关方法
* @date 2020/8/26 17:46
* @email [email protected]
*/
@Component
public class JwtUtils {
/**
* gson对象,提前初始化
*/
private final static Gson gson = new Gson();
/**
* gson解码
*
* @param encoded json字符串
* @param t 解码目标类型
* @param 解码目标类型
* @return 解码后的对象
*/
public static T decode(String encoded, Class t) {
return gson.fromJson(encoded, t);
}
/**
* gson编码
*
* @param t 需要编码的对象
* @return 编码后的结果
*/
public static String encode(Object t) {
return gson.toJson(t);
}
/**
* 从http请求中解析token
*
* @param request http请求
* @return token字符串
* @throws Exception 解析异常
*/
public static String getTokenFromHttpRequest(HttpServletRequest request) throws Exception {
String authorization = Strings.nullToEmpty(request.getHeader(JwtConstants.JWT_REQUEST_HEAD_KEY));
if (!authorization.startsWith(JwtConstants.JWT_REQUEST_HEAD_PREFIX)) {
throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CHECK_AUTH));
}
return authorization.substring(7);
}
/**
* 生成盐
*/
public static String generateSalt() {
return RandomStringUtils.randomAlphanumeric(16);
}
/**
* 密码加密
*/
public static String encryptPassword(String password, String salt) {
return Hashing.hmacMd5(salt.getBytes()).hashBytes(password.getBytes()).toString();
}
/**
* 密码验证
*/
public static boolean verifyPassword(String username,String password, String salt, String encryptedPassword) {
return encryptPassword(Strings.nullToEmpty(encryptedPassword), salt).equals(password);
}
}
控制层接口
package com.yezi_tool.demo_basic.controller;
import com.yezi_tool.demo_basic.commons.constants.ResponseConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.jwt.*;
import com.yezi_tool.demo_basic.service.IUserInfoService;
import lombok.Data;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @author Echo_Ye
* @title 登录接口
* @description 用于登录相关接口
* @date 2020/8/17 9:39
* @email [email protected]
*/
@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {
/**
* 用户信息业务层
*/
private final IUserInfoService userInfoService;
public LoginController(IUserInfoService userInfoService) {
this.userInfoService = userInfoService;
}
@Data
private static class LoginRequest {
private String username;
private String password;
private Boolean rememberMe;
}
@PostMapping("/login")
@ResponseBody
public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
String token = userInfoService.loginByUserNameAndPassword(loginRequest.getUsername(), loginRequest.getPassword());
returnMsg.setData(token);
return returnMsg;
}
@PostMapping("/testTeacher")
@ResponseBody
@Auth(TeacherAuthByJWT.Instance.class)
public ReturnMsg testTeacher(BaseAuthInstance auth, Integer mark) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
if (mark == null) {
throw new BaseException(ResponseConstants.RETURN_MSG_ABNORMAL_PARAM);
}
returnMsg.setData(auth);
return returnMsg;
}
@PostMapping("/testStudent")
@ResponseBody
@Auth(StudentAuthByJWT.Instance.class)
public ReturnMsg testStudent(BaseAuthInstance auth, Integer mark) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
if (mark == null) {
throw new BaseException(ResponseConstants.RETURN_MSG_ABNORMAL_PARAM);
}
returnMsg.setData(auth);
return returnMsg;
}
}
运行截图
-
登录
-
验证
7.3.全部代码
demo地址:https://gitee.com/echo_ye/jwt-demo
demo已能正常运转预期所有功能,但仅供参考,请视实际业务自行删减和修改,有疑问或者建议可以留言或者联系我~
BB两句
其实考虑到JWT的弊端,JWT在与传统session认证的比较之下,并不具备太多优势,甚至是部分地方有着无法弥补的劣势
==权衡之下,个人不建议使用JWT取代session认证==
作者:Echo_Ye
WX:Echo_YeZ
EMAIL :[email protected]
个人站点:在搭了在搭了。。。(右键 - 新建文件夹)