從零搭建微服務項目Pro(第6-1章——Spring Security+JWT實現用戶鑒權訪問與token刷新)

前言:

在現代的微服務架構中,用戶鑒權和訪問控制是非常重要的一部分。Spring Security 是 Spring 生態中用于處理安全性的強大框架,而 JWT(JSON Web Token)則是一種輕量級的、自包含的令牌機制,廣泛用于分布式系統中的用戶身份驗證和信息交換。

本章實現了一個門檻極低的Spring Security+JWT實現用戶鑒權訪問與token刷新demo項目。具體效果可看測試部分內容。

只需要創建一個spring-boot項目,導入下文pom依賴以及項目結構如下,將各類的內容粘貼即可。(不需要nacos、數據庫等配置,也不需要動yml配置文件。且用ai生成了html網頁,減去了用postman測試接口的麻煩)。

也可直接選擇下載項目源碼,鏈接如下:

wlf728050719/SpringCloudPro6-1https://github.com/wlf728050719/SpringCloudPro6-1

以及本專欄會持續更新微服務項目,每一章的項目都會基于前一章項目進行功能的完善,歡迎小伙伴們關注!同時如果只是對單章感興趣也不用從頭看,只需下載前一章項目即可,每一章都會有前置項目準備部分,跟著操作就能實現上一章的最終效果,當然如果是一直跟著做可以直接跳過這一部分。專欄目錄鏈接如下,其中Base篇為基礎微服務搭建,Pro篇為復雜模塊實現。

從零搭建微服務項目(全)-CSDN博客https://blog.csdn.net/wlf2030/article/details/145799620??????


依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>cn.bit</groupId><artifactId>Pro6_1</artifactId><version>0.0.1-SNAPSHOT</version><name>Pro6_1</name><description>Pro6_1</description><properties><java.version>17</java.version></properties><dependencies><!-- Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- OAuth2 Authorization Server (Spring Boot 3.x 推薦) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-authorization-server</artifactId></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><!-- JWT --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency><dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>2.3.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

核心:

工具類:

SaltUtil,用于生成隨機鹽。(不過由于本章沒有將用戶賬號密碼等信息存放在數據庫,在代碼中寫死用戶信息,所以這個工具類實際沒有作用)。

