乐趣区

关于springboot:一文搞懂Session和JWT登录认证

前言

目前在开发的小组结课我的项目中用到了 JWT 认证,简略分享一下,并看看与 Session 认证的异同。

登录认证(Authentication)的概念非常简单,就是通过肯定伎俩对用户的身份进行确认。

咱们都晓得 HTTP 是 无状态 的,服务器接管的每一次申请,对它来说都是“新来的”,并不知道客户端来过。

举个例子:
客户端 A: 我是 A, 给我一瓶水。
服务端 B:好,给你。
客户端 A:再给我来个面包。
服务端 B: 啥,你是谁?

即每一个申请对服务器来说都是新的。

咱们不可能每次操作都让用户输出用户名和明码,那么咱们如何让服务器记住咱们登录过了呢?

那就是 凭证。即每次申请都给服务器一个凭证通知服务器我是谁。

当初个别应用比拟多的认证形式有四种:

  • Session
  • Token
  • SSO 单点登录
  • OAtuth 登录

上面就来说说比拟罕用的前两种。

Session

Cookie + Session

最常见的就是 Cookie + Session 认证。

Session,是一种有状态的会话管理机制,其目标就是为了解决 HTTP 无状态申请带来的问题。

当用户登录认证申请通过时,服务端会将用户的信息存储起来,并生成一个 SessionId 发送给前端,前端将这个 SessionId 保存起来。之后前端再发送申请时都携带 SessionId,服务器端再依据这个 SessionId 来查看该用户有没有登录过。

这个 SessionId,个别是保留在 Cookie 中。

如果用户第一次拜访某个服务器时,服务器响应数据时会在响应头的 Set-Cookie 标识里将 Session Id 返回给浏览器,浏览器就将标识中的数据存在 Cookie 中。

上面咱们来简略写个 demo 测试一下:

初始化一个 spring boot 我的项目,并且代码如下:

demo

咱们只须要在用户登录的时候将用户信息存在 HttpSession 中

@RestController
public class UserController {@PostMapping("login")
    public String login(@RequestBody User user, HttpSession session) {if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
            // 登录胜利 写入 Session
            session.setAttribute("sessionId", user);
            return "login success";
        }
        return "username or password incorrect";
    }

    @GetMapping("/logout")
    public String logout(HttpSession session) {
        // 登记 删除 Session
        session.removeAttribute("sessionId");
        return "logout success";
    }

    public String api(HttpSession session) {
        // 用户操作 判断是否登录
        User user = (User) session.getAttribute("sessionId");
        if (user == null) {return "please login";}
        return "return data";
    }
}

上面咱们向 login 地址 申请,并查看响应。能够看到,用户登录时服务端会返回 Set-Cookie 字段。这些工作 Servlet 帮咱们做好了

上面咱们向 api 地址申请。能够看到,后续拜访服务端主动就会携带 Cookie:

服务端认证身份胜利,返回数据。


Session + Header 认证

以后开发的几个我的项目都是采纳这种模式。

将 Session 会话放进 申请头中作为认证信息。

上面简略写个 demo 并用 postman 测试

demo

pom.xml:


        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session</artifactId>
            <version>1.3.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

HeaderAndParamHttpSessionStrategy:

将 cookie 认证改为 Header 认证,申请关键字为 x-auth-token

/**
 * Header 或是申请参数中的带有 token 的认证策略
 * */
public class HeaderAndParamHttpSessionStrategy extends HeaderHttpSessionStrategy {
  /**
   * header 认证关键字名称
   */
  private String headerName = "x-auth-token";

  @Override
  public String getRequestedSessionId(HttpServletRequest request) {String token = request.getHeader(this.headerName);
    return (token != null && !token.isEmpty()) ? token : request.getParameter(this.headerName);
  }
}

MvcSecurityConfig:
应用 spring 提供的 MapSessionRepository 来帮忙咱们治理 Session

@Configuration
@EnableWebSecurity
@EnableSpringHttpSession
public class MvcSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeRequests()
                // 凋谢端口
                .antMatchers("**").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and().cors()
                .and().csrf().disable();
        http.headers().frameOptions().disable();
        return http.build();}

    /**
     * 应用 header 认证来替换默认的 cookie 认证
     */
    @Bean
    public HttpSessionStrategy httpSessionStrategy() {return new HeaderAndParamHttpSessionStrategy();
    }


    /**
     * 因为咱们启用了 @EnableSpringHttpSession 后,而非 RedisHttpSession.
     * 所以应该为 SessionRepository 提供一个实现。* 而 Spring 中默认给了一个 SessionRepository 的实现 MapSessionRepository.
     *
     * @return session 策略
     */
    @Bean
    public SessionRepository sessionRepository() {return new MapSessionRepository();
    }
}

