JWT认识与实践

669 阅读5分钟

认识JWT

JSON Web Tokens - jwt.io

JWT(JSON Web Token)是一种用于在网络上安全传输信息的开放标准。它由三个部分组成:头部载荷签名。头部包含加密算法令牌类型等信息,载荷包含用户信息和其他元数据,签名则通过使用密钥对头部和载荷进行加密来验证令牌的真实性和完整性。JWT 可以被用于身份验证授权,因为它可以帮助验证请求是否来自可信的源,并且可以将用户信息权限信息嵌入到令牌中,从而避免了每次请求都需要进行数据库查询的情况。

下面是一个JWT样例,头部、载荷、签名都用.进行分隔

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

下面是对于一个JWT的解析过程:

  1. 将JWT字符串按照点号(.)分成三个部分:头部载荷签名
  2. 解码头部,得到加密算法令牌类型等信息。
  3. 解码载荷,得到JWT中存储的信息
  4. 验证签名,确保JWT没有被篡改过。具体验证方式取决于使用的加密算法。
  5. 如果验证成功,则可以信任JWT中的信息。

需要注意的是,JWT只是一种基于文本的令牌,因此它不提供加密功能,只提供了签名功能。如果需要加密数据,可以将JWT作为一个整体进行加密

使用JWT

以下是一些JWT的具体使用例子:

  1. 身份验证:当用户成功登录时,服务器可以生成一个JWT并将其返回给客户端。客户端可以在后续请求中将该JWT作为身份验证凭据发送到服务器。服务器可以验证JWT的签名并确定用户是否有权访问所请求的资源。
  2. 单点登录:当用户成功登录到一个应用程序时,服务器可以生成一个JWT并将其返回给客户端。客户端可以在后续请求中将该JWT作为身份验证凭据发送到其他应用程序。其他应用程序可以验证JWT的签名并确定用户是否有权访问所请求的资源。
  3. 授权:当用户请求访问某个受保护的资源时,服务器可以检查JWT中包含的声明以确定用户是否有权访问该资源。例如,服务器可以检查JWT中是否包含特定的角色或权限声明。
  4. 信息交换:两个服务之间可以使用JWT来安全地交换信息。一个服务可以生成一个JWT并将其发送到另一个服务。接收方可以验证JWT的签名并提取其中包含的信息。
  5. 重置密码:当用户请求重置密码时,服务器可以生成一个包含重置令牌的JWT并将其发送到用户的电子邮件地址。用户可以使用该令牌来验证其身份并设置新密码。

以下是基于Vue+SpringBoot的一个简单例子:

 // 导入所需的包
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
 import org.springframework.web.bind.annotation.*;
 ​
 import java.util.Date;
 ​
 @RestController
 @RequestMapping("/api")
 public class AuthController {
 ​
     // 用户登录接口
     @PostMapping("/login")
     public String login(@RequestBody User user) {
         // 验证用户身份
         if (authenticate(user)) {
             // 生成JWT令牌
             String token = Jwts.builder()
                     .setSubject(user.getUsername())
                     .setExpiration(new Date(System.currentTimeMillis() + 3600000))
                     .signWith(SignatureAlgorithm.HS512, "secret")
                     .compact();
             return token;
         } else {
             return "Invalid credentials";
         }
     }
 ​
     // 单点登录接口
     @PostMapping("/sso")
     public String sso(@RequestHeader("Authorization") String token) {
         // 验证JWT令牌
         if (validate(token)) {
             return "Success";
         } else {
             return "Unauthorized";
         }
     }
 ​
     // 授权接口
     @GetMapping("/protected")
     public String protectedResource(@RequestHeader("Authorization") String token) {
         // 验证JWT令牌中是否包含特定的角色或权限声明
         Claims claims = Jwts.parser()
                 .setSigningKey("secret")
                 .parseClaimsJws(token.replace("Bearer ", ""))
                 .getBody();
         if (claims.get("role").equals("admin")) {
             return "Access granted";
         } else {
             return "Access denied";
         }
     }
 ​
     // 信息交换接口
     @PostMapping("/exchange")
     public String exchange(@RequestBody String data, @RequestHeader("Authorization") String token) {
         // 验证JWT令牌
         if (validate(token)) {
             // 处理数据并返回结果
             return "Processed data: " + data;
         } else {
             return "Unauthorized";
         }
     }
 ​
     // 重置密码接口
     @PostMapping("/reset-password")
     public String resetPassword(@RequestBody User user) {
         // 生成包含重置令牌的JWT令牌并发送到用户的电子邮件地址
         String token = Jwts.builder()
                 .setSubject(user.getUsername())
                 .setExpiration(new Date(System.currentTimeMillis() + 600000))
                 .claim("reset", true)
                 .signWith(SignatureAlgorithm.HS512, "secret")
                 .compact();
         // 发送电子邮件
         sendEmail(user.getEmail(), token);
         return "Email sent";
     }
 ​
     // 验证用户身份
     private boolean authenticate(User user) {
         // TODO: 实现用户身份验证逻辑
         return true;
     }
 ​
     // 验证JWT令牌
     private boolean validate(String token) {
         try {
             Jwts.parser().setSigningKey("secret").parseClaimsJws(token.replace("Bearer ", ""));
             return true;
         } catch (Exception e) {
             return false;
         }
     }
 ​
     // 发送电子邮件
     private void sendEmail(String email, String token) {
         // TODO: 实现发送电子邮件逻辑
     }
 ​
 }