package cn.bit.pro6_1.core.util;import java.security.SecureRandom;
import java.util.Base64;/*** 鹽值工具類* @author muze*/
public class SaltUtil {/*** 生成鹽值* @return 鹽值*/public static String generateSalt() {// 聲明并初始化長度為16的字節數組,用于存儲隨機生成的鹽值byte[] saltBytes = new byte[16];// 創建SecureRandom實例,用于生成強隨機數SecureRandom secureRandom = new SecureRandom();// 將隨機生成的鹽值填充到字節數組secureRandom.nextBytes(saltBytes);// 將字節數組編碼為Base64格式的字符串后返回return Base64.getEncoder().encodeToString(saltBytes);}
}

JwtUtil,用于生成和驗證token。(密鑰為了不寫配置文件就直接寫代碼里了,以及設置access token和refresh token失效時間為10s和20s方便測試)

package cn.bit.pro6_1.core.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;@Component
public class JwtUtil {private String secret = "wlf18086270070";private final Long accessTokenExpiration = 10L; // 1 小時private final Long refreshTokenExpiration = 20L; // 7 天public String generateAccessToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();return createToken(claims, userDetails.getUsername(), accessTokenExpiration);}public String generateRefreshToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();return createToken(claims, userDetails.getUsername(), refreshTokenExpiration);}private String createToken(Map<String, Object> claims, String subject, Long expiration) {return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)).signWith(SignatureAlgorithm.HS256, secret).compact();}public Boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}public String extractUsername(String token) {return extractClaim(token, Claims::getSubject);}public Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration);}private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {final Claims claims = extractAllClaims(token);return claimsResolver.apply(claims);}private Claims extractAllClaims(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}private Boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}public Date getAccessTokenExpiration() {return new Date(System.currentTimeMillis() + accessTokenExpiration * 1000);}public Date getRefreshTokenExpiration() {return new Date(System.currentTimeMillis() + refreshTokenExpiration * 1000);}
}

SecurityUtils,方便全局接口獲取請求的用戶信息。

package cn.bit.pro6_1.core.util;import lombok.experimental.UtilityClass;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;/*** 安全工具類** @author L.cm*/
@UtilityClass
public class SecurityUtils {/*** 獲取Authentication*/public Authentication getAuthentication() {return SecurityContextHolder.getContext().getAuthentication();}/*** 獲取用戶* @param authentication* @return HnqzUser* <p>*/public User getUser(Authentication authentication) {if (authentication == null || authentication.getPrincipal() == null) {return null;}Object principal = authentication.getPrincipal();if (principal instanceof User) {return (User) principal;}return null;}/*** 獲取用戶*/public User getUser() {Authentication authentication = getAuthentication();return getUser(authentication);}
}

用戶加載:

UserService,模擬數據庫中有admin和buyer兩個用戶密碼分別為123456和654321

package cn.bit.pro6_1.core.service;import cn.bit.pro6_1.core.util.SaltUtil;
import cn.bit.pro6_1.pojo.UserPO;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
@AllArgsConstructor
public class UserService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//模擬通過username通過feign拿取到了對應用戶UserPO user;BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();if (username.equals("admin")) {user = new UserPO();user.setUsername(username);user.setPassword(encoder.encode("123456"));user.setRoles("ROLE_ADMIN");user.setSalt(SaltUtil.generateSalt());}else if(username.equals("buyer")){user = new UserPO();user.setUsername(username);user.setPassword(encoder.encode("654321"));user.setRoles("ROLE_BUYER");user.setSalt(SaltUtil.generateSalt());}elsethrow new UsernameNotFoundException("not found");//模擬通過role從數據庫字典項中獲取對應角色權限,暫不考慮多角色用戶List<GrantedAuthority> authorities = new ArrayList<>();authorities.add(new SimpleGrantedAuthority(user.getRoles()));//先加入用戶角色//加入用戶對應角色權限if(user.getRoles().contains("ROLE_ADMIN")){authorities.add(new SimpleGrantedAuthority("READ"));authorities.add(new SimpleGrantedAuthority("WRITE"));}else if(user.getRoles().contains("ROLE_BUYER")){authorities.add(new SimpleGrantedAuthority("READ"));}return new User(user.getUsername(), user.getPassword(),authorities);}
}

過濾器:

JwtRequestFilter,用戶鑒權并將鑒權信息放secruity全局上下文

package cn.bit.pro6_1.core.filter;import cn.bit.pro6_1.core.util.JwtUtil;
import lombok.AllArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
@AllArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {private JwtUtil jwtUtil;private UserDetailsService userDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {final String authorizationHeader = request.getHeader("Authorization");String username = null;String jwt = null;if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {jwt = authorizationHeader.substring(7);username = jwtUtil.extractUsername(jwt);}if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);if (jwtUtil.validateToken(jwt, userDetails)) {UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}}chain.doFilter(request, response);}
}

配置類:

CorsConfig,跨域請求配置。(需要設置為自己前端運行的端口號)

package cn.bit.pro6_1.core.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import java.util.List;@Configuration
public class CorsConfig {@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration = new CorsConfiguration();configuration.setAllowedOrigins(List.of("http://localhost:63342")); // 明確列出允許的域名configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); // 允許的請求方法configuration.setAllowedHeaders(List.of("*")); // 允許的請求頭configuration.setAllowCredentials(true); // 允許攜帶憑證(如 Cookie)UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration); // 對所有路徑生效return source;}
}

ResourceServerConfig,資源服務器配置。配置鑒權過濾器鏈,以及退出登錄處理邏輯。在登錄認證和刷新token時不進行access token校驗,其余接口均進行token校驗。這里需要將jwt的過濾器放在logout的過濾器前,否則logout無法獲取secruity上下文中的用戶信息,報空指針錯誤,從而無法做后續比如清除redis中token,日志記錄等操作。