重新启动我的项目:

尝试 1 :向 login 地址申请:

能够看到,服务端的响应头上带有了 x-auth-token, 这种将 session 放在申请头部的认证就是 header 认证。

后果:响应头返回 x-auth-token 字段


尝试 2 :申请头不带 x-auth-token,向服务端申请 api

而这时候,如果客户端发送接下来的申请的时候,申请头不带上服务端返回的 x-auth-token,那么是 无奈失去认证 的。

后果:认证失败

尝试 3 :申请头带 x-auth-token,向服务端申请 api

将登录胜利之后,服务端返回

后果:认证胜利

通过以上的形式:咱们胜利将 Cookie + Session 认证,替换为了 Header + Session 认证。

那么换之后,有什么益处呢?

Header + Session 相较 Cookie + Session 有几点 益处

  • 避免跨站脚本攻打(XSS):应用 Cookie 存储会话 ID 的话,Cookie 是通过浏览器主动治理的,容易受到 XSS 攻打的影响。而将会话 ID 存储在头部,能够防止这种攻打。
  • 防止 CSRF 攻打:应用 Cookie 存储会话 ID 的话,攻击者能够利用 CSRF 攻打来获取 Cookie 中的会话 ID,从而伪造用户申请。将会话 ID 存储在头部的话,能够防止这种攻打。
  • 不受第三方 Cookie 反对的限度:如果用户的浏览器禁用了第三方 Cookie,那么应用 Cookie + Session 的形式就无奈应用。而将会话 ID 存储在头部,不须要应用 Cookie,不受这个限度。

毛病:

  • 会话 ID 存储在头部,可能被重放攻打利用
  • 执行性能代价较高:因为 HTTP 头比 Cookie 更大,因而将会话 ID 存储在头部通常会占用更多的网络资源,减少传输提早。

因而,应该依据具体的利用场景、协定、需要和平安要求来抉择适合的身份认证形式。

Token 认证

除了 Session 之外,目前比拟风行的做法就是应用 JWT(JSON Web Token)。

JWT 具备以下俩种个性:

  • 能够将一段数据加密成一段字符串,也能够从这字符串解密回数据
  • 能够对这个字符串进行校验,比方有没有过期,有没有被篡改

看到这,这不和 Session + Header 认证一样嘛!就是把 SessionId 换成了 JWT 字符串而已,有必要么?🤔

Session 和 JWT 有一个重要的区别,就是 Session 是有状态的,JWT 是无状态的。

即,Session 在服务端保留了用户信息,而 JWT 在服务端没有保留任何信息。

以后端携带 Session Id 到服务端时,服务端要查看其对应的 HttpSession 中有没有保留用户信息,保留了就代表登录了。

当应用 JWT 时,服务端只须要对这个字符串进行校验,校验通过就代表登录了。

上面持续从一个 Demo 体验:

demo

pom.xml:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

写一个工具类

public interface CommonService {
  /**
   * 签名秘钥
   */
  String SECRET = "shareMusic";

  // 依据用户 id 生成 token
  static String createJwtToken(Long id) {
    long ttlMillis = -1; // 示意不增加过期工夫
    return createJwtToken(id.toString(), ttlMillis);
  }

  /**
   * 生成 Token
   *
   * @param id        编号
   * @param ttlMillis 签发工夫(无效工夫,过期会报错)* @return token String
   */
  static String createJwtToken(String id, long ttlMillis) {

    // 签名算法,将对 token 进行签名
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 生成签发工夫
    long nowMillis = System.currentTimeMillis();
    Date now = new Date(nowMillis);

    // 通过秘钥签名 JWT
    byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
    Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

    // Let's set the JWT Claims
    JwtBuilder builder = Jwts.builder().setId(id)
        .setIssuedAt(now)
        .signWith(signatureAlgorithm, signingKey);

    // 如果指定了过期工夫,则增加
    if (ttlMillis >= 0) {
      long expMillis = nowMillis + ttlMillis;
      Date exp = new Date(expMillis);
      builder.setExpiration(exp);
    }

    return builder.compact();}

    // 验证并解析 JWT
    static Claims parseJWT(String jwt) { // 如果是空字符串间接返回 null
        if (jwt == null ||jwt.isEmpty()) {return null;}
        // 这个 Claims 对象蕴含了许多属性,比方签发工夫、过期工夫以及寄存的数据等
        Claims claims = null;
        // 解析失败了会抛出异样,所以咱们要捕获一下。token 过期、token 非法都会导致解析失败
        try {claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
                    .parseClaimsJws(jwt).getBody();} catch (JwtException e) {System.err.println("解析失败!");
        }
        return claims;
    }
}

