本文详细介绍了JWT单点登录的原理和实现步骤,涵盖了JWT的工作机制、JWT与单点登录的关系以及JWT单点登录的安全措施。文中还提供了使用Node.js和Spring Boot实现JWT单点登录的实际示例代码。JWT单点登录原理资料将帮助读者全面了解并实现这一安全认证机制。
JWT简介 什么是JWTJWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传递信息。JWT的设计目的是紧凑且易于使用,适合通过HTTP请求的头部进行传输。JWT通常用于身份验证,但也可以用于信息交换。JWT由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。这三部分分别负责不同的功能。
JWT的工作原理JWT的工作原理如下:
- 生成JWT令牌: 客户端请求服务器,请求携带用户名和密码,服务器验证用户信息后,生成一个JWT令牌。
- 发送JWT令牌: 服务器将生成的JWT令牌通过HTTP响应返回给客户端。
- 存储JWT令牌: 客户端接收到JWT令牌后,通常会将其存储在本地(如浏览器的local storage或session storage中)。
- 发送JWT令牌: 当用户尝试访问受保护的资源时,客户端会将JWT令牌通过HTTP请求的头部或作为URL参数发送给服务器。
- 验证JWT令牌: 服务器接收到JWT令牌后,进行验证,验证通过后,允许访问受保护的资源。
以下为生成JWT令牌的具体代码示例:
const jwt = require('jsonwebtoken');
const token = jwt.sign({
id: 1,
username: 'user123'
}, 'secret-key', { expiresIn: '1h' });
console.log(token);
JWT的组成部分
- 头部(Header): 由两部分组成,即令牌的类型(指定为JWT)以及所使用的签名算法(如HMAC SHA256或RSA)。
{ "typ": "JWT", "alg": "HS256" }
- 负载(Payload): 由声明(Claim)组成,包含各种声明。声明分为三类:
- 标准声明(Registered Claim): 这些声明有预定义的名字及用途。例如
iss
(发行者)、exp
(过期时间)、iat
(发行时间)等。 - 公开声明(Public Claim): 除了标准声明外,可以在JWT中包含任何其他声明(如用户ID、用户名、手机号等信息)。
- 私人声明(Private Claim): 私人声明是自定义声明,一般用于扩展,例如添加一些私有的声明,它们通常不使用。
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
- 标准声明(Registered Claim): 这些声明有预定义的名字及用途。例如
- 签名(Signature): 签名部分是指加密签名,用于验证消息的真实性和完整性。它由头部、负载和一个密钥组合在一起,通过加密算法生成。如
HMACSHA256
、RS256
等。HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
单点登录(Single Sign On, SSO)是一种身份验证方法,允许用户使用一组凭据(如用户名和密码)登录一个系统,然后自动访问所有其他已关联的受保护系统,而无需用户进行额外的身份验证。这样可以避免用户重复输入用户名和密码的过程,从而提升用户体验。
单点登录的好处- 简化用户操作: 用户只需要登录一次,即可访问多个应用程序或系统。
- 提升安全性: 通过集中化管理用户身份验证,可以简化安全措施的实施和管理。
- 提高效率: IT团队可以减少维护多套身份验证系统的负担,提高工作效率。
- 减少错误: 避免用户因频繁输入密码而引起的操作失误。
- 企业内部应用: 企业内部的各个应用系统可以通过SSO实现统一的身份验证,例如ERP系统、CRM系统、OA系统等。
- 多平台登录: 网站或应用可以通过SSO实现跨平台登录,例如Web端、移动端、桌面端等。
- 多租户系统: 多租户系统中,租户之间可以共享身份验证,简化租户管理。
- 云服务平台: 多个云服务提供商可以提供统一的登录入口,用户通过一个账号可以访问多个服务。
JWT非常适合用于实现单点登录,因为它具有以下优点:
- 无状态: 服务器端不需要存储任何状态信息,只需要验证令牌即可。
- 紧凑且易于传输: JWT令牌比较小,易于在网络中传输。
- 广泛支持: 大多数编程语言都支持JWT,便于跨平台实现。
- 安全性: 通过加密确保令牌的安全性。
JWT支持单点登录主要通过以下步骤实现:
- 统一身份验证: 当用户首次登录时,服务器验证用户身份并生成一个JWT令牌。
- 令牌缓存: 将生成的JWT令牌缓存在客户端(如浏览器的session或localStorage)。
- 令牌传递: 当用户访问其他服务时,客户端在请求中携带JWT令牌。
- 令牌验证: 目标服务接收请求后,通过验证JWT令牌来确认用户身份。
用户身份验证是JWT单点登录的第一步,需要验证用户提供的凭证(通常是用户名和密码)是否有效。
用户身份验证的具体代码示例
// 前端收集用户凭证
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'user123',
password: 'password123'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Login successful');
} else {
console.log('Login failed');
}
});
// 后端验证用户凭证
const express = require('express');
const bcrypt = require('bcryptjs');
const app = express();
app.use(express.json());
const users = [
{
id: 1,
username: 'user123',
password: '$2a$10$K93j62oF7zKu35Z6L67hce'
}
];
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (user && bcrypt.compareSync(password, user.password)) {
const token = jwt.sign({ id: user.id }, 'secret-key', { expiresIn: '1h' });
res.json({ success: true, token });
} else {
res.json({ success: false });
}
});
const jwt = require('jsonwebtoken');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
生成JWT令牌
当用户成功通过身份验证后,服务器将生成一个JWT令牌,这个令牌包含用户的某些信息(如用户ID、用户名等)。
const jwt = require('jsonwebtoken');
const token = jwt.sign({
id: 1,
username: 'user123'
}, 'secret-key', { expiresIn: '1h' });
console.log(token);
访问资源时验证JWT令牌
当用户访问受保护的资源时,服务器需要验证客户端提供的JWT令牌是否有效。
访问资源时验证JWT令牌的具体代码示例
// 前端发送请求
fetch('/protected-endpoint', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => response.json())
.then(data => {
console.log(data);
});
// 后端验证令牌
app.get('/protected-endpoint', (req, res) => {
const token = req.headers.authorization.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, 'secret-key', (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Failed to authenticate token' });
}
res.json({ message: 'Access granted', user: decoded });
});
});
JWT单点登录的安全措施
如何保护JWT的安全
保护JWT的安全性是实现单点登录的重要部分。以下是一些常见的安全措施:
- 密钥管理: 使用强大的密钥管理策略来保护用于生成JWT的密钥。
- 令牌签名: 使用加密算法(如HMAC、RSA)对JWT进行签名,以确保令牌的完整性。
- 令牌有效期: 设置合理的令牌有效时间,过期的令牌不再具有有效性。
- 令牌刷新: 提供令牌刷新机制,以便在令牌即将过期时自动获取新的令牌。
- 令牌信息: 尽量减少在负载中包含敏感信息,只传递必要的信息。
- 恶意用户攻击: 恶意用户可能尝试猜测或盗取他人的JWT令牌。
- 解决方案: 对JWT令牌进行签名,并设置令牌的有效时间,过期后的令牌不再有效。
- 中间人攻击: 中间人攻击者可能通过中间层截取和篡改JWT令牌。
- 解决方案: 使用HTTPS协议保护数据传输的安全性,避免中间人攻击。
- 令牌丢失: 用户可能丢失JWT令牌,例如在关闭浏览器窗口时。
- 解决方案: 提供令牌刷新功能,用户在令牌即将过期时可以自动获取新的令牌。
- 令牌篡改: 用户可能试图篡改JWT负载中的信息以获取未经授权的访问。
- 解决方案: 在负载中包含签名,使用加密算法确保令牌的完整性。
依赖安装
npm install express jsonwebtoken bcryptjs express-session
前端代码
<!DOCTYPE html>
<html>
<head>
<title>JWT SSO</title>
<script>
async function login() {
const response = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'user123',
password: 'password123'
})
});
const data = await response.json();
if (data.success) {
localStorage.setItem('token', data.token);
alert('Login successful');
} else {
alert('Login failed');
}
}
function logout() {
localStorage.removeItem('token');
alert('Logout successful');
}
</script>
</head>
<body>
<button onclick="login()">Login</button>
<button onclick="logout()">Logout</button>
</body>
</html>
后端代码
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(bodyParser.json());
app.use(session({
secret: 'secret-key', // 用于签名session对象的字符串
resave: false,
saveUninitialized: true
}));
const users = [
{
id: 1,
username: 'user123',
password: '$2a$10$K93j62oF7zKu35Z6L67hce'
}
];
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (user && bcrypt.compareSync(password, user.password)) {
const token = jwt.sign({ id: user.id }, 'secret-key', { expiresIn: '1h' });
res.json({ success: true, token });
} else {
res.json({ success: false });
}
});
app.get('/protected-endpoint', (req, res) => {
const token = req.headers.authorization.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, 'secret-key', (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Failed to authenticate token' });
}
res.json({ message: 'Access granted', user: decoded });
});
});
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
使用Spring Boot实现JWT单点登录
依赖管理
在pom.xml
中添加以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
前端代码
<!DOCTYPE html>
<html>
<head>
<title>JWT SSO</title>
<script>
async function login() {
const response = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'user123',
password: 'password123'
})
});
const data = await response.json();
if (data.success) {
localStorage.setItem('token', data.token);
alert('Login successful');
} else {
alert('Login failed');
}
}
function logout() {
localStorage.removeItem('token');
alert('Logout successful');
}
</script>
</head>
<body>
<button onclick="login()">Login</button>
<button onclick="logout()">Logout</button>
</body>
</html>
后端代码
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.MacSigner;
import com.nimbusds.jose.crypto.MacVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("user123".equals(username)) {
return new User("user123",
new BCryptPasswordEncoder().encode("password123"),
new ArrayList<>());
} else {
throw new UsernameNotFoundException("User not found");
}
}
}
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
@Service
public class JwtTokenUtil {
private final PasswordEncoder encoder;
public JwtTokenUtil(PasswordEncoder encoder) {
this.encoder = encoder;
}
public String generateToken(UserDetails userDetails) {
try {
JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.subject(userDetails.getUsername())
.claim("authorities", userDetails.getAuthorities())
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + 3600000L))
.build();
SignedJWT signedJWT = new SignedJWT(header, claims);
signedJWT.sign(new MacSigner(encoder.encode("secret-key")));
return signedJWT.serialize();
} catch (JOSEException e) {
throw new RuntimeException(e);
}
}
public boolean validateToken(String token, UserDetails userDetails) {
try {
SignedJWT signedJWT = SignedJWT.parse(token);
MacVerifier verifier = new MacVerifier(encoder.encode("secret-key"));
return signedJWT.verify(verifier) && signedJWT.getJWTClaimsSet().getSubject().equals(userDetails.getUsername());
} catch (JOSEException e) {
throw new RuntimeException(e);
}
}
public boolean validateToken(String token) {
try {
SignedJWT signedJWT = SignedJWT.parse(token);
MacVerifier verifier = new MacVerifier(encoder.encode("secret-key"));
return signedJWT.verify(verifier);
} catch (JOSEException e) {
throw new RuntimeException(e);
}
}
}
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.JWTClaimsSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService jwtInMemoryUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
JWTClaimsSet claims = jwtTokenUtil.parseClaims(jwtToken);
username = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = jwtInMemoryUserDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.Map;
@RestController
public class JwtAuthenticationController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService jwtInMemoryUserDetailsService;
@PostMapping("/login")
public Map<String, String> createAuthenticationToken(@RequestBody JwtRequest jwtRequest) throws Exception {
authenticate(jwtRequest.getUsername(), jwtRequest.getPassword());
final UserDetails userDetails = jwtInMemoryUserDetailsService.loadUserByUsername(jwtRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return Collections.singletonMap("token", token);
}
private void authenticate(String username, String password) throws Exception {
Objects.requireNonNull(username);
Objects.requireNonNull(password);
UserDetails userDetails = jwtInMemoryUserDetailsService.loadUserByUsername(username);
if (!encoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("Invalid username or password");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null,
Collections.<GrantedAuthority>emptyList()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
@GetMapping("/protected-endpoint")
public Map<String, String> getProtectedResource(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
JWTClaimsSet claims = jwtTokenUtil.parseClaims(token);
String username = claims.getSubject();
if (jwtTokenUtil.validateToken(token, jwtInMemoryUserDetailsService.loadUserByUsername(username))) {
return Collections.singletonMap("response", "Access granted");
} else {
return Collections.singletonMap("response", "Invalid token");
}
} catch (Exception e) {
return Collections.singletonMap("response", "Invalid token");
}
} else {
return Collections.singletonMap("response", "No token");
}
}
}
``
通过以上示例,可以理解如何在不同的技术栈中实现JWT单点登录,包括前端和后端的实现细节。
共同學(xué)習(xí),寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章