package cn.bit.pro6_1.core.config;import cn.bit.pro6_1.core.filter.JwtRequestFilter;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsConfigurationSource;import jakarta.servlet.http.HttpServletResponse;@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class ResourceServerConfig {private final JwtRequestFilter jwtRequestFilter;private final CorsConfigurationSource corsConfigurationSource;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.cors(cors -> cors.configurationSource(corsConfigurationSource)).csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF.authorizeHttpRequests(auth -> auth.requestMatchers("/authenticate", "/refresh-token").permitAll() // 允許匿名訪問.requestMatchers("/admin/**").hasRole("ADMIN") // ADMIN 角色可訪問.requestMatchers("/buyer/**").hasRole("BUYER") // BUYER 角色可訪問.anyRequest().authenticated() // 其他請求需要認證).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 無狀態會話).logout(logout -> logout.logoutUrl("/auth/logout") // 退出登錄的 URL.addLogoutHandler(logoutHandler()) // 自定義退出登錄處理邏輯.logoutSuccessHandler(logoutSuccessHandler()) // 退出登錄成功后的處理邏輯.invalidateHttpSession(true) // 使 HTTP Session 失效.deleteCookies("JSESSIONID") // 刪除指定的 Cookie).addFilterBefore(jwtRequestFilter, LogoutFilter.class); // 添加 JWT 過濾器return http.build();}@Beanpublic LogoutHandler logoutHandler() {return (request, response, authentication) -> {if (authentication != null) {// 用戶已認證,執行正常的登出邏輯System.out.println("User logged out: " + authentication.getName());// 這里可以添加其他邏輯,例如記錄日志、清理資源等} else {// 用戶未認證,處理未登錄的情況System.out.println("Logout attempt without authentication");// 可以選擇記錄日志或執行其他操作}};}@Beanpublic LogoutSuccessHandler logoutSuccessHandler() {return (request, response, authentication) -> {// 退出登錄成功后的邏輯,例如返回 JSON 響應response.setStatus(HttpServletResponse.SC_OK);response.getWriter().write("Logout successful");};}
}

Pojo:

封裝登錄請求和響應,以及用戶實體類

package cn.bit.pro6_1.pojo;import lombok.Data;@Data
public class LoginRequest {private String username;private String password;
}
package cn.bit.pro6_1.pojo;import lombok.Data;import java.util.Date;@Data
public class LoginResponse {private String accessToken;private String refreshToken;private Date accessTokenExpires;private Date refreshTokenExpires;
}
package cn.bit.pro6_1.pojo;import lombok.Data;@Data
public class UserPO {private Integer id;private String username;private String password;private String roles;private String salt;
}

接口:

全局異常抓取

package cn.bit.pro6_1.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import io.jsonwebtoken.ExpiredJwtException;
import java.nio.file.AccessDeniedException;@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 全局異常.* @param e the e* @return R*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public String handleGlobalException(Exception e) {log.error("全局異常信息 ex={}", e.getMessage(), e);return e.getLocalizedMessage();}/*** AccessDeniedException* @param e the e* @return R*/@ExceptionHandler(AccessDeniedException.class)@ResponseStatus(HttpStatus.FORBIDDEN)public String handleAccessDeniedException(AccessDeniedException e) {log.error("拒絕授權異常信息 ex={}", e.getLocalizedMessage(),e);return e.getLocalizedMessage();}/**** @param e the e* @return R*/@ExceptionHandler(ExpiredJwtException.class)@ResponseStatus(HttpStatus.FORBIDDEN)public String handleExpiredJwtException(ExpiredJwtException e) {log.error("Token過期 ex={}", e.getLocalizedMessage(),e);return e.getLocalizedMessage();}
}

登錄接口