同时改写一下咱们的 Controller:
登录胜利返回

    @PostMapping("login")
    public String login(@RequestBody User user, HttpServletResponse response) {if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
            // 判断胜利 返回头退出 token
            String token = CommonService.createJwtToken(user.getUsername());
            response.setHeader("Authorization", token);
            return "login success";
        }
        return "username or password incorrect";
    }

    @GetMapping("/api")
    public String api(HttpServletRequest request) {String jwt = request.getHeader("Authorization");
        // 解析失败就提醒用户登录
        if (CommonService.parseJWT(jwt) == null) {return "please login";}
        return "return data";
    }

重启服务开始测试。

尝试 1 :向 login 地址申请.

能够看到,咱们胜利地在服务端响应头中返回了 JWT,它是以 Authorization 作为名字的字段。

尝试 2 :不带 JWT,向 api 地址申请.

认证失败。

尝试 3 :带 JWT,向 api 地址申请.

后果:认证胜利,返回数据。


下面咱们胜利应用 JWT 实现了登录和申请 api。上面简略看一下它的原理:

JWT 原理:

JWT 通常由三局部组成:Header、Payload 和 Signature。

  • Header:蕴含 Token 类型(即 JWT)和所应用的签名算法信息(如 HS256)。
  • Payload:存储了一些形容信息,如 Token 的颁发者、过期工夫、拜访权限等,也能够蕴含一些用户的自定义数据。
  • Signature:由服务器端生成,用于验证 Token 的正确性和完整性。

咱们将方才获取到的 JWT 去在线网站解密一下:
能够看到,获取到了咱们传输的信息: 用户名

在线网站将 Header 和 Payload 中的 Base64 编码信息通过简略的算法将其还原成原始的明文数据

能够看到签名是无奈解密的,这是因为 JWT 的签名次要是用于保障 JWT 的完整性 和避免 JWT 被篡改 或伪造

signature 能够抉择对称加密算法或者非对称加密算法,罕用的就是 HS256、RS256。

具体 JWt 解析过程能够看这篇文章:https://www.freecodecamp.org/chinese/news/how-to-sign-and-val…

JWT 登记

你可能会留意到,下面的 JWT 办法没有登记的性能。那么如何登记?🤔

事实上,JWT 是无状态的认证形式,因而它自身并不提供登记的机制。让咱们从后盾登记 token,这对于 jwt 来说并不是那么简略,并不能像删除 session 那样来删除 token。

JWT 的目标与 session 不同,不可能强制删除或生效曾经生成的 token。

咱们能够采纳上面两种形式:

Token 过期⏰。

通过过期工夫机制。能够在 生成 JWT Token 时设置一个过期工夫,一旦 Token 过期后则视为有效。通过这种形式,能够保障 Token 在肯定工夫内无效,同时也防止了 Token 滥用和被盗用的危险。

我还是想登记

如果我有一个严格的登记性能,无奈期待 Token 主动过期怎么办?🕵️‍

那么存储一个所谓的“名单”,判断 Token 是无效的。个别能够采纳 Redis,

校验时,查看提供的 token 在 redis 中是否无效,如何有效的话就让用户去登录。

从这个方面也体现出了 JWt 更适宜分布式构造。

Session 和 JWT

两者的不同

存储地位:Session 信息是存储在服务端的,而 JWT 将认证信息存储在客户端的 Token 中。

是否须要状态:Session 基于状态来保护会话,如果会话状态失落或者被篡改,服务器将会从新初始化会话。而 JWT 身份认证机制是无状态的,每个申请均蕴含足够的信息,服务器不须要维持任何状态。这一点使得 JWT 身份认证机制特地适宜于分布式系统。

安全性:Session 是基于某种算法生成的 Session ID 来保护用户状态的,如果 Session ID 被窃取或者伪造,会话会受到攻打,凭证会生效。而 JWT 通过签名来避免伪造和篡改,只有在通过验证后能力应用。

扩展性:Session 计划个别实用于繁多的服务或者单个利用,而 JWT 身份认证机制实用于跨域、分布式服务调用等多场景。

两种形式都能够实现登录认证,至于具体选型就依据本人理论业务需要来了。

参考文章:
https://www.cnblogs.com/RudeCrab/p/14251154.html#%E6%94%B6%E5…
https://segmentfault.com/a/1190000041216780
https://cloud.tencent.com/developer/news/837117

退出移动版