认识JWT
JSON Web Tokens - jwt.io
JWT(JSON Web Token)是一种用于在网络上安全传输信息
的开放标准。它由三个部分组成:头部
、载荷
和签名
。头部包含加密算法
和令牌类型
等信息,载荷包含用户信息
和其他元数据
,签名则通过使用密钥
对头部和载荷进行加密来验证令牌的真实性和完整性。JWT 可以被用于身份验证
和授权
,因为它可以帮助验证请求是否来自可信的源,并且可以将用户信息
和权限信息
嵌入到令牌中,从而避免了每次请求都需要进行数据库查询
的情况。
下面是一个JWT样例,头部、载荷、签名都用.
进行分隔
1 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
下面是对于一个JWT的解析过程:
将JWT字符串按照点号(.)分成三个部分:头部
、载荷
和签名
。
解码头部,得到加密算法
和令牌类型
等信息。
解码载荷,得到JWT中存储的信息
。
验证签名,确保JWT没有被篡改过。具体验证方式取决于使用的加密算法。
如果验证成功,则可以信任JWT中的信息。
需要注意的是,JWT只是一种基于文本的令牌
,因此它不提供加密功能
,只提供了签名
功能。如果需要加密数据,可以将JWT作为一个整体进行加密 。
使用JWT
以下是一些JWT的具体使用例子:
身份验证:当用户成功登录时,服务器
可以生成一个JWT并将其返回给客户端
。客户端可以在后续请求中将该JWT作为身份验证凭据
发送到服务器。服务器可以验证JWT的签名并确定用户是否有权访问所请求的资源。
单点登录:当用户成功登录到一个应用程序时,服务器可以生成一个JWT并将其返回给客户端。客户端可以在后续请求中将该JWT作为身份验证凭据发送到其他应用程序。其他应用程序可以验证JWT的签名并确定用户是否有权访问所请求的资源。
授权:当用户请求访问某个受保护的资源时,服务器可以检查JWT中包含的声明以确定用户是否有权访问该资源。例如,服务器可以检查JWT中是否包含特定的角色或权限声明。
信息交换:两个服务之间可以使用JWT来安全地交换信息。一个服务可以生成一个JWT并将其发送到另一个服务。接收方可以验证JWT的签名并提取其中包含的信息。
重置密码:当用户请求重置密码时,服务器可以生成一个包含重置令牌的JWT并将其发送到用户的电子邮件地址。用户可以使用该令牌来验证其身份并设置新密码。
以下是基于Vue+SpringBoot的一个简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 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)) { 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) { if (validate(token)) { return "Success" ; } else { return "Unauthorized" ; } } @GetMapping("/protected") public String protectedResource (@RequestHeader("Authorization") String token) { 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) { if (validate(token)) { return "Processed data: " + data; } else { return "Unauthorized" ; } } @PostMapping("/reset-password") public String resetPassword (@RequestBody User user) { 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) { return true ; } 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) { } }
下面的代码是一个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请求,并支持拦截器来处理请求和响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 <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时,需要注意以下几点:
安全性: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) - 椭圆曲线密钥交换协议,用于在两个参与者之间安全地共享密钥。
过期时间:为了防止令牌被滥用,应该设置适当的过期时间
,并定期更新
令牌。
要为Spring Boot中的JWT令牌设置过期时间,我们可以使用以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 ; 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(); } 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注释来安排任务。这是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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; } @Scheduled(fixedRate = 1800000) public void updateToken () { currentToken = jwtUtil.generateToken("user123" ); } }
数据隐私:不要将敏感数据
存储在JWT令牌中,因为它们可以通过解码令牌
来访问。
令牌刷新:在某些情况下,可能需要刷新JWT令牌,例如用户更改密码
或权限
等。在这种情况下,需要重新颁发新的令牌。
跨站点请求伪造(CSRF
)攻击:为了防止CSRF
攻击,应该在JWT令牌中包含CSRF
令牌,并在每个请求中验证它。
CSRF攻击
是一种利用用户已经登录的身份
来进行恶意操作的攻击方式。攻击者会在第三方网站上放置一个恶意代码
,当用户访问该网站时,代码会自动向目标网站发送请求,利用用户的登录状态进行操作。为了防止CSRF攻击,可以在JWT令牌中包含CSRF令牌,并在每个请求中验证它。在每个请求中,服务器会验证请求头
或请求参数
中的CSRF令牌是否与JWT令牌中的CSRF令牌一致,如果不一致则拒绝该请求。
滥用检测:监控系统以检测任何异常活动,如频繁的登录尝试或使用同一JWT令牌进行多个请求。该系统跟踪用户行为并标记任何偏离正常模式的活动,有助于防止未经授权的访问并保护系统免受攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ${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} } }