package cn.bit.pro6_1.controller;import cn.bit.pro6_1.pojo.LoginRequest;
import cn.bit.pro6_1.pojo.LoginResponse;
import cn.bit.pro6_1.core.service.UserService;
import cn.bit.pro6_1.core.util.JwtUtil;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Date;@RestController
@RequestMapping("/authenticate")
@AllArgsConstructor
public class AuthenticationController {private final JwtUtil jwtUtil;private final UserService userService;private final PasswordEncoder passwordEncoder;@PostMappingpublic ResponseEntity<LoginResponse> createAuthenticationToken(@RequestBody LoginRequest loginRequest) {// 生成 Access Token 和 Refresh TokenUserDetails userDetails = userService.loadUserByUsername(loginRequest.getUsername());if(!passwordEncoder.matches(loginRequest.getPassword(), userDetails.getPassword())) {throw new RuntimeException("密碼錯誤");}String accessToken = jwtUtil.generateAccessToken(userDetails);String refreshToken = jwtUtil.generateRefreshToken(userDetails);// 獲取 Token 過期時間Date accessTokenExpires = jwtUtil.getAccessTokenExpiration();Date refreshTokenExpires = jwtUtil.getRefreshTokenExpiration();// 返回 Token 和過期時間LoginResponse loginResponse = new LoginResponse();loginResponse.setAccessToken(accessToken);loginResponse.setRefreshToken(refreshToken);loginResponse.setAccessTokenExpires(accessTokenExpires);loginResponse.setRefreshTokenExpires(refreshTokenExpires);return ResponseEntity.ok(loginResponse);}
}

access token刷新接口

package cn.bit.pro6_1.controller;import cn.bit.pro6_1.pojo.LoginRequest;
import cn.bit.pro6_1.pojo.LoginResponse;
import cn.bit.pro6_1.core.service.UserService;
import cn.bit.pro6_1.core.util.JwtUtil;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Date;@RestController
@RequestMapping("/authenticate")
@AllArgsConstructor
public class AuthenticationController {private final JwtUtil jwtUtil;private final UserService userService;private final PasswordEncoder passwordEncoder;@PostMappingpublic ResponseEntity<LoginResponse> createAuthenticationToken(@RequestBody LoginRequest loginRequest) {// 生成 Access Token 和 Refresh TokenUserDetails userDetails = userService.loadUserByUsername(loginRequest.getUsername());if(!passwordEncoder.matches(loginRequest.getPassword(), userDetails.getPassword())) {throw new RuntimeException("密碼錯誤");}String accessToken = jwtUtil.generateAccessToken(userDetails);String refreshToken = jwtUtil.generateRefreshToken(userDetails);// 獲取 Token 過期時間Date accessTokenExpires = jwtUtil.getAccessTokenExpiration();Date refreshTokenExpires = jwtUtil.getRefreshTokenExpiration();// 返回 Token 和過期時間LoginResponse loginResponse = new LoginResponse();loginResponse.setAccessToken(accessToken);loginResponse.setRefreshToken(refreshToken);loginResponse.setAccessTokenExpires(accessTokenExpires);loginResponse.setRefreshTokenExpires(refreshTokenExpires);return ResponseEntity.ok(loginResponse);}
}

admin

package cn.bit.pro6_1.controller;import cn.bit.pro6_1.core.util.SecurityUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/admin")
public class AdminController {@GetMapping("/info")@PreAuthorize("hasRole('ADMIN')") // 只有 ADMIN 角色可以訪問public String adminInfo() {User user = SecurityUtils.getUser();System.out.println(user.getUsername());return "This is admin info. Only ADMIN can access this.";}@GetMapping("/manage")@PreAuthorize("hasRole('ADMIN')") // 只有 ADMIN 角色可以訪問public String adminManage() {return "This is admin management. Only ADMIN can access this.";}
}

buyer

package cn.bit.pro6_1.controller;import cn.bit.pro6_1.core.util.SecurityUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/buyer")
public class BuyerController {@GetMapping("/info")@PreAuthorize("hasRole('BUYER')") // 只有 BUYER 角色可以訪問public String buyerInfo() {User user = SecurityUtils.getUser();System.out.println(user.getUsername());return "This is buyer info. Only BUYER can access this.";}@GetMapping("/order")@PreAuthorize("hasRole('BUYER')") // 只有 BUYER 角色可以訪問public String buyerOrder() {return "This is buyer order. Only BUYER can access this.";}
}

前端:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>權限控制測試</title><style>body {font-family: Arial, sans-serif;background-color: #f4f4f4;margin: 0;padding: 20px;}.container {max-width: 600px;margin: auto;background: #fff;padding: 20px;border-radius: 8px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);}h1, h2 {color: #333;}label {display: block;margin: 10px 0 5px;}input[type="text"],input[type="password"] {width: 100%;padding: 10px;margin-bottom: 20px;border: 1px solid #ccc;border-radius: 4px;}button {background-color: #28a745;color: white;padding: 10px;border: none;border-radius: 4px;cursor: pointer;width: 100%;}button:hover {background-color: #218838;}.result {margin-top: 20px;}.error {color: red;}.logout-button {background-color: #dc3545; /* 紅色按鈕 */margin-top: 10px;}.logout-button:hover {background-color: #c82333;}</style>
</head>
<body>
<div class="container"><h1>登錄</h1><form id="loginForm"><label for="username">用戶名:</label><input type="text" id="username" name="username" required><label for="password">密碼:</label><input type="password" id="password" name="password" required><button type="submit">登錄</button></form><div class="result" id="loginResult"></div><h2>Token 失效倒計時</h2><div id="accessTokenCountdown"></div><div id="refreshTokenCountdown"></div><h2>測試接口</h2><button onclick="testAdminInfo()">測試 /admin/info</button><button onclick="testBuyerInfo()">測試 /buyer/info</button><!-- 退出按鈕 --><button class="logout-button" onclick="logout()">退出登錄</button><div class="result" id="apiResult"></div>
</div><script>let accessToken = '';let refreshToken = '';let accessTokenExpires;let refreshTokenExpires;let accessTokenCountdownInterval;let refreshTokenCountdownInterval;document.getElementById('loginForm').addEventListener('submit', async (event) => {event.preventDefault();const username = document.getElementById('username').value;const password = document.getElementById('password').value;const response = await fetch('http://localhost:8080/authenticate', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ username, password })});if (response.ok) {const data = await response.json();accessToken = data.accessToken;refreshToken = data.refreshToken;accessTokenExpires = new Date(data.accessTokenExpires).getTime();refreshTokenExpires = new Date(data.refreshTokenExpires).getTime();document.getElementById('loginResult').innerHTML = `<p>登錄成功!Access Token: ${accessToken}</p>`;startCountdown('accessTokenCountdown', accessTokenExpires, 'Access Token 將在 ');startCountdown('refreshTokenCountdown', refreshTokenExpires, 'Refresh Token 將在 ');} else {document.getElementById('loginResult').innerHTML = `<p class="error">登錄失敗,狀態碼: ${response.status}</p>`;}});function startCountdown(elementId, expirationTime, prefix) {const countdownElement = document.getElementById(elementId);const interval = setInterval(() => {const now = new Date().getTime();const distance = expirationTime - now;if (distance <= 0) {clearInterval(interval);countdownElement.innerHTML = `${prefix}已過期`;} else {const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));const seconds = Math.floor((distance % (1000 * 60)) / 1000);countdownElement.innerHTML = `${prefix}${hours} 小時 ${minutes} 分鐘 ${seconds} 秒后過期`;}}, 1000);// 根據元素 ID 記錄對應的計時器if (elementId === 'accessTokenCountdown') {accessTokenCountdownInterval = interval;} else if (elementId === 'refreshTokenCountdown') {refreshTokenCountdownInterval = interval;}}async function testAdminInfo() {if (!accessToken) {alert('請先登錄!');return;}const response = await fetch('http://localhost:8080/admin/info', {method: 'GET',headers: {'Authorization': `Bearer ${accessToken}`}});if (response.ok) {const data = await response.text();document.getElementById('apiResult').innerHTML = `<p>響應: ${data}</p>`;} else if (response.status === 403) {await refreshAccessToken();await testAdminInfo(); // 重新嘗試} else {document.getElementById('apiResult').innerHTML = `<p class="error">訪問失敗,狀態碼: ${response.status}</p>`;}}async function testBuyerInfo() {if (!accessToken) {alert('請先登錄!');return;}const response = await fetch('http://localhost:8080/buyer/info', {method: 'GET',headers: {'Authorization': `Bearer ${accessToken}`}});if (response.ok) {const data = await response.text();document.getElementById('apiResult').innerHTML = `<p>響應: ${data}</p>`;} else if (response.status === 403) {await refreshAccessToken();await testBuyerInfo(); // 重新嘗試} else {document.getElementById('apiResult').innerHTML = `<p class="error">訪問失敗,狀態碼: ${response.status}</p>`;}}async function refreshAccessToken() {const response = await fetch('http://localhost:8080/refresh-token', {method: 'POST',headers: {'Authorization': refreshToken}});if (response.ok) {const data = await response.json();accessToken = data.accessToken; // 更新 access tokenaccessTokenExpires = new Date(data.accessTokenExpires).getTime(); // 更新過期時間document.getElementById('loginResult').innerHTML = `<p>Access Token 刷新成功!新的 Access Token: ${accessToken}</p>`;// 更新 accessToken 的倒計時startCountdown('accessTokenCountdown', accessTokenExpires, 'Access Token 將在 ');} else if (response.status === 403) {// 清除 tokens 并提示用戶重新登錄accessToken = '';refreshToken = '';document.getElementById('loginResult').innerHTML = `<p class="error">刷新 Token 失敗,請重新登錄。</p>`;alert('請重新登錄!');} else {document.getElementById('loginResult').innerHTML = `<p class="error">刷新 Token 失敗,狀態碼: ${response.status}</p>`;}}// 退出登錄邏輯async function logout() {// 調用退出登錄接口const response = await fetch('http://localhost:8080/auth/logout', {method: 'POST',headers: {'Authorization': `Bearer ${accessToken}`}});if (response.ok) {// 清除本地存儲的 tokensaccessToken = '';refreshToken = '';// 停止倒計時clearInterval(accessTokenCountdownInterval);clearInterval(refreshTokenCountdownInterval);// 更新頁面顯示document.getElementById('loginResult').innerHTML = `<p>退出登錄成功!</p>`;document.getElementById('accessTokenCountdown').innerHTML = '';document.getElementById('refreshTokenCountdown').innerHTML = '';document.getElementById('apiResult').innerHTML = '';} else {document.getElementById('loginResult').innerHTML = `<p class="error">退出登錄失敗,狀態碼: ${response.status}</p>`;}}
</script>
</body>
</html>

