前后端分离单点登录原理
一、引言
随着互联网技术的不断发展,前后端分离架构已经成为了主流,在这种架构下,前端和后端分别负责不同的功能,前端负责用户界面的展示,后端负责数据的处理和业务逻辑的实现,单点登录(Single Sign-On,SSO)是一种身份验证机制,它允许用户在一次登录后访问多个系统,而无需再次输入用户名和密码,本文将介绍前后端分离单点登录的原理,并通过一个具体的案例来实现前后端分离单点登录。
二、前后端分离单点登录原理
前后端分离单点登录的原理主要包括以下几个方面:
1、用户认证:用户在前端输入用户名和密码,前端将用户信息发送到后端进行认证,后端验证用户信息的正确性,并生成一个认证令牌(Token)。
2、令牌生成:后端生成的认证令牌包含了用户的身份信息和过期时间等信息,令牌可以是一个字符串、一个数字或者一个 JSON 对象等。
3、令牌存储:后端将生成的认证令牌存储到数据库、缓存或者本地存储等地方,令牌的存储方式可以根据实际情况进行选择。
4、令牌返回:后端将生成的认证令牌返回给前端,前端将令牌存储到本地存储或者 Cookie 中,以便在后续的请求中携带令牌。
5、请求拦截:前端在发送请求到后端之前,会先检查本地存储或者 Cookie 中是否存在认证令牌,如果存在令牌,则将令牌携带在请求头中发送到后端。
6、令牌验证:后端在接收到请求后,会先检查请求头中是否携带了认证令牌,如果存在令牌,则验证令牌的有效性,如果令牌有效,则允许用户访问系统;如果令牌无效,则返回错误信息。
7、令牌刷新:后端在令牌过期之前,会自动刷新令牌,刷新后的令牌会覆盖原来的令牌,以保证用户的登录状态始终有效。
三、前后端分离单点登录案例实现
为了更好地理解前后端分离单点登录的原理,下面通过一个具体的案例来实现前后端分离单点登录。
1、后端实现
(1)创建数据库表
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(255) NOT NULL ); CREATE TABLE tokens ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, token VARCHAR(255) NOT NULL, expires_at TIMESTAMP NOT NULL );
(2)创建用户实体类
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username", nullable = false, unique = true) private String username; @Column(name = "password", nullable = false) private String password; // 省略 getter 和 setter 方法 }
(3)创建令牌实体类
@Entity @Table(name = "tokens") public class Token { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "user_id", nullable = false) private Long userId; @Column(name = "token", nullable = false, unique = true) private String token; @Column(name = "expires_at", nullable = false) private LocalDateTime expiresAt; // 省略 getter 和 setter 方法 }
(4)创建用户服务接口
public interface UserService { User findByUsername(String username); void save(User user); }
(5)创建用户服务实现类
@Service public class UserServiceImpl implements UserService { @Autowired private UserRepository userRepository; @Override public User findByUsername(String username) { return userRepository.findByUsername(username); } @Override public void save(User user) { userRepository.save(user); } }
(6)创建令牌服务接口
public interface TokenService { Token generateToken(User user); boolean validateToken(String token); void refreshToken(String token); }
(7)创建令牌服务实现类
@Service public class TokenServiceImpl implements TokenService { @Autowired private UserService userService; @Autowired private TokenRepository tokenRepository; @Override public Token generateToken(User user) { String token = UUID.randomUUID().toString(); LocalDateTime expiresAt = LocalDateTime.now().plusHours(1); Token tokenEntity = new Token(); tokenEntity.setUserId(user.getId()); tokenEntity.setToken(token); tokenEntity.setExpiresAt(expiresAt); tokenRepository.save(tokenEntity); return tokenEntity; } @Override public boolean validateToken(String token) { Token tokenEntity = tokenRepository.findByToken(token); if (tokenEntity == null || tokenEntity.getExpiresAt().isBefore(LocalDateTime.now())) { return false; } return true; } @Override public void refreshToken(String token) { Token tokenEntity = tokenRepository.findByToken(token); if (tokenEntity!= null) { LocalDateTime expiresAt = LocalDateTime.now().plusHours(1); tokenEntity.setExpiresAt(expiresAt); tokenRepository.save(tokenEntity); } } }
(8)创建用户控制器
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @Autowired private TokenService tokenService; @PostMapping("/login") public String login(@RequestBody User user) { User existingUser = userService.findByUsername(user.getUsername()); if (existingUser == null ||!existingUser.getPassword().equals(user.getPassword())) { return "Invalid username or password"; } Token token = tokenService.generateToken(existingUser); return token.getToken(); } @GetMapping("/logout") public void logout(@RequestHeader("Authorization") String token) { tokenService.refreshToken(token); } }
(9)创建令牌控制器
@RestController @RequestMapping("/api/tokens") public class TokenController { @Autowired private TokenService tokenService; @GetMapping("/validate") public boolean validate(@RequestHeader("Authorization") String token) { return tokenService.validateToken(token); } }
(10)创建数据库连接池
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>3.4.5</version> </dependency>
(11)创建配置文件
spring: datasource: url: jdbc:mysql://localhost:3306/sso?useSSL=false&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver security: user: name: user password: password
2、前端实现
(1)创建登录页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录</title> </head> <body> <form action="/api/users/login" method="post"> <label for="username">用户名:</label><input type="text" id="username" name="username"><br> <label for="password">密码:</label><input type="password" id="password" name="password"><br> <input type="submit" value="登录"> </form> </body> </html>
(2)创建首页
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>首页</title> </head> <body> <h1>欢迎来到首页!</h1> <a href="/logout">退出</a> </body> </html>
(3)创建请求拦截器
axios.interceptors.request.use( function(config) { // 在发送请求之前,从本地存储中获取令牌 var token = localStorage.getItem('token'); if (token) { // 如果存在令牌,则将令牌添加到请求头中 config.headers.Authorization = 'Bearer'+ token; } return config; }, function(error) { // 对请求错误进行处理 return Promise.reject(error); } );
(4)创建登录逻辑
document.getElementById('loginForm').addEventListener('submit', function(event) { event.preventDefault(); var username = document.getElementById('username').value; var password = document.getElementById('password').value; axios.post('/api/users/login', { username: username, password: password }) .then(function(response) { // 如果登录成功,则将令牌存储到本地存储中 localStorage.setItem('token', response.data); // 跳转到首页 window.location.href = '/'; }) .catch(function(error) { // 如果登录失败,则显示错误信息 document.getElementById('loginError').innerHTML = error.response.data; }); });
(5)创建退出逻辑
document.getElementById('logout').addEventListener('click', function() { axios.get('/api/tokens/validate', { headers: { Authorization: 'Bearer'+ localStorage.getItem('token') } }) .then(function(response) { // 如果令牌有效,则调用退出接口 if (response.data) { axios.get('/api/tokens/logout', { headers: { Authorization: 'Bearer'+ localStorage.getItem('token') } }) .then(function(response) { // 如果退出成功,则清除令牌并跳转到登录页面 localStorage.removeItem('token'); window.location.href = '/login'; }) .catch(function(error) { // 如果退出失败,则显示错误信息 document.getElementById('logoutError').innerHTML = error.response.data; }); } }) .catch(function(error) { // 如果令牌无效,则直接跳转到登录页面 localStorage.removeItem('token'); window.location.href = '/login'; }); });
四、总结
本文介绍了前后端分离单点登录的原理,并通过一个具体的案例实现了前后端分离单点登录,通过前后端分离单点登录,可以提高系统的安全性和用户体验,减少用户的操作步骤,提高系统的性能和可扩展性。
评论列表