zoukankan      html  css  js  c++  java
  • 【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.安全性

    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

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

  • 相关阅读:
    Android 系统广播机制
    NBUT 1457 Sona (莫队算法)
    内存分配--静态内存、栈和堆
    Hibernate主键生成策略
    UVA 1482
    servlet开篇
    C语言的代码内存布局具体解释
    Mirantis Fuel fundations
    openstack中文文档
    C++ 之再继续
  • 原文地址:https://www.cnblogs.com/silent-bug/p/13615180.html
Copyright © 2011-2022 走看看