下面的代码是一个Vue.js组件,提供了各种身份验证和授权相关操作的用户界面。该组件包含一个用户登录表单,该表单使用用户的凭据向服务器发送POST请求。如果登录成功,服务器将响应一个JWT(JSON Web Token),该JWT存储在组件的token属性中。sso方法发送一个POST请求到服务器,以使用Authorization头中的JWT启动单点登录过程。protectedResource方法发送一个GET请求以访问受保护的资源,再次使用Authorization头中的JWT。exchangeData方法使用表单输入的数据和Authorization头中的JWT发送一个POST请求以与服务器交换数据。最后,resetPassword方法使用表单中输入的电子邮件发送一个POST请求以重置用户的密码。该组件使用Axios库向服务器发出HTTP请求。该库提供了一个简单和一致的API来发出HTTP请求,并支持拦截器来处理请求和响应。

 <template>
   <div>
     <h2>Login</h2>
     <form @submit.prevent="login">
       <div>
         <label for="username">Username:</label>
         <input type="text" id="username" v-model="user.username" required>
       </div>
       <div>
         <label for="password">Password:</label>
         <input type="password" id="password" v-model="user.password" required>
       </div>
       <button type="submit">Login</button>
     </form>
     <hr>
     <h2>Single Sign-On</h2>
     <button @click="sso">SSO</button>
     <hr>
     <h2>Protected Resource</h2>
     <button @click="protectedResource">Access Protected Resource</button>
     <hr>
     <h2>Exchange Data</h2>
     <form @submit.prevent="exchangeData">
       <label for="data">Data:</label>
       <input type="text" id="data" v-model="data">
       <button type="submit">Exchange Data</button>
     </form>
     <hr>
     <h2>Reset Password</h2>
     <form @submit.prevent="resetPassword">
       <div>
         <label for="email">Email:</label>
         <input type="email" id="email" v-model="user.email" required>
       </div>
       <button type="submit">Reset Password</button>
     </form>
   </div>
 </template>
 ​
 <script>
 import axios from 'axios';
 ​
 export default {
   name: 'App',
   data() {
     return {
       user: {
         username: '',
         password: '',
         email: '',
       },
       token: '',
       data: '',
     };
   },
   methods: {
     login() {
       axios.post('/api/login', this.user)
         .then(response => {
           this.token = response.data;
           console.log(this.token);
         })
         .catch(error => {
           console.error(error);
         });
     },
     sso() {
       axios.post('/api/sso', null, { headers: { Authorization: `Bearer ${this.token}` } })
         .then(response => {
           console.log(response.data);
         })
         .catch(error => {
           console.error(error);
         });
     },
     protectedResource() {
       axios.get('/api/protected', { headers: { Authorization: `Bearer ${this.token}` } })
         .then(response => {
           console.log(response.data);
         })
         .catch(error => {
           console.error(error);
         });
     },
     exchangeData() {
       axios.post('/api/exchange', this.data, { headers: { Authorization: `Bearer ${this.token}` } })
         .then(response => {
           console.log(response.data);
         })
         .catch(error => {
           console.error(error);
         });
     },
     resetPassword() {
       axios.post('/api/reset-password', this.user)
         .then(response => {
           console.log(response.data);
         })
         .catch(error => {
           console.error(error);
         });
     },
   },
 };
 </script>

注意

