个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道
如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充
随着分布式的普及,session的成本正变得越来越高,因而一种不需要session,而直接将身份信息放在token中的方案应运而生–JWT
请留意,本文主要整理相关思路,为方便理解,示例代码并不完整,更谈不上严谨
若需要实际使用的demo,请直接查看整理后的代码,完整demo会传到github或码云
JWT全程为Json web token,是一种开放标准(RFC 7519),定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。
这种数字签名的设计,紧密且安全,特别适用与分布式登录和单点登录的场景。
JWT一般用于验证身份,也可以根据业务,添加其他业务逻辑所需要的的信息,如权限。
JWT可以直接用于认证,也可以进行加密。
打个比方:一个用户在权限森严的地区活动
- session认证:登记时告知用户一个编号,用户到一个地方就报出自己的编号,然后工作人员去查询这个编号能去哪不能去哪,以此来判断是否放行
- JWT认证:登记时发给用户一个证件,用户到一个地方就出示证件,工作人员确认证件是自己家的,然后直接看证件上写了能去哪,以此判断是否放行
两者的利弊显而易见,
传统session认证的方案:传统session认证,一般仅在前后端传递cookie,作为session的关键词,后端再根据cookie查询对应的session,从而确认登陆者的身份和权限等信息。session通常存于缓存、数据库或者redis等中间件,redis最为常见。
传统session认证的弊端:无论将session存于何处,用户登录的时候都必须存储认证信息,且大部分请求都执行一次session查询,因而
JWT的方案:JWT不需要将认证信息进行保存,直接将其加密后在前后端传递,后端进行解密即可获取身份和其他声明信息
JWT的优势:JWT的优势即解决了session认证的弊端,JWT将认证信息在前后端传递,而后端本身不存储信息,因而
JWT的弊端:有点多,留在文末说,不然可能会打消你继续看下去的想法。。。综合考虑其实我不建议JWT替代session认证
JWT由三段信息组成:头部(header)、载荷(payload)、签证(signature)
https://jwt.io/可以模拟JWT的生成和解码
JWT
,HS256
,也可选择其他加密算法{
'typ': 'JWT',
'alg': 'HS256'
}
对应base64UrlEncode
编码为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
载荷即存放有效数据的地方,但是因为可被解码,不建议存放敏感信息
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
对应base64UrlEncode
编码为:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
签证实际上就是将header和payload进行base64编码,再通过秘钥加密后的密文,用于保证jwt不会被伪造或人为修改,生成方式如下
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
因此秘钥非常重要,必须保证其不会被泄露或破解,否则整个验证系统将如同虚设
将前面两个示例信息,使用秘钥echo
进行加密的结果为QrtQpbkSSLyGt1qRQ4nZ3K0OcyO7CCv0HxIdsvYYSFU
前端只需在登录成功后保存返回的token,在发起其他请求的时候,向请求头中加入字段Authorization
,并加上Bearer标注即可
fetch('api/user/info', {
headers: {
'Authorization': 'Bearer ' + token
}
})
发起的请求头如下所示
Authorization
,即认证信息还有其他集成方法,有兴趣的可以自行查阅
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.10.3version>
dependency>
添加一个依赖就行了,其余的没必要
# JWT
# 发行者
spring.jwt.name=echo
# 密钥
spring.jwt.base64Secret=23333
# jwt中过期时间设置(分)
spring.jwt.jwtExpires=120
秘钥是最核心的数据,是保证令牌不被伪造的唯一防线,无论如何也不能泄露,安全起见建议每个项目都生成随机的字符串作为秘钥
注解用于标记需要认证的目标,当然也可以标记不需要认证的目标,最好使用AOP自定义注解拦截,来应对不同业务需求
注解@JwtCheck
,用于标记接口需要验证,只需要
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtCheck {
boolean required() default true;
}
其余注解请根据实际业务添加
核心就是生成和解析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);
}
}
为方便测试仅做模拟查询,实际应用请移步从数据库查询,可参考文末整理后的代码
@Service
public class UserService {
/**
* 模拟数据库的数据
*/
private List<UserInfo> 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;
}
}
这里应该是最关键的地方,用于拦截注解标记的方法,在这里进行身份验证,失败则不再继续,成功则回到切入点
- 使用
@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<String> 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);
}
}
简单的写两个方法,一个用于登录,一个用于测试登录结果
@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<String, Object> 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;
}
}
以下均不考虑查询数据库或者缓存,否则就相当于放弃自己仅有的优势
如果需要强制下线,或者报废旧的令牌,JWT完全无法做到
就算下发新的令牌,旧的令牌在时效内依然有效
JWT的payload内容越多,令牌便越大,开销也会越来越大,甚至超出cookie的长度限制(cookie一般限制在4k,而redis是512M。。),请求带的参数往往只有几个,本末倒置了吧。。。
以上问题在session认证均不会出现!!!
如用户注册后发送一封激活邮件,包括一个链接,需要用户在时限内点击,超时即失效,
那么需要的以下条件:
JWT的payload、expires、签名都完美契合以上条件
jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。
优化代码结构
可对类或者方法标记需要或跳过认证,并指定认证的身份
统一使用AOP切面拦截请求,进行身份认证,并在认证成功后将身份信息插入切入点
允许不同身份的用户登录,并使用不同格式的令牌
密码使用盐和MD5加密
使用策略设计模式
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<T> {
/**
* 抽象方法-获取类型
*/
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<String, Object> 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<String, Object> 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<StudentAuthByJWT.Instance> {
/**
* 用户类型
*/
@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<String> 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<TeacherAuthByJWT.Instance> {
/**
* 用户类型
*/
@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<String> 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<String> 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));
}
}
}
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<Byte, AbstractAuthByJWT> jwtAuthMap = new HashMap<>();
public JwtService(List<AbstractAuthByJWT> 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<UserInfo> 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<UserInfo>().eq(UserInfo.COL_USERNAME, userName));
}
@Override
public UserInfo selectByMobile(String mobile) {
return userInfoMapper.selectOne(new QueryWrapper<UserInfo>().eq(UserInfo.COL_MOBILE, mobile));
}
@Override
public List<String> queryPermission(String username) {
List<Map<String, Object>> list = permissionInfoMapper.selectByUsername(username);
List<String> permissionList = list.size() > 0 ? list.stream().
map(m -> String.valueOf(m.get(PermissionInfo.COL_PERMISSION_NAME))).
collect(Collectors.toList()) : new ArrayList<>();
return permissionList;
}
@Override
public UserInfo checkUser(String username, String password) {
return userInfoMapper.selectOne(new QueryWrapper<UserInfo>()
.eq(UserInfo.COL_USERNAME, username)
.eq(UserInfo.COL_PASSWORD, password)
);
}
@Override
public String loginByMobileAndPassword(String mobile, String password) throws Exception {
return loginGeneric(selectByMobile(mobile)
, user -> JwtUtils.verifyPassword(user.getUsername(), user.getPassword(), user.getSalt(), password)
);
}
@Override
public String loginByUserNameAndPassword(String username, String password) throws Exception {
return loginGeneric(selectByUserName(username)
, user -> JwtUtils.verifyPassword(user.getUsername(), user.getPassword(), user.getSalt(), password)
);
}
/**
* 通用登录逻辑
*
* @param user 用户实体
* @param callback 验证回调
* @return Token
* @throws BaseException 登录异常
*/
private String loginGeneric(UserInfo user, Function<UserInfo, Boolean> callback) throws Exception {
//检查账号
if (null == user) {
throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_INCORRECT_USERNAME);
}
//检查密码
if (!callback.apply(user)) {
throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_INCORRECT_PASSWORD);
}
//检查账号是否被禁用
if (user.getStatus() == CommonConstants.STATE_DISABLED) {
throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_ACCOUNT_DISABLE);
}
//判断登陆者类型
String token = makeToken(user);
if (StringUtils.isBlank(token)) {
throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CREATE_TOKEN));
}
return token;
}
/**
* 根据用户类型生成token,主要用户判断用户类型
*
* @param userInfo 用户信息
* @throws Exception 可能抛出自定义异常
* @return生成的toen字符串
*/
public String makeToken(UserInfo userInfo) throws Exception {
//开始生成token
String token = "";
switch (userInfo.getType()) {
case CustomConstants.USER_TYPE_ADMIN:
//管理员,暂不处理该类型人员
break;
case CustomConstants.USER_TYPE_STUDENT:
//学生
StudentInfo studentInfo = studentInfoMapper.selectById(userInfo.getPersonId());
token = jwtService.
getAuth(CustomConstants.USER_TYPE_STUDENT).
create(new StudentAuthByJWT.Instance(
userInfo.getId(),
queryPermission(userInfo.getUsername()),
userInfo.getUsername(),
userInfo.getType(),
userInfo.getPersonId(),
studentInfo.getName(),
studentInfo.getGender(),
studentInfo.getNum(),
studentInfo.getGradeId()));
break;
case CustomConstants.USER_TYPE_TEACHER:
//老师
TeacherInfo teacherInfo = teacherInfoMapper.selectById(userInfo.getPersonId());
token = jwtService.
getAuth(CustomConstants.USER_TYPE_TEACHER).
create(new TeacherAuthByJWT.Instance(
userInfo.getId(),
queryPermission(userInfo.getUsername()),
userInfo.getUsername(),
userInfo.getType(),
userInfo.getPersonId(),
teacherInfo.getName(),
teacherInfo.getGender(),
teacherInfo.getCollegeId()));
break;
default:
break;
}
return token;
}
}
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> T decode(String encoded, Class<T> 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;
}
}
demo地址:https://gitee.com/echo_ye/jwt-demo
demo已能正常运转预期所有功能,但仅供参考,请视实际业务自行删减和修改,有疑问或者建议可以留言或者联系我~
其实考虑到JWT的弊端,JWT在与传统session认证的比较之下,并不具备太多优势,甚至是部分地方有着无法弥补的劣势
权衡之下,个人不建议使用JWT取代session认证
作者:Echo_Ye
WX:Echo_YeZ
EMAIL :[email protected]
个人站点:在搭了在搭了。。。(右键 - 新建文件夹)