測試:

啟動服務,打開前端:

1.輸入錯誤的賬號

后端拋出用戶名未找到的異常

2.輸入錯誤密碼

后端拋出密碼錯誤異常

3.正確登錄

顯示兩個token有效期倒計時以及access-token的值

4.訪問admin接口

5.訪問buyer接口

會看到access-token會不斷刷新,但不會顯示"This is buyer info. Only BUYER can access this."字體,看上去有點鬼畜,原因是前端寫的是在收到403狀態碼后會以為是access-token過期而會訪問fresh接口并再次執行一次接口。但實際上這個403是因為沒有對應權限所導致的,這個問題無論改前端還是后端都能解決,但前端是ai生成的且我自己也不是很了解,后端也可限定不同異常的錯誤響應碼,但正如開篇所說本章只是各基礎demo所以就懶的改了。反正請求確實是攔截到了。

6.測試token刷新

在access-token過期但refresh-token未過期時測試admin,能夠看到刷新成功且重新訪問接口成功

fresh-token過期后則顯示重新登錄


最后:

auth模塊在微服務項目中的重要性都不言而喻,目前只是實現了一個簡單的框架,在后面幾章會添加feign調用的鑒權,以及redis存放token從而同時獲取有狀態和無狀態校驗的優點,以及mysql交互獲取數據庫中信息等。還敬請關注!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/898524.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/898524.shtml
英文地址,請注明出處:http://en.pswp.cn/news/898524.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