在项目中使用JWT时,需要注意以下几点:

  1. 安全性:JWT令牌是基于密钥签名的,因此确保在使用时使用强大的加密算法和安全的密钥管理。

    以下是一些强大的加密算法:

    • AES (Advanced Encryption Standard) - 对称加密算法,用于加密数据传输和存储。
    • RSA (Rivest–Shamir–Adleman) - 非对称加密算法,用于数字签名和密钥交换。
    • HMAC (Hash-based Message Authentication Code) - 基于哈希函数的消息认证码,用于验证数据完整性和真实性。
    • SHA-256 (Secure Hash Algorithm 256-bit) - 哈希函数,用于生成固定长度的摘要,常用于密码学应用中。
    • ECDH (Elliptic Curve Diffie-Hellman) - 椭圆曲线密钥交换协议,用于在两个参与者之间安全地共享密钥。
  2. 过期时间:为了防止令牌被滥用,应该设置适当的过期时间,并定期更新令牌。

    要为Spring Boot中的JWT令牌设置过期时间,我们可以使用以下代码:

     ​
     import io.jsonwebtoken.Claims;
     import io.jsonwebtoken.Jwts;
     import io.jsonwebtoken.SignatureAlgorithm;
     import io.jsonwebtoken.security.Keys;
     import java.security.Key;
     import java.util.Date;
     ​
     public class JwtUtil {
         private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
         private static final long expirationTimeInMs = 3600000; // 1 hour
         // 生成JWT令牌
         public static String generateToken(String subject) {
             Date now = new Date();
             Date expiration = new Date(now.getTime() + expirationTimeInMs);
     ​
             return Jwts.builder()
                     .setSubject(subject)
                     .setIssuedAt(now)
                     .setExpiration(expiration)
                     .signWith(key)
                     .compact();
         }
         // 验证JWT令牌
         public static boolean validateToken(String token) {
             try {
                 Jwts.parser().setSigningKey(key).parseClaimsJws(token);
                 return true;
             } catch (Exception e) {
                 return false;
             }
         }
     ​
         public static String getSubjectFromToken(String token) {
             Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
             return claims.getSubject();
         }
     }
    

    要定期更新JWT令牌,我们可以实现一个定时任务,在当前令牌过期之前生成一个新令牌。我们可以使用Spring的@Scheduled注释来安排任务。这是一个例子:

     import org.springframework.scheduling.annotation.Scheduled;
     ​
     public class TokenScheduler {
         private final JwtUtil jwtUtil;
         private String currentToken;
     ​
         public TokenScheduler(JwtUtil jwtUtil) {
             this.jwtUtil = jwtUtil;
             this.currentToken = jwtUtil.generateToken("user123");
         }
     ​
         public String getCurrentToken() {
             return currentToken;
         }
         // 使用Spring的@Scheduled注释来安排任务
         @Scheduled(fixedRate = 1800000) // 30 minutes
         public void updateToken() {
             currentToken = jwtUtil.generateToken("user123");
         }
     }
     ​
    
  3. 数据隐私:不要将敏感数据存储在JWT令牌中,因为它们可以通过解码令牌来访问。

  4. 令牌刷新:在某些情况下,可能需要刷新JWT令牌,例如用户更改密码权限等。在这种情况下,需要重新颁发新的令牌。

  5. 跨站点请求伪造(CSRF)攻击:为了防止CSRF攻击,应该在JWT令牌中包含CSRF令牌,并在每个请求中验证它。

CSRF攻击是一种利用用户已经登录的身份来进行恶意操作的攻击方式。攻击者会在第三方网站上放置一个恶意代码,当用户访问该网站时,代码会自动向目标网站发送请求,利用用户的登录状态进行操作。为了防止CSRF攻击,可以在JWT令牌中包含CSRF令牌,并在每个请求中验证它。在每个请求中,服务器会验证请求头请求参数中的CSRF令牌是否与JWT令牌中的CSRF令牌一致,如果不一致则拒绝该请求。

  1. 滥用检测:监控系统以检测任何异常活动,如频繁的登录尝试或使用同一JWT令牌进行多个请求。该系统跟踪用户行为并标记任何偏离正常模式的活动,有助于防止未经授权的访问并保护系统免受攻击
 // 导入必要的包
 ${INSERT_HERE}
 ​
 // 为滥用检测系统定义一个类
 public class AbuseDetectionSystem {
 ​
     // 定义必要的变量和数据结构
     ${INSERT_HERE}
 ​
     // 监视用户活动并检测任何可疑行为的方法
     public void monitorUserActivity(User user, Request request) {
         // 检查用户活动是否在正常模式内
         if (!isActivityWithinNormalPatterns(user, request)) {
             // 如果不是,则标记为可疑活动
             flagSuspiciousActivity(user, request);
             // 处理滥用或可疑行为
             handleAbuse(user, request);
         }
         // 更新用户活动历史记录
         updateActivityHistory(user, request);
     }
 ​
     // 标记任何偏离正常模式的活动的方法
     private void flagSuspiciousActivity(User user, Request request) {
         // 标记用户的活动为可疑
         ${INSERT_HERE}
     }
 ​
     // 更新用户活动历史记录的方法
     private void updateActivityHistory(User user, Request request) {
         // 将用户的活动添加到历史记录中
         ${INSERT_HERE}
     }
 ​
     // 检查用户的活动是否在正常模式内的方法
     private boolean isActivityWithinNormalPatterns(User user, Request request) {
         // 检查用户的活动是否在正常模式内
         ${INSERT_HERE}
     }
 ​
     // 处理任何检测到的滥用或可疑行为的方法
     private void handleAbuse(User user, Request request) {
         // 处理滥用或可疑行为
         ${INSERT_HERE}
     }
 }