精华内容
下载资源
问答
  • Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息...

    Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519). 定义了一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT 可以使用 HMAC 算法或者是 RSA 的公私秘钥对进行签名。

    JWT 请求流程

    这里还要注意:光理论是不够的。在此顺便送大家十套2020最新JAVA架构项目实战教程及大厂面试题库,进我扣裙 :七吧伞吧零而衣零伞 (数字的谐音)转换下可以找到了,还可以跟老架构师交流

    1. 用户使用账号和面发出 post 请求;

    2. 服务器使用私钥创建一个 jwt;

    3. 服务器返回这个 jwt 给浏览器;

    4. 浏览器将该 jwt 串在请求头中像服务器发送请求;

    5. 服务器验证该 jwt;

    6. 返回响应的资源给浏览器。

    JWT 的主要应用场景

    身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含 JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用 JWT 对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

    优点

    1. 简洁 (Compact): 可以通过 URL,POST 参数或者在 HTTP header 发送,因为数据量小,传输速度也很快

    2. 自包含 (Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库

    3. 因为 Token 是以 JSON 加密的形式保存在客户端的,所以 JWT 是跨语言的,原则上任何 web 形式都支持。

    4. 不需要在服务端保存会话信息,特别适用于分布式微服务。

    `

    JWT 的结构

    JWT 是由三段信息构成的,将这三段信息文本用. 连接一起就构成了 JWT 字符串。

    就像这样:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

    JWT 包含了三部分:

    Header 头部 (标题包含了令牌的元数据,并且包含签名和 / 或加密算法的类型)

    Payload 负载 (类似于飞机上承载的物品)

    Signature 签名 / 签证

    Header

    JWT 的头部承载两部分信息:token 类型和采用的加密算法。

    {

    "alg": "HS256",

    "typ": "JWT"

    }

    声明类型:这里是 jwt

    声明加密的算法:通常直接使用 HMAC SHA256

    加密算法是单向函数散列算法,常见的有 MD5、SHA、HAMC。

    MD5(message-digest algorithm 5) (信息 - 摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过 MD5 后都能生成唯一的 MD5 值

    SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于 MD5

    HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的 Hash 算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证

    Payload

    载荷就是存放有效信息的地方。

    有效信息包含三个部分

    1. 标准中注册的声明

    2. 公共的声明

    3. 私有的声明

    标准中注册的声明 (建议但不强制使用) :

    iss: jwt 签发者

    sub: 面向的用户 (jwt 所面向的用户)

    aud: 接收 jwt 的一方

    exp: 过期时间戳 (jwt 的过期时间,这个过期时间必须要大于签发时间)

    nbf: 定义在什么时间之前,该 jwt 都是不可用的.

    iat: jwt 的签发时间

    jti: jwt 的唯一身份标识,主要用来作为一次性 token, 从而回避重放攻击。

    公共的声明 :

    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密.

    私有的声明 :

    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。

    Signature

    jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:

    header (base64 后的)

    payload (base64 后的)

    secret

    这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用. 连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。

    密钥 secret 是保存在服务端的,服务端会根据这个密钥进行生成 token 和进行验证,所以需要保护好。

    下面来进行 SpringBoot 和 JWT 的集成

    引入 JWT 依赖,由于是基于 Java,所以需要的是 java-jwt

    com.auth0

    java-jwt

    3.4.0

    需要自定义一个注解

    需要登录并且具有角色才能才能进行操作的注解 LoginRequired

    @Target({ElementType.METHOD, ElementType.TYPE})

    @Retention(RetentionPolicy.RUNTIME)

    public @interface LoginRequired {

    // 是否进行校验

    boolean required() default true;

    // 默认管理员

    RoleEnum role() default RoleEnum.ADMIN;

    }

    @Target: 注解的作用目标

    @Target(ElementType.TYPE)—— 接口、类、枚举、注解

    @Target(ElementType.FIELD)—— 字段、枚举的常量

    @Target(ElementType.METHOD)—— 方法

    @Target(ElementType.PARAMETER)—— 方法参数

    @Target(ElementType.CONSTRUCTOR) —— 构造函数

    @Target(ElementType.LOCAL_VARIABLE)—— 局部变量

    @Target(ElementType.ANNOTATION_TYPE)—— 注解

    @Target(ElementType.PACKAGE)—— 包

    @Retention:注解的保留位置

    RetentionPolicy.SOURCE: 这种类型的 Annotations 只在源代码级别保留,编译时就会被忽略,在 class 字节码文件中不包含。

    RetentionPolicy.CLASS: 这种类型的 Annotations 编译时被保留,默认的保留策略,在 class 文件中存在,但 JVM 将会忽略,运行时无法获得。

    RetentionPolicy.RUNTIME: 这种类型的 Annotations 将被 JVM 保留,所以他们能在运行时被 JVM 或其他使用反射机制的代码所读取和使用。

    @Document:说明该注解将被包含在 javadoc 中

    @Inherited:说明子类可以继承父类中的该注解

    简单自定义一个实体类 User, 使用 lombok 简化实体类的编写

    @Data

    @Accessors(chain = true)

    public class AuthUserVO extends BaseVO {

    /**

    * 主键

    */

    private Long id;

    /**

    * 社交账户ID

    */

    private String socialId;

    /**

    * 用户名

    */

    private String name;

    /**

    * 密码

    */

    private String password;

    /**

    * 角色主键 1 普通用户 2 admin

    */

    private Long roleId;

    /**

    * 头像

    */

    private String avatar;

    private String token;

    }

    需要写 token 的生成方法

    /**

    * 生成Token

    * @param authUserVO

    * @return

    */

    public static String getToken(AuthUserVO authUserVO) {

    String sign = authUserVO.getPassword();

    return JWT.create().withExpiresAt(new Date(System.currentTimeMillis()+ Constants.EXPIRE_TIME)).withAudience(JsonUtil.toJsonString(authUserVO.setPassword(null)))

    .sign(Algorithm.HMAC256(sign));

    }

    Algorithm.HMAC256(): 使用 HS256 生成 token, 密钥则是用户的密码,唯一密钥的话可以保存在服务端。

    withAudience() 存入需要保存在 token 的信息,这里我把用户 ID 存入 token 中

    接下来需要写一个拦截器去获取 token 并验证 token

    实现一个拦截器就需要实现 HandlerInterceptor 接口

    HandlerInterceptor 接口主要定义了三个方法

    1.boolean preHandle ():

    预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义 Controller, 返回值为 true 表示继续流程(如调用下一个拦截器或处理器)或者接着执行

    postHandle() 和 afterCompletion();false 表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。

    2.void postHandle():

    后处理回调方法,实现处理器的后处理(DispatcherServlet 进行视图返回渲染之前进行调用),此时我们可以通过 modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView 也可能为 null。

    3.void afterCompletion():

    整个请求处理完毕回调方法,该方法也是需要当前对应的 Interceptor 的 preHandle() 的返回值为 true 时才会执行,也就是在 DispatcherServlet 渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于 try-catch-finally 中的 finally,但仅调用处理器执行链中

    主要流程:

    1. 从 http 请求头中取出 token,

    2. 判断是否映射到方法

    3. 检查是否有 passtoken 注释,有则跳过认证

    4. 检查有没有需要用户登录的注解,有则需要取出并验证

    5. 认证通过则可以访问,不通过会报相关错误信息

    配置拦截器

    在配置类上添加了注解 @Configuration,标明了该类是一个配置类并且会将该类作为一个 SpringBean 添加到 IOC 容器内

    @Configuration

    public class InterceptorConfig extends WebMvcConfigurerAdapter {

    @Override

    public void addInterceptors(InterceptorRegistry registry) {

    registry.addInterceptor(authenticationInterceptor())

    .addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录

    }

    @Bean

    public AuthenticationInterceptor authenticationInterceptor() {

    return new AuthenticationInterceptor();

    }

    }

    WebMvcConfigurerAdapter 该抽象类其实里面没有任何的方法实现,只是空实现了接口

    WebMvcConfigurer 内的全部方法,并没有给出任何的业务逻辑处理,这一点设计恰到好处的让我们不必去实现那些我们不用的方法,都交由 WebMvcConfigurerAdapter 抽象类空实现,如果我们需要针对具体的某一个方法做出逻辑处理,仅仅需要在

    WebMvcConfigurerAdapter 子类中 @Override 对应方法就可以了。

    注:

    在 SpringBoot2.0 及 Spring 5.0 中 WebMvcConfigurerAdapter 已被废弃

    网上有说改为继承 WebMvcConfigurationSupport,不过试了下,还是过期的

    解决方法:

    直接实现 WebMvcConfigurer (官方推荐)

    @Override

    public void addInterceptors(InterceptorRegistry registry) {

    // 可添加多个

    registry.addInterceptor(authenticationInterceptor).addPathPatterns("/**");

    }

    InterceptorRegistry 内的 addInterceptor 需要一个实现 HandlerInterceptor 接口的拦截器实例,addPathPatterns 方法用于设置拦截器的过滤路径规则。

    这里我拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录

    在数据访问接口中加入登录操作注解

    @LoginRequired

    @PutMapping("/admin/v1/update")

    public Result updateUser(@RequestBody AuthUserVO authUserVO) {

    return authUserService.updateUser(authUserVO);

    }

    注销登录

    网上很多案例都很少说怎么退出登录的,有点人说直接生成新的 token,我觉得还是后端控制比较好。这里我存储了每次生成的 token,用了一个定时器去扫描这里的过期 token,每次校验的时候都去数据库中看有没有,如果没有就报 token 验证失败。

    /**

    * 获取用户Session信息

    * @return

    */

    public static UserSessionVO getUserSessionInfo() {

    // 获取请求对象

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

    // 获取请求头Token值

    String token = Optional.ofNullable(request.getHeader(Constants.AUTHENTICATION)).orElse(null);

    if (StringUtils.isBlank(token)) {

    return null;

    }

    // 获取 token 中的 user id

    AuthUser authUser = null;

    try {

    authUser = JsonUtil.parseObject(JWT.decode(token).getAudience().get(0), AuthUser.class);

    } catch (JWTDecodeException j) {

    ExceptionUtil.rollback("token解析失败", ErrorConstants.INVALID_TOKEN);

    }

    AuthUserDao userDao = BeanTool.getBean(AuthUserDao.class);

    AuthUser user = userDao.selectById(authUser.getId());

    if (user == null) {

    ExceptionUtil.rollback("用户不存在,请重新登录", ErrorConstants.LOGIN_ERROR);

    }

    // 验证 token

    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();

    try {

    jwtVerifier.verify(token);

    } catch (JWTVerificationException e) {

    ExceptionUtil.rollback("token验证失败", ErrorConstants.LOGIN_ERROR);

    }

    AuthTokenDao authTokenDao = BeanTool.getBean(AuthTokenDao.class);

    Integer count = authTokenDao.selectCount(new LambdaQueryWrapper().eq(AuthToken::getToken, token).eq(AuthToken::getUserId, user.getId()).ge(AuthToken::

    展开全文
  • JWT官网: [https://jwt.io/][https_jwt.io] JWT(Java版)的github地址:...什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).**定义了一种简洁...

    JWT官网: [https://jwt.io/][https_jwt.io]
    JWT(Java版)的github地址:[https://github.com/jwtk/jjwt][https_github.com_jwtk_jjwt]

    什么是JWT

    Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).**定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。**因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

    JWT请求流程

    图片.png

    1. 用户使用账号和面发出post请求;
    2. 服务器使用私钥创建一个jwt;
    3. 服务器返回这个jwt给浏览器;
    4. 浏览器将该jwt串在请求头中像服务器发送请求;
    5. 服务器验证该jwt;
    6. 返回响应的资源给浏览器。

    JWT的主要应用场景

    身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,**可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)**中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

    优点

    1.简洁(Compact): 可以通过URLPOST参数或者在HTTP header发送,因为数据量小,传输速度也很快
    2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
    3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
    4.不需要在服务端保存会话信息,特别适用于分布式微服务。

    `

    JWT的结构

    JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。
    就像这样:
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

    JWT包含了三部分:
    Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
    Payload 负载 (类似于飞机上承载的物品)
    Signature 签名/签证

    Header

    JWT的头部承载两部分信息:token类型和采用的加密算法。

    { 
      "alg": "HS256",
       "typ": "JWT"
    }
    

    声明类型:这里是jwt
    声明加密的算法:通常直接使用 HMAC SHA256

    加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。
    MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值
    SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
    HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证

    Payload

    载荷就是存放有效信息的地方。
    有效信息包含三个部分
    1.标准中注册的声明
    2.公共的声明
    3.私有的声明

    标准中注册的声明 (建议但不强制使用) :

    iss: jwt签发者
    sub: 面向的用户(jwt所面向的用户)
    aud: 接收jwt的一方
    exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

    公共的声明 :

    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

    私有的声明 :

    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

    Signature

    jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
    header (base64后的)
    payload (base64后的)
    secret

    这个部分需要base64加密后的headerbase64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
    密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。

    下面来进行SpringBoot和JWT的集成

    引入JWT依赖,由于是基于Java,所以需要的是java-jwt

    <dependency>
          <groupId>com.auth0</groupId>
          <artifactId>java-jwt</artifactId>
          <version>3.4.0</version>
    </dependency>
    

    需要自定义一个注解

    需要登录并且具有角色才能才能进行操作的注解LoginRequired

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LoginRequired {
    
        // 是否进行校验
        boolean required() default true;
    
        // 默认管理员
        RoleEnum role() default RoleEnum.ADMIN;  
    }
    
    @Target:注解的作用目标

    @Target(ElementType.TYPE)——接口、类、枚举、注解
    @Target(ElementType.FIELD)——字段、枚举的常量
    @Target(ElementType.METHOD)——方法
    @Target(ElementType.PARAMETER)——方法参数
    @Target(ElementType.CONSTRUCTOR) ——构造函数
    @Target(ElementType.LOCAL_VARIABLE)——局部变量
    @Target(ElementType.ANNOTATION_TYPE)——注解
    @Target(ElementType.PACKAGE)——包

    @Retention:注解的保留位置

    RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
    RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
    RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。

    @Document:说明该注解将被包含在javadoc
    @Inherited:说明子类可以继承父类中的该注解

    简单自定义一个实体类User,使用lombok简化实体类的编写

    @Data
    @Accessors(chain = true)
    public class AuthUserVO extends BaseVO {

    /**
     * 主键
     */
    private Long id;
    
    /**
     * 社交账户ID
     */
    private String socialId;
    
    /**
     * 用户名
     */
    private String name;
    
    /**
     * 密码
     */
    private String password;
    
    /**
     * 角色主键 1 普通用户 2 admin
     */
    private Long roleId;
    
    /**
     * 头像
     */
    private String avatar;
    
    private String token;
    

    }

    需要写token的生成方法

    /**
     * 生成Token
     * @param authUserVO
     * @return
     */
    public static String getToken(AuthUserVO authUserVO) {
        String sign = authUserVO.getPassword();
        return JWT.create().withExpiresAt(new Date(System.currentTimeMillis()+ Constants.EXPIRE_TIME)).withAudience(JsonUtil.toJsonString(authUserVO.setPassword(null)))
                .sign(Algorithm.HMAC256(sign));
    }
    

    Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。
    withAudience()存入需要保存在token的信息,这里我把用户ID存入token

    接下来需要写一个拦截器去获取token并验证token

    实现一个拦截器就需要实现HandlerInterceptor接口

    HandlerInterceptor接口主要定义了三个方法
    1.boolean preHandle ()
    预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行
    postHandle()afterCompletion()false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。

    2.void postHandle()
    后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null

    3.void afterCompletion():
    整个请求处理完毕回调方法,该方法也是需要当前对应的InterceptorpreHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中

    主要流程:

    1.从 http 请求头中取出 token
    2.判断是否映射到方法
    3.检查是否有passtoken注释,有则跳过认证
    4.检查有没有需要用户登录的注解,有则需要取出并验证
    5.认证通过则可以访问,不通过会报相关错误信息

    配置拦截器

    在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内

    @Configuration
    public class InterceptorConfig extends WebMvcConfigurerAdapter {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(authenticationInterceptor())
                    .addPathPatterns("/**");    // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
        }
        @Bean
        public AuthenticationInterceptor authenticationInterceptor() {
            return new AuthenticationInterceptor();
        }
    }
    

    WebMvcConfigurerAdapter该抽象类其实里面没有任何的方法实现,只是空实现了接口
    WebMvcConfigurer内的全部方法,并没有给出任何的业务逻辑处理,这一点设计恰到好处的让我们不必去实现那些我们不用的方法,都交由WebMvcConfigurerAdapter抽象类空实现,如果我们需要针对具体的某一个方法做出逻辑处理,仅仅需要在
    WebMvcConfigurerAdapter子类中@Override对应方法就可以了。

    注:
    SpringBoot2.0Spring 5.0WebMvcConfigurerAdapter已被废弃
    网上有说改为继承WebMvcConfigurationSupport,不过试了下,还是过期的

    解决方法:

    直接实现WebMvcConfigurer (官方推荐)

      @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // 可添加多个
            registry.addInterceptor(authenticationInterceptor).addPathPatterns("/**");
        }
    

    InterceptorRegistry内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。
    这里我拦截所有请求,通过判断是否有@LoginRequired注解 决定是否需要登录

    在数据访问接口中加入登录操作注解

       @LoginRequired
       @PutMapping("/admin/v1/update")
       public Result updateUser(@RequestBody AuthUserVO authUserVO) {
           return authUserService.updateUser(authUserVO);
       }
    

    注销登录

    网上很多案例都很少说怎么退出登录的,有点人说直接生成新的token,我觉得还是后端控制比较好。这里我存储了每次生成的token,用了一个定时器去扫描这里的过期token,每次校验的时候都去数据库中看有没有,如果没有就报token验证失败。

        /**
         * 获取用户Session信息
         * @return
         */
        public static UserSessionVO getUserSessionInfo() {
    
            // 获取请求对象
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    
            // 获取请求头Token值
            String token = Optional.ofNullable(request.getHeader(Constants.AUTHENTICATION)).orElse(null);
    
            if (StringUtils.isBlank(token)) {
                return null;
            }
    
            // 获取 token 中的 user id
            AuthUser authUser = null;
            try {
                authUser = JsonUtil.parseObject(JWT.decode(token).getAudience().get(0), AuthUser.class);
            } catch (JWTDecodeException j) {
                ExceptionUtil.rollback("token解析失败", ErrorConstants.INVALID_TOKEN);
            }
    
            AuthUserDao userDao = BeanTool.getBean(AuthUserDao.class);
            AuthUser user = userDao.selectById(authUser.getId());
            if (user == null) {
                ExceptionUtil.rollback("用户不存在,请重新登录", ErrorConstants.LOGIN_ERROR);
            }
    
            // 验证 token
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
            try {
                jwtVerifier.verify(token);
            } catch (JWTVerificationException e) {
                ExceptionUtil.rollback("token验证失败", ErrorConstants.LOGIN_ERROR);
            }
    
            AuthTokenDao authTokenDao = BeanTool.getBean(AuthTokenDao.class);
            Integer count = authTokenDao.selectCount(new LambdaQueryWrapper<AuthToken>().eq(AuthToken::getToken, token).eq(AuthToken::getUserId, user.getId()).ge(AuthToken::getExpireTime, LocalDateTime.now()));
            if (count.equals(Constants.ZERO)) {
                ExceptionUtil.rollback("token验证失败", ErrorConstants.LOGIN_ERROR);
            }
    
            UserSessionVO userSessionVO = new UserSessionVO();
            userSessionVO.setName(user.getName()).setSocialId(user.getSocialId()).setRoleId(user.getRoleId()).setId(user.getId());
            return userSessionVO;
        }
    

    定时器

       @Override
        public Result logout() {
            UserSessionVO userSessionInfo = SessionUtil.getUserSessionInfo();
            this.authTokenDao.delete(new LambdaQueryWrapper<AuthToken>().eq(AuthToken::getUserId, userSessionInfo.getId()));
            return Result.createWithSuccessMessage();
        }
    

    定时器

        @Scheduled(cron = "0 0/1 * * * ?")
        private void scanToken() {
            log.debug(" {} 扫描过期Token", LocalDateTime.now());
            authTokenDao.delete(new LambdaQueryWrapper<AuthToken>().le(AuthToken::getExpireTime, LocalDateTime.now()));
        }
    

    不加注解的话默认不验证,登录接口一般是不验证的。在getMessage()中我加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问

    注意:这里的key一定不能错,因为在拦截器中是取关键字token的值
    String token = httpServletRequest.getHeader(Constants.AUTHENTICATION);
    加上token之后就可以顺利通过验证和进行接口访问了

    github项目源码地址:https://github.com/byteblogs168/hello-blog

    这是我最近开源的hello-blog,欢迎使用喜欢的给个Star

    注: 本文参考了 https://www.jianshu.com/p/e88d3f8151db。

    展开全文
  • 常见的鉴权方式有两种,一种是基于session,另一种是基于token方式的鉴权,我们来浅谈一下两种 鉴权方式的区别。两种鉴权方式对比session安全性:session是基于cookie进行用户识别的,cookie如果被截获,用户很容易...

    常见的鉴权方式有两种,一种是基于session,另一种是基于token方式的鉴权,我们来浅谈一下两种 鉴权方式的区别。

    两种鉴权方式对比

    session

    1. 安全性:session是基于cookie进行用户识别的,cookie如果被截获,用户很容易受到跨站请求伪造的攻击。
    2. 扩展性:session是有状态的,是具有IP黏贴性和有中心化特性的,在分布式环境下,虽然每台服务器业务逻辑一样,但是session是保存在各个服务器中的,而且每个服务器内存是不共享的,如果使用session去实现分布式部署的话,需要使用其他的一些技术手段去实现,比如spring session,将session保存在第三方服务中,比如redis,这样一旦第三方服务出现问题,整个验权系统就会奔溃,在电商系统及高并发系统中的集群化处理显然是不合适的。
    3. 抗压能力:通常session是存储在内存中的,每个用户通过认证后都会将session存储在服务器内存中,当用户量增大的情况下服务器的压力也随之增大。

    token

    1. 安全性:浏览器会将接收到的token值存储在Local Storage中,(通过js代码写入Local Storage,通过js获取,并不会像cookie一样自动携带)
    2. 扩展性:token是无状态的,是去中心化的,在分布式环境下,各个服务器中的服务只对token进行数据查询,它不需要在服务端保留用户信息或者会话信息,这意味着用户不需要考虑登录的是哪一台服务器,高效的解决了session扩展性的弊端。
    3. 抗压能力:token与session的不同主要在认证成功后,会对当前用户数据进行加密,生成一个加密字符串token,返还给客户端(服务器端并不进行保存)

    基于token的鉴权方式

    业界常用的授权标准有两种,一种是使用auth2,这种方式更适合于类似第三方授权登录,比如微信、微博、QQ信任登录业务。另一种是oauth,即第三方无需知道用户和密码就可以申请获得该资源的授权,更适用于对用户的权限校验并分配访问权限,比如常见的登录后分配可见资源(按钮、菜单等)类型网站。

    Javashop电商系统 采用的是oauth方式的鉴权标准。我们以系统的应用为例来介绍oauth的方案。

    70a5e81fc5829fef42c0424828530fca.png

    1. 登录

    服务端校验密码,成功后返回access_token和refresh_token,客户端记录上述token。

    2. 访问API

    在访问API之前解析access_token,并且查看是否过期,如果不过 期则请求API,如果过期,则要刷新令牌,在请求API。

    3. 刷新token

    携带有效期的refresh_token换回有效token,如果refresh_token过期,则需要用户重新登录。

    4. 注销

    请求注销api,服务器端和客户端应同时删除token的存储。

    38dd1e0ebe44bbd24f7f73afe3cae666.png

    1. 客户端请求API

    携带access_token信息,如果生成环境不会直接携带access_token,会使用加密后的签名校验。祥见以下防重放机制。

    2. 获取token

    根据环境不同而有不同的获取token方式。

    3. 解析token

    通过JWT工具将token解析。

    4. 由redis读取token

    根据uid拼接key读取access_token, 如果不存在这个用户的token说明已经登出。

    5. 验证token

    判断次token是否属于此uid,判断token是否过期,如果过期则进行以下刷新token的流程。

    6. 注入权限

    如果token验证成功,根据user信息生成权限注入到spring安全上下文中。

    刷新token流程

    54d195f1ee38903c3773d864b67cabd9.png

    1. 客户端请求API

    携带refresh_token,如果是生产环境不会直接携带refresh_token信息,详见以下防重放攻击。

    2. 获取token

    根据环境不同而有不同的获取token方式。

    3. 解析token

    通过JWT工具将token解析。

    4. token读取

    根据uid拼接key读取出access_token,如果不存在这个用户的token说明用户已经登出。

    5. 验证token

    判断此token是否属于此uid,判断token是否已经过期,如果过期,则返回refresh_token过期错误,此时用户需要重新登录。

    6. 刷新token

    如果refresh_token 验证成功,则重新生成access_token和refresh_token,上述有效期以当前时间向后计算,替换此用户在redis中的token,并将token返回给客户端。

    防重放机制

    dd9e814e2a567ab8c4a23e4fa72dbc6e.png

    一、 参数的读取

    1. 在生产环境时,不能直接传递token,而是要传递签名数据,服务器端验签后由Redis中获取签名。

    2. 如果是非生产环境,直接由header中读取token。

    二、 生产环境传递如下参数

    memberid (用户id)

    nonce(随机字串,6位)

    timestamp(当前时间戳,到秒)

    sign= md5( uid+ nonce + timestamp +token )

    三、 验证逻辑

    1. 验证时间戳

    判断时间戳是否起过60s,大于60s则判别为重放功击。

    2. 验证nonce

    首先验证nonce在 reids中是否存在,如果存在,则判别为重放功击,否则将nonce记录在redis中(key为:"nonce"+uid+"_"+nonce),失效时间为60s。

    3. 验证sign

    md5( uid+ nonce + timestamp +token ) 验证是签名是否通过。

    4. 验证token

    通过uid拿到token ,验证逻辑同验权流程。

    当然在不同的业务场景下实现方案是多种多样的,仅以此方案抛转引玉,供大家参考。

    fdba3bc26509d7c0a902aab412726a3c.png
    展开全文
  • 业务场景在前后分离场景下,越来越多的项目使用token作为接口的安全机制,APP端或者WEB端(使用VUE、REACTJS等构建)使用token与后端接口交互,以达到安全的目的。本文结合stackover以及本身项目实践,试图总结出一个...

    业务场景

    在前后分离场景下,越来越多的项目使用token作为接口的安全机制,APP端或者WEB端(使用VUE、REACTJS等构建)使用token与后端接口交互,以达到安全的目的。本文结合stackover以及本身项目实践,试图总结出一个通用的,可落地的方案。

    基本思路

    单个token

    token(A)过期设置为15分钟

    前端发起请求,后端验证token(A)是否过期;如果过期,前端发起刷新token请求,后端设置已再次授权标记为true,请求成功

    前端发起请求,后端验证再次授权标记,如果已经再次授权,则拒绝刷新token的请求,请求成功

    如果前端每隔72小时,必须重新登录,后端检查用户最后一次登录日期,如超过72小时,则拒绝刷新token的请求,请求失败

    授权token加上刷新token

    用户仅登录一次,用户改变密码,则废除token,重新登录

    1.0实现

    1.登录成功,返回access_token和refresh_token,客户端缓存此两种token;

    2.使用access_token请求接口资源,成功则调用成功;如果token超时,客户端

    携带refresh_token调用中间件接口获取新的access_token;

    3.中间件接受刷新token的请求后,检查refresh_token是否过期。

    如过期,拒绝刷新,客户端收到该状态后,跳转到登录页;

    如未过期,生成新的access_token和refresh_token并返回给客户端(如有可能,让旧的refresh_token失效),客户端携带新的access_token重新调用上面的资源接口。

    4.客户端退出登录或修改密码后,调用中间件注销旧的token(使access_token和refresh_token失效),同时清空客户端的access_token和refresh_toke。

    后端表

    id user_id client_id client_secret refresh_token expire_in create_date del_flag

    2.0实现

    场景: access_token访问资源 refresh_token授权访问 设置固定时间X必须重新登录

    1.登录成功,后台jwt生成access_token(jwt有效期30分钟)和refresh_token(jwt有效期15天),并缓存到redis(hash-key为token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全部token,也可以根据sub-key,废除部分设备的token。),设置过期时间为1个月,保证最终所有token都能删除),返回后,客户端缓存此两种token;

    2.使用access_token请求接口资源,校验成功且redis中存在该access_token(未废除)则调用成功;如果token超时,中间件删除access_token(废除);客户端再次携带refresh_token调用中间件接口获取新的access_token;

    3.中间件接受刷新token的请求后,检查refresh_token是否过期。

    如过期,拒绝刷新,删除refresh_token(废除); 客户端收到该状态后,跳转到登录页;

    如未过期,检查缓存中是否有refresh_token(是否被废除),如果有,则生成新的access_token并返回给客户端,客户端接着携带新的access_token重新调用上面的资源接口。

    4.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token和refresh_token(废除)),同时清空客户端侧的access_token和refresh_toke。

    5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。

    6.以上3刷新access_token可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:失效,长时间未登录,频繁刷新)

    2.0 变动

    1.登录

    2.登录拦截器

    3.增加刷新access_token接口

    4.退出登录

    5.修改密码

    3.0实现

    场景:自动续期 长时间未使用需重新登录

    1.登录成功,后台jwt生成access_token(jwt有效期30分钟),并缓存到redis(hash-key为access_token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全部token),设置access_token过期时间为7天,保证最终所有token都能删除),返回后,客户端缓存此token;

    2.使用access_token请求接口资源,校验成功且redis中存在该access_token(未废除)则调用成功;如果token超时,中间件删除access_token(废除),同时生成新的access_token并返回。客户端收到新的access_token,

    再次请求接口资源。

    3.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token(废除)),同时清空客户端侧的access_token。

    4.以上2 可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:长时间未登录,频繁刷新)

    5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。

    3.0 变动

    1.登录

    2.登录拦截器

    3.退出登录

    4.修改密码

    1.3 场景:token过期重新登录 长时间未使用需重新登录

    1.登录成功,后台jwt生成access_token(jwt有效期7天),并缓存到redis,key为 "user_id:access_token",value为access_token(根据用户id,可以人工废除指定用户全部token),设置缓存过期时间为7天,保证最终所有token都能删除,请求返回后,客户端缓存此access_token;

    2.使用access_token请求接口资源,校验成功且redis中存在该access_token(未废除)则调用成功;如果token超时,中间件删除access_token(废除),同时生成新的access_token并返回。客户端收到新的access_token,

    再次请求接口资源。

    3.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token(废除)),同时清空客户端侧的access_token。

    4.以上2 可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:长时间未登录,频繁刷新)

    5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。

    1.3 变动

    1.登录

    2.登录拦截器

    3.退出登录

    4.修改密码

    解决方案

    2.0 场景: access_token访问资源 refresh_token授权访问 设置固定时间X必须重新登录

    1.登录成功,后台jwt生成access_token(jwt有效期30分钟)和refresh_token(jwt有效期15天),并缓

    存到redis(hash-key为token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全

    部token,也可以根据sub-key,废除部分设备的token。),设置过期时间为1个月,保证最终所有token都

    能删除),返回后,客户端缓存此两种token;

    2.使用access_token请求接口资源,校验成功且redis中存在该access_token(未废除)则调用成功;如果

    token超时,中间件删除access_token(废除);客户端再次携带refresh_token调用中间件接口获取新的

    access_token;

    3.中间件接受刷新token的请求后,检查refresh_token是否过期。

    如过期,拒绝刷新,删除refresh_token(废除); 客户端收到该状态后,跳转到登录页;

    如未过期,检查缓存中是否有refresh_token(是否被废除),如果有,则生成新的access_token并返回给

    客户端,客户端接着携带新的access_token重新调用上面的资源接口。

    4.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token和refresh_token(

    废除)),同时清空客户端侧的access_token和refresh_toke。

    5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。

    6.以上3刷新access_token可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(

    拒绝的场景:失效,长时间未登录,频繁刷新)

    2.0 变动

    1.登录

    2.登录拦截器

    3.增加刷新access_token接口

    4.退出登录

    5.修改密码

    3.0 场景:自动续期 长时间未使用需重新登录

    1.登录成功,后台jwt生成access_token(jwt有效期30分钟),并缓存到redis(hash-key为

    access_token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全部token,也可以

    根据sub-key,废除部分设备的token。),设置access_token过期时间为1个月,保证最终所有token都能删

    除),返回后,客户端缓存此token;

    2.使用access_token请求接口资源,校验成功且redis中存在该access_token(未废除)则调用成功;如果

    token超时,中间件删除access_token(废除),同时生成新的access_token并返回。客户端收到新的

    access_token,

    再次请求接口资源。

    3.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token(废除)),同时清

    空客户端侧的access_token。

    4.以上2 可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:长

    时间未登录,频繁刷新)

    5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。

    3.0 变动

    1.登录

    2.登录拦截器

    3.退出登录

    4.修改密码

    4.0 场景:token过期重新登录 长时间未使用需重新登录

    1.登录成功,后台jwt生成access_token(jwt有效期7天),并缓存到redis,key为

    "user_id:access_token" + 用户id,value为access_token(根据用户id,可以人工废除指定用户全部

    token),设置缓存过期时间为7天,保证最终所有token都能删除,请求返回后,客户端缓存此

    access_token;

    2.使用access_token请求接口资源,校验成功且redis中存在该access_token(未废除)则调用成功;如果

    token超时,中间件删除access_token(废除),同时生成新的access_token并返回。客户端收到新的

    access_token,

    再次请求接口资源。

    3.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token(废除)),同时清

    空客户端侧的access_token。

    4.以上2 可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:长

    时间未登录,频繁刷新)

    5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。

    4.0 变动

    1.登录

    2.登录拦截器

    3.退出登录

    4.修改密码

    最终实现

    后端

    在登录接口中 如果校验账号密码成功 则根据用户id和用户类型创建jwt token(有效期设置为-1,即永不过期),得到A

    更新登录日期(当前时间new Date()即可)(业务上可选),得到B

    在redis中缓存key为ACCESS_TOKEN:userId:A(加上A是为了防止用户多个客户端登录 造成token覆盖),value为B的毫秒数(转换成字符串类型),过期时间为7天(7 * 24 * 60 * 60)

    在登录结果中返回json格式为{"result":"success","token": A}

    用户在接口请求header中携带token进行登录,后端在所有接口前置拦截器进行拦截,作用是解析token 拿到userId和用户类型(用户调用业务接口只需要传token即可), 如果解析失败(抛出SignatureException),则返回json(code = 0 ,info= Token验证不通过, errorCode = '1001'); 此外如果解析成功,验证redis中key为ACCESS_TOKEN:userId:A 是否存在 如果不存在 则返回json(code = 0 ,info= 会话过期请重新登录, errorCode = '1002'); 如果缓存key存在,则自动续7天超时时间(value不变),实现频繁登录用户免登陆。

    把userId和用户类型放入request参数中 接口方法中可以直接拿到登录用户信息

    如果是修改密码或退出登录 则废除access_tokens(删除key)

    前端(VUE)

    用户登录成功,则把username存入cookie中,key为loginUser;把token存入cookie中,key为accessToken 把token存入Vuex全局状态中

    进入首页

    展开全文
  • token在客户端与服务器端的交互流程Token的优点和思考参考代码:核心代码使用参考,不是全部代码JWT token的组成头部(Header),格式如下:{“typ”: “JWT”,“alg”: “HS256”}由上可知,该token使用HS256加密算法...
  • JWT简介 :json Web Token(缩写JWT)是目前最流行的跨域认证解决方案session登录的认证方案是看,用户从客户端传递用户名和密码登录信息,服务端认证后将信息储存在session中,将session_id放入cookie中,以后访问...
  • 退出登录时,如果不使JWT Token失效会产生如下2个问题问题1-未过期的token还是可以用要是用户在多个设备登录了,而且本地保存了token。当一个地方丢弃token,但是这个token要是没有过期,那之前token还是可以用的。...
  • I am using jwt plugin and strategy in hapijs.I am able to create jwt token while login user and authenticate other API using the same token through 'jwt' strategy.I am setting the token in request.sta...
  • 我最近也在做这个一个解决方案是oauth2 中把JwtTokenStore 改成RedisTokenStore ,然后登出的时候清redis另外还是借用的redis用户每次登录生成token,我还是在redis中把这个token存储登出的时候清除redis中存储的...
  • 1.JWT简介JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。2 JSON Web Token的应用...
  • JWT(JSON Web Token)是目前最流行的认证方案之一。博客园、各种技术公众号隔三差五就会推一篇JWT相关的文章,真的多如牛毛。但我对JWT有点困惑,今天写出来跟大家探讨探讨,不要喷哈。JWT原理本文默认读者已经对...
  • #helps.py中 class LoginRequiredAuthentication(BaseAuthentication): def authenticate(self, request): token = request.META.get('HTTP_TOKEN') #请求头中拿token if token: try: payload = jwt.decod
  • 什么是 JWT概念JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。JWT 原理JWT 组成JWT 由三部分组成:Header,Payload,Signature 三个部分组成,并且最后由.拼接而成。HeaderHeader 部分是一个 JSON 对象,...
  • 什么是 JWT概念JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。JWT 原理JWT 组成JWT 由三部分组成:Header,Payload,Signature 三个部分组成,并且最后由.拼接而成。HeaderHeader 部分是一个 JSON 对象,...
  • 什么是 JWT概念JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。JWT 原理JWT 组成JWT 由三部分组成:Header,Payload,Signature 三个部分组成,并且最后由.拼接而成。HeaderHeader 部分是一个 JSON 对象,...
  • 流程1.使用微信小程序登录和获取用户信息Api接口2.把Api获取的用户资料和code发送给django后端3....后端通过JSON web token方式登录,把token和用户id传回小程序6.小程序将token和用户id保存在storag...
  • JWT(JSON WEB TOKEN)的特点是无状态,通常用来作为验证登录以及鉴权,在这一方面,JWT体现出了他的优点。然而,如果使用JWT实现,用户主动注销、强制登出(禁止登陆)、忘记密码、修改密码,JWT续签等方面的需求,就会让...
  • 为了支持注销,我的解决方案是在注销时将该token加入到服务器的redis黑名单中。 JWT与OAuth的区别 这两个概念总有人用混淆,所以一起介绍了。 OAuth2是一种授权框架,用在使用第三方账号登录的情况(比如使用weibo, ...
  • A User 将会使用以下功能 注册并创建一个新帐户 登录到他们的帐户 注销和丢弃 token 并离开应用程序 获取登录用户的详细信息 检索可供用户使用的产品列表 按ID查找特定产品 将新产品添加到用户产品列表中 编辑...
  • 注销问题,由于服务端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。 密码重置,密码重置后,...
  • ddefewfewf:留着也没用啊Molita:不删留着干啥嘞就是 post session 和 delete ... 参考黑名单的实现.## 登出的做法就是, 客户端把本地的 token 删除, 这样子就不存在这个 token, 服务端也不关心这个. 就证明这...

空空如也

空空如也

1 2 3 4 5 ... 14
收藏数 277
精华内容 110
关键字:

token注销