使用HAI來打通DeepSeek的任督二脈

一、什么是HAI HAI是一款專注于AI與科學計算領域的云服務產品&#xff0c;旨在為開發者、企業及科研人員提供高效、易用的算力支持與全棧解決方案。主要使用場景為&#xff1a; AI作畫&#xff0c;AI對話/寫作、AI開發/測試。 二、開通HAI 選擇CPU算力 16核32GB&#xff0c;這…

【保姆級】阿里云codeup配置Git的CI/CD步驟

以下是通過阿里云CodeUp的Git倉庫進行CI/CD配置的詳細步驟&#xff0c;涵蓋前端&#xff08;Vue 3&#xff09;和后端&#xff08;Spring Boot&#xff09;項目的自動化打包&#xff0c;并將前端打包結果嵌入到Nginx的Docker鏡像中&#xff0c;以及將后端打包的JAR文件拷貝至Do…

LINUX網絡編程API原型詳細解析

1. 網絡體系 1.1. 簡介 網絡采用分而治之的方法設計&#xff0c;將網絡的功能劃分為不同的模塊&#xff0c;以分層的形式有機組合在一起。 每層實現不同的功能&#xff0c;其內部實現方法對外部其他層次來說是透明的。每層向上層提供服務&#xff0c;同時使用下層提供…

