【JWT】初识与集成,及优缺点

个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


前言

随着分布式的普及,session的成本正变得越来越高,因而一种不需要session,而直接将身份信息放在token中的方案应运而生–JWT


请留意,本文主要整理相关思路,为方便理解,示例代码并不完整,更谈不上严谨

若需要实际使用的demo,请直接查看整理后的代码,完整demo会传到github或码云


1.介绍

1.1.什么是JWT

JWT全程为Json web token,是一种开放标准(RFC 7519),定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。

这种数字签名的设计,紧密且安全,特别适用与分布式登录和单点登录的场景。

JWT一般用于验证身份,也可以根据业务,添加其他业务逻辑所需要的的信息,如权限。

JWT可以直接用于认证,也可以进行加密。


1.2.架构

image-20200826100521370

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
  }
})

发起的请求头如下所示

image-20200826114453473


3.2.后端使用

登录:
  • 接收登录请求,验证账号密码等信息,确认其身份验证无误
  • 查询其他业务相关信息,如身份权限等,组装成payload数据
  • 通过预设规则,生成JWT
  • 通过http的response返回JWT给前端,结束请求
其余请求:
  • 接收请求,取出头字段Authorization,即认证信息
  • 根据预设规则,解码获得明文信息,对JWT进行验证
  • 获取JWT中的业务相关信息,并处理此请求相关业务
  • 返回业务结果给前端,结束请求

4.集成(基于Java+SpringBoot+AOP)

还有其他集成方法,有兴趣的可以自行查阅

4.1.添加依赖

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

添加一个依赖就行了,其余的没必要


# 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<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;
    }

}

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<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);
    }

}

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<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;
    }
}

执行结果

  • 执行登录接口,若账号密码正确则返回token字符串

    image-20200826181729989
  • 执行测试接口,将token放在请求头里

    image-20200827094946884


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 echo_yezi@qq.com
 */
@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 echo_yezi@qq.com
 */
@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 echo_yezi@qq.com
 */
@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 echo_yezi@qq.com
 */
@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 echo_yezi@qq.com
 */
@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 echo_yezi@qq.com
 */
@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 echo_yezi@qq.com
 */
@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 echo_yezi@qq.com
 */
@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 echo_yezi@qq.com
 */
@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;
    }
}

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 echo_yezi@qq.com
 */
@Component
public class JwtUtils {

    /**
     * gson对象,提前初始化
     */
    private final static Gson gson = new Gson();

    /**
     * gson解码
     *
     * @param encoded json字符串
     * @param t       解码目标类型
     * @param <T>     解码目标类型
     * @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 echo_yezi@qq.com
 */
@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;
    }
}
运行截图
  • 登录

    image-20200903172126432

  • 验证

    image-20200904171723793

7.3.全部代码

demo地址:https://gitee.com/echo_ye/jwt-demo

demo已能正常运转预期所有功能,但仅供参考,请视实际业务自行删减和修改,有疑问或者建议可以留言或者联系我~


BB两句

其实考虑到JWT的弊端,JWT在与传统session认证的比较之下,并不具备太多优势,甚至是部分地方有着无法弥补的劣势

权衡之下,个人不建议使用JWT取代session认证



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页