藍橋杯 之 暴力回溯

文章目錄 數字接龍小u的最大連續移動次數問題迷宮 在藍橋杯中&#xff0c;十分喜歡考察對于網格的回溯的問題&#xff0c;對于這類的問題&#xff0c;常常會使用到這個DFS和BFS進行考察&#xff0c;不過無論怎么考察&#xff0c;都只是會在最基礎的模本的基礎上&#xff0c;根據…

微信小程序的業務域名配置(通過ingress網關的注解)

一、背景 微信小程序的業務域名配置&#xff08;通過kong網關的pre-function配置&#xff09;是依靠kong實現&#xff0c;本文將通過ingress網關實現。 而我們的服務是部署于阿里云K8S容器&#xff0c;當然內核與ingress無異。 找到k8s–>網絡–>路由 二、ingress注解 …

Python數據可視化工具:六西格瑪及其基礎工具概覽

在當今數據驅動的時代&#xff0c;數據分析和可視化工具成為了各行業優化流程、提升質量的關鍵手段。六西格瑪&#xff08;Six Sigma&#xff09;作為一種以數據為基礎、追求完美質量的管理理念&#xff0c;其實施依賴于一系列基礎工具的靈活運用。而Python&#xff0c;憑借其強…

集群環境下Redis 商品庫存系統設計

目錄 環境實現基本結構代碼業務代碼主體庫存管理模塊 后續問題高并發臨界值與樂觀鎖問題 完整代碼總結后話 環境 我們現在要做商品秒殺系統。功能很簡單&#xff0c;就是庫存刪減。用戶先下單減庫存&#xff0c;之后再進行扣款。 實現 基本結構代碼 那么我們先看下如何搭建…

Spring MVC響應數據

handler方法分析 /*** TODO: 一個controller的方法是控制層的一個處理器,我們稱為handler* TODO: handler需要使用RequestMapping/GetMapping系列,聲明路徑,在HandlerMapping中注冊,供DS查找!* TODO: handler作用總結:* 1.接收請求參數(param,json,pathVariable,共享域等…

基于圖像識別的醫學影像大數據診斷系統的設計與實現

標題:基于圖像識別的醫學影像大數據診斷系統的設計與實現 內容:1.摘要 隨著醫學影像技術的快速發展&#xff0c;醫學影像數據量呈爆炸式增長&#xff0c;傳統的人工診斷方式在處理海量數據時效率低下且容易出現誤差。本研究的目的是設計并實現一個基于圖像識別的醫學影像大數據…

Python散點圖(Scatter Plot):數據探索的“第一張圖表”

在數據可視化領域,散點圖是一種強大而靈活的工具,它能夠幫助我們直觀地理解和探索數據集中變量之間的關系。本文將深入探討散點圖的核心原理、應用場景以及如何使用Python進行高效繪制。 后續幾篇將介紹高級技巧、復雜應用場景。 Python散點圖(Scatter Plot):高階分析、散點…

【redis】在 Spring中操作 Redis

文章目錄 基礎設置依賴StringRedisTemplate庫的封裝 運行StringList刪庫 SetHashZset 基礎設置 依賴 需要選擇這個依賴 StringRedisTemplate // 后續 redis 測試的各種方法&#xff0c;都通過這個 Controller 提供的 http 接口來觸發 RestController public class MyC…

微服務》》Kubernetes (K8S) 集群 安裝

關閉交換空間 # 切換 超級管理員身份 # 查看交換空間 free -h # 關閉交換空間 swapoff -a避免開啟啟動交換空間 # 注釋swap開頭的行 vim /etc/fstab關閉防火墻 # 關閉防火墻 # 因為K8S 是集群形式存在的 至少三臺 一主二從 &#xff08;一個master 兩個node&#xff09…

HTTP和RPC的區別

RPC和 HTTP是兩種常見的通信方式&#xff0c;它們在設計目標、使用場景和技術實現上有顯著區別。以下是它們的詳細對比&#xff1a; 1. 定義與核心思想 特性RPCHTTPRemote Procedure Call遠程過程調用HyperText Transfer Protocol超文本傳輸協議定義一種協議或框架&#xff0…

MySQL 簡記

MySQL 簡記 mysql中的數據存儲的結構是B樹 其與B樹的相同點是&#xff0c;B樹一個節點也可以存放多條數據&#xff0c;并且從左到右依次增大&#xff1b;不同點是&#xff0c;B樹的葉子結點之間也能相互連接。那么實際上是采取利用空間換區時間的策略。 那么B樹的樹結構like…

十七、實戰開發 uni-app x 項目(仿京東)- 后端指南

前面我們已經用uniappx進行了前端實戰學習 一、實戰 開發uni-app x項目(仿京東)-規劃-CSDN博客 二、實戰 開發uni-app x項目(仿京東)-項目搭建-CSDN博客 三、實戰開發 uni-app x 項目(仿京東)- 技術選型-CSDN博客 四、實戰開發 uni-app x 項目(仿京東)- 頁面設計-C…

Infura 簡介

文章目錄 Infura 簡介Infura 的主要功能Infura 的替代方案&#xff08;類似服務&#xff09;AlchemyQuickNodeAnkrMoralisPocket Network 什么時候選擇 Infura&#xff1f; Infura 簡介 Infura 是一個 區塊鏈基礎設施即服務&#xff08;BaaS, Blockchain as a Service&#xf…

TouchSocket TcpService:構建高性能Tcp服務的終極利器

這里寫目錄標題 TouchSocket TCPService&#xff1a;構建高性能TCP服務的終極利器引言TCPService核心特性快速入門&#xff1a;5分鐘搭建TCP服務1. 創建基礎TCP服務2. 自定義插件處理數據 高級用法實戰1. 客戶端連接管理 性能與穩定性保障示例與源碼結語 TouchSocket TCPServic…

Android Fresco 框架緩存模塊源碼深度剖析(二)

一、引言 在 Android 應用開發中&#xff0c;圖片加載和處理是常見且重要的功能。頻繁的圖片加載不僅會消耗大量的網絡流量&#xff0c;還會影響應用的性能和響應速度。因此&#xff0c;有效的緩存機制對于提升圖片加載效率和用戶體驗至關重要。Fresco 是 Facebook 開源的一款…

springboot使用163發送自定義html格式的郵件

springboot使用163發送html格式的郵件 效果: 下面直接開始教學 注冊郵箱&#xff0c;生成授權碼 獲取163郵箱的授權碼&#xff0c;可以按照以下步驟操作&#xff1a; 登錄163郵箱 打開瀏覽器&#xff0c;訪問 163郵箱登錄頁面。 使用你的郵箱賬號和密碼登錄。進入郵箱設置 登…

【Kafka】深入了解Kafka

集群的成員關系 Kafka使用Zookeeper維護集群的成員信息。 每一個broker都有一個唯一的標識&#xff0c;這個標識可以在配置文件中指定&#xff0c;也可以自動生成。當broker在啟動時通過創建Zookeeper的臨時節點把自己的ID注冊到Zookeeper中。broker、控制器和其他一些動態系…