中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁 > news >正文

寧波做網(wǎng)站的大公司排名關(guān)鍵詞搜索推廣排行榜

寧波做網(wǎng)站的大公司排名,關(guān)鍵詞搜索推廣排行榜,上海建設(shè)官方網(wǎng)站,蘇州網(wǎng)站推廣找蘇州夢(mèng)易行springboot3.x jwtspring security6.x實(shí)現(xiàn)用戶登錄認(rèn)證 什么是JWT JWT(JSON Web Token)是一種開放標(biāo)準(zhǔn)(RFC 7519),它用于在網(wǎng)絡(luò)應(yīng)用環(huán)境中傳遞聲明。通常,JWT用于身份驗(yàn)證和信息交換。JWT的一個(gè)典型用法是…

springboot3.x jwt+spring security6.x實(shí)現(xiàn)用戶登錄認(rèn)證

什么是JWT

JWT(JSON Web Token)是一種開放標(biāo)準(zhǔn)(RFC 7519),它用于在網(wǎng)絡(luò)應(yīng)用環(huán)境中傳遞聲明。通常,JWT用于身份驗(yàn)證和信息交換。JWT的一個(gè)典型用法是在Web應(yīng)用中實(shí)現(xiàn)基于Token的身份驗(yàn)證。

JWT 的特點(diǎn)

自包含(Self-contained):JWT 包含了用戶的認(rèn)證信息和其他相關(guān)的數(shù)據(jù),因此無需在服務(wù)器端存儲(chǔ)會(huì)話信息。它是通過加密或簽名的方式來保證數(shù)據(jù)的完整性和防止篡改。

JWT的結(jié)構(gòu)

JWT由三部分組成:

Header(頭部):包含令牌的類型(通常是JWT)和加密算法(如HMAC SHA256或RSA)。
Payload(負(fù)載):包含聲明(Claims)。這些聲明可以是關(guān)于實(shí)體(通常是用戶)和附加數(shù)據(jù)的斷言。例如,可以包含用戶的ID、角色、權(quán)限等。
Signature(簽名):用來驗(yàn)證消息的完整性以及發(fā)送者的身份。通過Header中的算法和密鑰生成簽名。

什么是spring Security

Spring Security 是一個(gè)功能強(qiáng)大且高度可定制的身份驗(yàn)證和訪問控制框架,通常用于保護(hù) Spring 應(yīng)用程序。它提供了各種安全功能,如:

認(rèn)證(Authentication):用于確認(rèn)用戶的身份。例如,檢查用戶名和密碼。
授權(quán)(Authorization):用于控制用戶是否有權(quán)限訪問某些資源。通常通過角色或權(quán)限進(jìn)行授權(quán)。
防護(hù)功能:提供 CSRF 防護(hù)、會(huì)話管理、跨站點(diǎn)請(qǐng)求偽造防護(hù)等安全功能。

Spring Security 是一個(gè)全面的安全框架,可以與多種身份驗(yàn)證機(jī)制集成,如基于表單的登錄、OAuth2、LDAP、JWT 等。

有了spring Security為什么還需要JWT

Spring Security 本身并不負(fù)責(zé)管理用戶登錄后如何保持會(huì)話的狀態(tài),這部分通常是通過 會(huì)話Session(以前的方式) 或 JWT 來完成的。

JWT 和 spring Security關(guān)系

Spring Security 和 JWT 之間并沒有直接的關(guān)系,但是它們可以很好地結(jié)合使用。在一個(gè)典型的現(xiàn)代 Web 應(yīng)用中,Spring Security 負(fù)責(zé)保護(hù)應(yīng)用的安全性,而 JWT 通常用于實(shí)現(xiàn)基于 token 的無狀態(tài)認(rèn)證。

Spring Boot 中的安全驗(yàn)證和存儲(chǔ)用戶信息

在Spring Boot應(yīng)用中,常用的安全驗(yàn)證方式是結(jié)合 Spring Security 和 JWT。Spring Security提供了多種身份驗(yàn)證機(jī)制,而JWT則用于實(shí)現(xiàn)無狀態(tài)的認(rèn)證方案。JWT通常在用戶登錄成功后生成,并作為后續(xù)請(qǐng)求的認(rèn)證憑證。

一般流程

  1. 用戶登錄:用戶通過用戶名和密碼發(fā)送請(qǐng)求給后端,后端驗(yàn)證用戶身份。
  2. 生成JWT:身份驗(yàn)證通過后,后端生成JWT,并將其發(fā)送給前端。JWT通常在HTTP響應(yīng)的Authorization頭中返回。
  3. 存儲(chǔ)JWT:前端將JWT存儲(chǔ)在本地(例如localStorage或sessionStorage)或Cookie中。
  4. 后續(xù)請(qǐng)求:每次前端向后端發(fā)起請(qǐng)求時(shí),JWT都會(huì)包含在請(qǐng)求頭的Authorization字段中,后端通過JWT驗(yàn)證用戶身份。
  5. 驗(yàn)證JWT:后端通過Spring Security中的過濾器解碼JWT,并驗(yàn)證簽名。如果JWT有效,用戶身份就會(huì)被確認(rèn)。

為什么說JWT是無狀態(tài)登錄

是因?yàn)樗氖褂梅绞讲恍枰诜?wù)器端存儲(chǔ)會(huì)話信息,也就是說,服務(wù)器不需要保存任何關(guān)于客戶端的狀態(tài)信息

有狀態(tài)和無狀態(tài)

有狀態(tài)認(rèn)證(Stateful Authentication)

在傳統(tǒng)的認(rèn)證機(jī)制中,服務(wù)器會(huì)維護(hù)一個(gè)會(huì)話(session)來記錄客戶端的身份信息。例如,在基于會(huì)話的認(rèn)證中,當(dāng)用戶成功登錄時(shí),服務(wù)器會(huì)創(chuàng)建一個(gè)會(huì)話并生成一個(gè)唯一的會(huì)話 ID。這個(gè)會(huì)話 ID 會(huì)存儲(chǔ)在服務(wù)器端的內(nèi)存或數(shù)據(jù)庫中,且會(huì)在客戶端的瀏覽器中以 Cookie 或其他方式保存(例如,JSESSIONID)。每次客戶端發(fā)起請(qǐng)求時(shí),都會(huì)攜帶會(huì)話 ID,服務(wù)器根據(jù)會(huì)話 ID 查找會(huì)話信息來確認(rèn)用戶身份。

問題:服務(wù)器需要維護(hù)會(huì)話狀態(tài)。每當(dāng)用戶發(fā)送請(qǐng)求時(shí),服務(wù)器需要查詢存儲(chǔ)的會(huì)話數(shù)據(jù)。這會(huì)增加服務(wù)器的負(fù)擔(dān),尤其是在分布式系統(tǒng)中,需要將會(huì)話數(shù)據(jù)共享或同步到不同的服務(wù)器節(jié)點(diǎn)。

無狀態(tài)認(rèn)證(Stateless Authentication)

與會(huì)話認(rèn)證不同,無狀態(tài)認(rèn)證不需要在服務(wù)器上存儲(chǔ)任何客戶端的會(huì)話信息。服務(wù)器驗(yàn)證身份時(shí),通過客戶端攜帶的信息(如 JWT)來驗(yàn)證請(qǐng)求,而不依賴于服務(wù)器端存儲(chǔ)的會(huì)話狀態(tài)。JWT 本身包含了足夠的信息來完成身份驗(yàn)證和授權(quán),服務(wù)器只需解析和驗(yàn)證這個(gè) token,而無需查詢?nèi)魏未鎯?chǔ)的信息。

為什么不手動(dòng)封裝json存儲(chǔ)

數(shù)據(jù)完整性與安全性

JWT: JWT 的一個(gè)核心特性是 簽名,它確保了數(shù)據(jù)的 完整性和安全性。JWT 中的簽名是由服務(wù)器的私鑰生成的,任何人如果修改了 JWT 的內(nèi)容,簽名就會(huì)變得無效,服務(wù)器就能發(fā)現(xiàn)數(shù)據(jù)被篡改。
例如,如果你將用戶信息存儲(chǔ)為 JSON,前端將其發(fā)送給服務(wù)器,那么攻擊者可能會(huì)篡改這個(gè) JSON 數(shù)據(jù)(如修改用戶角色、權(quán)限等),而服務(wù)器無法判斷數(shù)據(jù)是否被篡改,除非你做額外的安全處理(比如加密)。
自定義 JSON: 如果你讓前端封裝 JSON 并發(fā)送到服務(wù)器,服務(wù)器將無法知道數(shù)據(jù)是否被篡改。沒有類似 JWT 簽名的機(jī)制,前端發(fā)送的 JSON 數(shù)據(jù)就很容易被篡改。為了解決這個(gè)問題,你需要自己實(shí)現(xiàn)一些安全機(jī)制(如加密或自定義簽名),但這會(huì)增加實(shí)現(xiàn)的復(fù)雜度。

無狀態(tài)性與有效期管理

JWT:JWT 是自包含的,它不僅僅包含用戶信息,還可以包含 過期時(shí)間(exp)等信息。這樣,服務(wù)器可以非常容易地判斷一個(gè) JWT 是否過期,無需查詢數(shù)據(jù)庫或任何狀態(tài)。

自定義 JSON:如果前端僅僅發(fā)送一個(gè) JSON 數(shù)據(jù)包,服務(wù)器就無法知道該數(shù)據(jù)是否已經(jīng)過期。為了實(shí)現(xiàn)這一點(diǎn),你需要自己維護(hù)一個(gè)機(jī)制,比如在 JSON 中加入時(shí)間戳,或者在服務(wù)器端保存用戶的會(huì)話過期時(shí)間。這會(huì)導(dǎo)致服務(wù)器變得有狀態(tài),因?yàn)榉?wù)器需要管理每個(gè)用戶的會(huì)話生命周期。

服務(wù)器性能與分布式架構(gòu)支持

JWT:由于 JWT 是自包含的,所有的信息都在 token 中,服務(wù)器不需要存儲(chǔ)任何會(huì)話信息,這意味著它是 無狀態(tài)的。無狀態(tài)性在 分布式架構(gòu) 中特別有優(yōu)勢(shì),因?yàn)槎鄠€(gè)微服務(wù)可以獨(dú)立地驗(yàn)證 JWT 而無需共享會(huì)話數(shù)據(jù)。

自定義 JSON:如果你選擇自己封裝 JSON,并由前端存儲(chǔ)和發(fā)送,每次請(qǐng)求時(shí),服務(wù)器就需要檢查和驗(yàn)證這個(gè) JSON 信息。尤其是在分布式環(huán)境中,你可能需要通過共享會(huì)話存儲(chǔ)(如 Redis)來確保各個(gè)服務(wù)都能正確驗(yàn)證用戶身份,增加了管理的復(fù)雜性。

可擴(kuò)展性

JWT:由于 JWT 是標(biāo)準(zhǔn)化的,支持的庫和工具非常多,你可以很容易地實(shí)現(xiàn)不同平臺(tái)(如前端、后端、移動(dòng)端等)之間的認(rèn)證,而且不需要太多的額外工作。JWT 自帶的靈活性和規(guī)范使得它可以適應(yīng)多種需求。

自定義 JSON:如果你手動(dòng)封裝 JSON 并且實(shí)現(xiàn)自己的認(rèn)證機(jī)制,雖然在小范圍內(nèi)可以工作,但這種方法缺乏標(biāo)準(zhǔn)化和統(tǒng)一性,擴(kuò)展性差。在系統(tǒng)規(guī)模擴(kuò)大或需求變化時(shí),可能會(huì)遇到很多維護(hù)和兼容性問題。

實(shí)現(xiàn)

1.引入依賴

引入JWT依賴
        <!-- JWT library --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.11.2</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.2</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.2</version><scope>runtime</scope></dependency>
引入spring Security依賴
        <!-- Spring Security for Authentication --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>

2.實(shí)現(xiàn) UserDetailsService接口

UserDetailsService 是 Spring Security 中一個(gè)非常重要的接口,負(fù)責(zé)從數(shù)據(jù)庫或其他存儲(chǔ)中加載用戶的詳細(xì)信息,通常用于身份驗(yàn)證和授權(quán)過程。

在 Spring Security 中,用戶信息通常存儲(chǔ)在數(shù)據(jù)庫或其他外部系統(tǒng)中。為了進(jìn)行認(rèn)證和授權(quán),Spring Security 需要從這些存儲(chǔ)中獲取用戶的詳細(xì)信息,如用戶名、密碼、角色、權(quán)限等。為了實(shí)現(xiàn)這一點(diǎn),Spring Security 提供了 UserDetailsService 接口,它定義了如何加載這些用戶信息。

package com.example.demonew.service.loginService;import com.example.demonew.entity.UserInfo;
import com.example.demonew.mapper.loginMapper.LoginMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.stereotype.Service;import java.util.Map;@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate LoginMapper loginMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {UserInfo user = loginMapper.getUserInfo(username);if (user == null) {throw new UsernameNotFoundException("User not found");}//將信息添加到User也可以說是UserDetails 對(duì)象中return User.withUsername(user.getUsername()).password(user.getPassword()).roles(user.getRole()).build();}}
總結(jié)

實(shí)現(xiàn) UserDetailsService 接口的主要目的是將用戶的認(rèn)證信息(包括用戶名、密碼、角色等)從數(shù)據(jù)庫等存儲(chǔ)中提取出來,并轉(zhuǎn)化為 Spring Security 能夠理解并使用的 UserDetails 對(duì)象。在 Spring Security 的認(rèn)證和授權(quán)流程中,UserDetailsService 是用來提供用戶信息的核心組件。

當(dāng)用戶嘗試登錄時(shí),Spring Security 會(huì)調(diào)用 loadUserByUsername 方法來查找用戶,并使用返回的 UserDetails 來進(jìn)行身份驗(yàn)證和授權(quán)。因此,實(shí)現(xiàn) UserDetailsService 是將自定義的用戶數(shù)據(jù)源(如數(shù)據(jù)庫)與 Spring Security 進(jìn)行集成的關(guān)鍵步驟。

3.實(shí)現(xiàn)JWT工具類

這里工具類實(shí)現(xiàn)了,生成jwt token,解密jwt, 以及通過jwt獲取用戶名,驗(yàn)證令牌等功能,方便后續(xù)調(diào)用

package com.example.demonew.util;import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.security.Key;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;@Component
@Slf4j
public class JwtTokenProviderUtil {@Value("${app.jwt-secret}")private String jwtSecret;@Value("${app.jwt-expiration-milliseconds}")private long jwtExpirationDate;private Key key;// 使用懶加載方式獲取密鑰private Key getKey() {if (key == null) {synchronized (this) {if (key == null) {key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));}}}return key;}// 生成 JWT tokenpublic String generateToken(Authentication authentication){String username = authentication.getName();Date currentDate = new Date();Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);String token = Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(expireDate).signWith(getKey(),SignatureAlgorithm.HS256).compact();return token;}private Key key(){return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));}//解密tokenpublic String resolveToken(HttpServletRequest request) {String bearerToken = request.getHeader("Authorization");if (bearerToken != null && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7);}return null;}// 解析 JWT 令牌并返回其主體private Claims getClaimsFromToken(String token) {try {return Jwts.parserBuilder().setSigningKey(getKey()).build().parseClaimsJws(token).getBody();} catch (JwtException e) {log.error("Error parsing JWT token: {}", e.getMessage());throw new RuntimeException("JWT token parsing failed", e);}}// 驗(yàn)證 JWT 令牌public boolean validateToken(String token) {try{Jwts.parserBuilder().setSigningKey(getKey()).build().parseClaimsJws(token);return true;} catch (MalformedJwtException e) {log.error("Invalid JWT token: {}", e.getMessage());} catch (ExpiredJwtException e) {log.error("JWT token is expired: {}", e.getMessage());} catch (UnsupportedJwtException e) {log.error("JWT token is unsupported: {}", e.getMessage());} catch (IllegalArgumentException e) {log.error("JWT claims string is empty: {}", e.getMessage());}return false;}// 獲取用戶認(rèn)證信息public Authentication getAuthentication(String token) {String username = getUsername(token);List<GrantedAuthority> authorities = getAuthorities(token);return new UsernamePasswordAuthenticationToken(username, "", authorities);}//通過jwt信息獲取用戶名public String getUsername(String token) {Claims claims = getClaimsFromToken(token);String username = claims.getSubject();return username;}public List<GrantedAuthority> getAuthorities(String token) {Claims claims = getClaimsFromToken(token);List<String> roles = (List<String>)claims.get("roles");return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());}}
設(shè)置密鑰和過期時(shí)間
app:jwt-secret: 60fosjWhlsy6bxLjSv5P/8fvmvanEEAjUQm3KLkSuMc= # 密鑰 不唯一自己設(shè)置jwt-expiration-milliseconds: 604800000
隨機(jī)生成密鑰
    @Testvoid jwtSecretGenerator (){SecureRandom random = new SecureRandom();byte[] secret = new byte[32];  // 256 位random.nextBytes(secret);String jwtSecret = Base64.getEncoder().encodeToString(secret);  // 將字節(jié)數(shù)組轉(zhuǎn)換為 Base64 編碼字符串System.out.println(jwtSecret);}

4.spring Security配置類

Spring Security 配置類主要用于設(shè)置安全策略,過濾器鏈以及禁用默認(rèn)的 CSRF 等。
spring boot3.0廢棄了extends WebSecurityConfigurerAdapter的方式,所以這里采用添加@Bean新方式

SecurityConfig 是 Spring Security 的配置類,作用是配置 Spring Security 的安全策略,進(jìn)行身份認(rèn)證和授權(quán)管理

可以看出spring Security就是通過http來做跨域,權(quán)限等控制

package com.example.demonew.config;import com.example.demonew.service.loginService.CustomUserDetailsService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
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.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity  // 確保添加這個(gè)注解來啟用 Spring Security 配置
public class SecurityConfig {@Resourceprivate CustomUserDetailsService userDetailsService;@Autowiredprivate  JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {// 配置 HTTP 安全策略http.csrf(csrf -> csrf.disable()) // 由于你使用的是前后端分離的架構(gòu),CSRF 防護(hù)通常是不需要的。禁用 CSRF 防護(hù)可以避免 Spring Security 對(duì)跨站請(qǐng)求偽造攻擊的保護(hù),從而簡(jiǎn)化 API 調(diào)用。//授權(quán)配置.authorizeHttpRequests(authz -> authz.requestMatchers("/login").permitAll() // 允許所有用戶訪問 /user/login 路徑下的資源(通常是登錄接口)。其他接口需要認(rèn)證后訪問。.requestMatchers("/test/**").hasAuthority("ROLE_ADMIN") // 僅允許具有 ADMIN 角色的用戶訪問 /test/** 路徑下的資源。.anyRequest().authenticated() //所有其他請(qǐng)求都需要進(jìn)行身份認(rèn)證才能訪問。)//  jwtAuthenticationFilter是一個(gè)自定義的過濾器,它負(fù)責(zé)處理請(qǐng)求中的 JWT 認(rèn)證。此過濾器被配置為在 Spring Security 自帶的 UsernamePasswordAuthenticationFilter 之前執(zhí)行,這意味著在 Spring Security 處理用戶名和密碼認(rèn)證之前,會(huì)先進(jìn)行 JWT 校驗(yàn)。.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class).userDetailsService(userDetailsService) //配置自定義的 UserDetailsService 用于加載用戶信息.formLogin(form -> form.disable()) // Spring Security 默認(rèn)啟用基于表單的登錄認(rèn)證。如果你使用的是前后端分離的方式,通常不會(huì)使用傳統(tǒng)的表單登錄,因此禁用它。.logout(logout -> logout.permitAll()); // 配置登出功能,允許所有用戶進(jìn)行登出。return http.build();  // 返回配置好的 SecurityFilterChain}/*這個(gè)方法配置了 Spring Security 的 AuthenticationManager,它是進(jìn)行用戶認(rèn)證的核心組件。配置了一個(gè) DaoAuthenticationProvider,它使用 CustomUserDetailsService 來加載用戶信息,使用 BCryptPasswordEncoder 進(jìn)行密碼驗(yàn)證。DaoAuthenticationProvider:是 Spring Security 用于基于數(shù)據(jù)庫的用戶認(rèn)證提供程序,它會(huì)從 UserDetailsService 中加載用戶信息,并使用密碼編碼器對(duì)用戶密碼進(jìn)行校驗(yàn)。UserDetailsService:CustomUserDetailsService 實(shí)現(xiàn)了 UserDetailsService 接口,用于加載用戶信息(例如,用戶名、密碼、角色等)。它通常會(huì)從數(shù)據(jù)庫中查詢用戶信息。PasswordEncoder:BCryptPasswordEncoder 用于對(duì)用戶輸入的密碼進(jìn)行加密,然后與存儲(chǔ)在數(shù)據(jù)庫中的加密密碼進(jìn)行對(duì)比。BCrypt 是一種常見的密碼加密算法。返回 AuthenticationManager:該管理器負(fù)責(zé)執(zhí)行認(rèn)證流程,返回經(jīng)過認(rèn)證的 Authentication 對(duì)象。*/@Beanpublic AuthenticationManager authenticationManager(UserDetailsService userDetailsService,PasswordEncoder passwordEncoder) {DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();authenticationProvider.setUserDetailsService(userDetailsService); // 配置 UserDetailsService,用于加載用戶信息authenticationProvider.setPasswordEncoder(passwordEncoder); // 配置密碼編碼器,用于密碼校驗(yàn)return new ProviderManager(authenticationProvider); // 返回一個(gè) ProviderManager,管理多個(gè)認(rèn)證提供者}/*該方法返回一個(gè) BCryptPasswordEncoder 實(shí)例,用于對(duì)用戶的密碼進(jìn)行加密和驗(yàn)證。BCryptPasswordEncoder 是一種安全的密碼加密算法,Spring Security 默認(rèn)推薦使用這種加密方式。*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}//    @Bean
//    public SecurityContextRepository securityContextRepository() {
//        return new HttpSessionSecurityContextRepository();
//    }}

5.創(chuàng)建JWT 過濾器

創(chuàng)建一個(gè)過濾器來攔截每個(gè)請(qǐng)求,提取 JWT,并驗(yàn)證它。過濾器會(huì)檢查請(qǐng)求頭是否帶有 JWT,如果有,它會(huì)驗(yàn)證 JWT 并通過 Spring Security 認(rèn)證上下文設(shè)置用戶身份。

JwtAuthenticationFilter 是一個(gè)用于處理 JWT(JSON Web Token)身份驗(yàn)證的過濾器類,繼承自 Spring Security 的 OncePerRequestFilter。它的作用是在每次請(qǐng)求時(shí),檢查請(qǐng)求頭中的 JWT token,并進(jìn)行校驗(yàn),如果 token 驗(yàn)證通過,則根據(jù) token 中的信息設(shè)置 Spring Security 的 Authentication,確保用戶能夠通過 JWT 認(rèn)證來訪問受保護(hù)的資源。

package com.example.demonew.config;import com.example.demonew.service.loginService.LoginService;
import com.example.demonew.util.JwtTokenProviderUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtTokenProviderUtil jwtTokenProvider;@Autowiredprivate UserDetailsService userDetailsService;/*這是過濾器的核心方法,它會(huì)在每次 HTTP 請(qǐng)求時(shí)被調(diào)用。方法中的處理流程如下:*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {// 通過 getTokenFromRequest(request) 方法,嘗試從 HTTP 請(qǐng)求的 Authorization 頭中提取 JWT。如果 token 存在且以 Bearer 開頭,則提取出 token 部分。String token = getTokenFromRequest(request);/* 校驗(yàn) token使用 jwtTokenProvider.validateToken(token) 校驗(yàn) JWT 的合法性。例如,檢查 token 是否過期、是否篡改等。如果 token 無效或過期,認(rèn)證過程會(huì)被跳過,后續(xù)請(qǐng)求會(huì)被拒絕。*/if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)){try {// 從 token 獲取 usernameString username = jwtTokenProvider.getUsername(token);// 加載與 token 關(guān)聯(lián)的用戶UserDetails userDetails = userDetailsService.loadUserByUsername(username);/** 創(chuàng)建一個(gè) UsernamePasswordAuthenticationToken 實(shí)例,傳入 userDetails 和該用戶的權(quán)限(userDetails.getAuthorities())。* UsernamePasswordAuthenticationToken 是 Spring Security 用來封裝用戶身份信息的一個(gè)對(duì)象,它包含了用戶的身份信息和權(quán)限。* */UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));/** 使用 SecurityContextHolder.getContext().setAuthentication(authenticationToken) 將身份認(rèn)證信息(即 authenticationToken)存儲(chǔ)在 Spring Security 的上下文中。* 這樣,Spring Security 在處理后續(xù)請(qǐng)求時(shí),就能基于這個(gè)認(rèn)證信息對(duì)請(qǐng)求進(jìn)行授權(quán)控制。* */SecurityContextHolder.getContext().setAuthentication(authenticationToken);}catch (Exception e){// 記錄日志并清除認(rèn)證上下文log.warn("JWT Token validation failed: " + e.getMessage());SecurityContextHolder.clearContext();}}else {// 無 token 或 token 無效,清除認(rèn)證上下文logger.warn("JWT Token is missing or invalid");SecurityContextHolder.clearContext();}/*最后,調(diào)用 filterChain.doFilter(request, response) 讓請(qǐng)求繼續(xù)傳遞給下一個(gè)過濾器或最終的目標(biāo)(如 Controller)。這一步是確保請(qǐng)求能夠正常進(jìn)入應(yīng)用的下一階段*/filterChain.doFilter(request, response);}// 從請(qǐng)求頭獲取 JWT 格式為:Authorization: Bearer <token>private String getTokenFromRequest(HttpServletRequest request) {String bearerToken = request.getHeader("Authorization");if (bearerToken != null && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7); // 去掉 "Bearer " 前綴}return null;}
}
為什么使用 OncePerRequestFilter

OncePerRequestFilter 是 Spring 提供的一個(gè)過濾器基類,確保每次請(qǐng)求只會(huì)調(diào)用一次過濾器的 doFilterInternal 方法,而不是每個(gè)過濾器鏈都執(zhí)行一次。這可以有效避免重復(fù)執(zhí)行相同的邏輯,保證 JWT 校驗(yàn)只執(zhí)行一次。

創(chuàng)建登錄接口

這樣用戶先通過用戶名,密碼進(jìn)行登錄,然后校驗(yàn)信息,返回userDetail對(duì)象

package com.example.demonew.controller.login;import com.example.demonew.common.Result;
import com.example.demonew.config.SecurityConfig;
import com.example.demonew.service.loginService.LoginService;
import com.example.demonew.util.JwtTokenProviderUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
@RequestMapping("/user")
public class LoginController {@Autowiredprivate LoginService loginService;@Autowiredprivate JwtTokenProviderUtil jwtTokenProviderUtil;@Autowiredprivate AuthenticationManager authenticationManager;@PostMapping("/login")public Result Login(@RequestBody LoginRequest loginRequest){loginService.login(loginRequest.getUsername(),loginRequest.getPassword());// 登錄授權(quán)Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(),loginRequest.getPassword()));// 將登錄用戶信息交給SpringSecurity管理SecurityContextHolder.getContext().setAuthentication(authentication);// 利用用戶授權(quán)信息生成JWT令牌String token = jwtTokenProviderUtil.generateToken(authentication);return Result.success(token);};@PostMapping("/register")public Result registerUser(@RequestBody LoginRequest loginRequest) {BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();String encodedPassword = bCryptPasswordEncoder.encode(loginRequest.password);String password=encodedPassword;loginService.register(loginRequest.getUsername(),password);return new Result();}// 登錄請(qǐng)求體的封裝類public static class LoginRequest {private String username;private String password;// getters and setterspublic String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}}

準(zhǔn)備數(shù)據(jù)

在這里插入圖片描述
密碼這里先手動(dòng)生成一個(gè)

    @Testvoid testBCryptPasswordEncoder(){BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();String encode = encoder.encode("123456");System.out.println(encode);}

postman測(cè)試

登錄

登錄不需要帶Authorization信息,登錄后jwt會(huì)生成token返回,后續(xù)請(qǐng)求需要帶上
在這里插入圖片描述

其他接口

沒權(quán)限 未帶認(rèn)證
在這里插入圖片描述

帶上認(rèn)證成功訪問
在這里插入圖片描述

錯(cuò)誤總結(jié)

執(zhí)行filterChain.doFilter(request, response);后無法進(jìn)入controller

解決

檢查securityFilterChain方法中的requestMatchers方法,url是否正確
requestMatchers(“/user/login”).permitAll() // 開放登錄接口

Caused by: java.lang.IllegalArgumentException: A UserDetailsService must be set

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0: Error creating bean with name 'securityFilterChain' defined in class path resource [com/example/demonew/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: Could not postProcess org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a of type class org.springframework.security.authentication.dao.DaoAuthenticationProviderat org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:896)at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:849)at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:146)at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:509)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1441)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:336)at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:289)at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:334)at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)at org.springframework.beans.factory.support.DefaultListableBeanFactory.instantiateSingleton(DefaultListableBeanFactory.java:1122)at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingleton(DefaultListableBeanFactory.java:1093)at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1030)at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:987)at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:627)at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752)at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439)at org.springframework.boot.SpringApplication.run(SpringApplication.java:318)at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361)at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350)at com.example.demonew.DemoNewApplication.main(DemoNewApplication.java:16)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'securityFilterChain' defined in class path resource [com/example/demonew/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: Could not postProcess org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a of type class org.springframework.security.authentication.dao.DaoAuthenticationProviderat org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:657)at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:645)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1357)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1187)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:563)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:336)at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:289)at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:334)at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)at org.springframework.beans.factory.support.DefaultListableBeanFactory.addCandidateEntry(DefaultListableBeanFactory.java:1883)at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1847)at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveMultipleBeanCollection(DefaultListableBeanFactory.java:1737)at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveMultipleBeans(DefaultListableBeanFactory.java:1705)at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1580)at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1519)at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:888)... 22 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: Could not postProcess org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a of type class org.springframework.security.authentication.dao.DaoAuthenticationProviderat org.springframework.beans.factory.support.SimpleInstantiationStrategy.lambda$instantiate$0(SimpleInstantiationStrategy.java:199)at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiateWithFactoryMethod(SimpleInstantiationStrategy.java:88)at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:168)at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653)... 39 common frames omitted
Caused by: java.lang.RuntimeException: Could not postProcess org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a of type class org.springframework.security.authentication.dao.DaoAuthenticationProviderat org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor.postProcess(AutowireBeanFactoryObjectPostProcessor.java:71)at org.springframework.security.config.annotation.SecurityConfigurerAdapter$CompositeObjectPostProcessor.postProcess(SecurityConfigurerAdapter.java:130)at org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess(SecurityConfigurerAdapter.java:82)at org.springframework.security.config.annotation.authentication.configurers.userdetails.AbstractDaoAuthenticationConfigurer.configure(AbstractDaoAuthenticationConfigurer.java:96)at org.springframework.security.config.annotation.authentication.configurers.userdetails.AbstractDaoAuthenticationConfigurer.configure(AbstractDaoAuthenticationConfigurer.java:36)at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.configure(AbstractConfiguredSecurityBuilder.java:398)at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:352)at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38)at org.springframework.security.config.annotation.web.builders.HttpSecurity.beforeConfigure(HttpSecurity.java:3301)at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:351)at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38)at com.example.demonew.config.SecurityConfig.securityFilterChain(SecurityConfig.java:49)at com.example.demonew.config.SecurityConfig$$SpringCGLIB$$0.CGLIB$securityFilterChain$2(<generated>)at com.example.demonew.config.SecurityConfig$$SpringCGLIB$$FastClass$$1.invoke(<generated>)at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258)at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:348)at com.example.demonew.config.SecurityConfig$$SpringCGLIB$$0.securityFilterChain(<generated>)at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)at java.base/java.lang.reflect.Method.invoke(Method.java:580)at org.springframework.beans.factory.support.SimpleInstantiationStrategy.lambda$instantiate$0(SimpleInstantiationStrategy.java:171)... 42 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a': A UserDetailsService must be setat org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:413)at org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor.initializeBeanIfNeeded(AutowireBeanFactoryObjectPostProcessor.java:98)at org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor.postProcess(AutowireBeanFactoryObjectPostProcessor.java:67)... 61 common frames omitted
Caused by: java.lang.IllegalArgumentException: A UserDetailsService must be setat org.springframework.util.Assert.notNull(Assert.java:181)at org.springframework.security.authentication.dao.DaoAuthenticationProvider.doAfterPropertiesSet(DaoAuthenticationProvider.java:99)at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.afterPropertiesSet(AbstractUserDetailsAuthenticationProvider.java:119)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1855)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804)... 64 common frames omitted
Disconnected from the target VM, address: '127.0.0.1:59351', transport: 'socket'Process finished with exit code 1
解決

2025-01-14 11:59:16.898 WARN — [nio-8080-exec-2] o.s.s.c.bcrypt.BCryptPasswordEncoder : Encoded password does not look like BCrypt

解決

密碼格式不正確,確保數(shù)據(jù)庫中的密碼使用 BCrypt 加密

        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();String encode = encoder.encode("123456");

TemplateInputException

詳細(xì)信息如下

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.thymeleaf.exceptions.TemplateInputException: Error resolving template [user/login], template might not exist or might not be accessible by any of the configured Template Resolvers] with root cause
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [user/login], template might not exist or might not be accessible by any of the configured Template Resolvers

在返回return時(shí)候報(bào)錯(cuò)

解決

在 pom.xml 中刪除 Thymeleaf 依賴(如果不使用 Thymeleaf)

原因

Spring Boot 默認(rèn)渲染視圖:

當(dāng)你使用 Spring Boot 啟動(dòng)項(xiàng)目時(shí),如果沒有明確聲明返回類型,Spring Boot 會(huì)默認(rèn)嘗試將響應(yīng)返回為視圖(view)頁面,而不是直接返回 JSON 或其他格式的數(shù)據(jù)。
在你的代碼中,login 方法返回的是 Result.success(token),這個(gè) token 是你生成的 JWT,應(yīng)該作為 API 的響應(yīng)數(shù)據(jù)返回,而不是試圖通過 Thymeleaf 渲染 user/login 模板。
返回的是數(shù)據(jù),而非視圖:

如果你的 login 是一個(gè) RESTful 風(fēng)格的 API,應(yīng)該返回 JSON 數(shù)據(jù)而不是試圖渲染頁面。Spring 默認(rèn)會(huì)將沒有指定視圖的請(qǐng)求作為視圖解析,導(dǎo)致它去找 user/login.html。

http://www.risenshineclean.com/news/44103.html

相關(guān)文章:

  • 如何免費(fèi)網(wǎng)站建設(shè)怎么可以在百度發(fā)布信息
  • 做網(wǎng)站運(yùn)營(yíng)的女生多嗎百度公司簡(jiǎn)介
  • 上海小微企業(yè)名錄查詢seo短視頻網(wǎng)頁入口營(yíng)銷
  • 公司網(wǎng)站建設(shè)北京北京做百度推廣的公司
  • 網(wǎng)站開發(fā)用啥語言廣州各區(qū)正在進(jìn)一步優(yōu)化以下措施
  • 順德做網(wǎng)站公司seo搜狗
  • 網(wǎng)站后端怎么做河北網(wǎng)站建設(shè)公司排名
  • 手機(jī)模板網(wǎng)站模板鞏義網(wǎng)站優(yōu)化公司
  • 網(wǎng)頁的動(dòng)態(tài)效果網(wǎng)店關(guān)鍵詞怎么優(yōu)化
  • 自己做網(wǎng)站需要服務(wù)器培訓(xùn)機(jī)構(gòu)查詢網(wǎng)
  • 自助申請(qǐng)海外網(wǎng)站長(zhǎng)沙網(wǎng)絡(luò)公關(guān)公司
  • 傳統(tǒng)營(yíng)銷渠道有哪些seo網(wǎng)站排名優(yōu)化培訓(xùn)教程
  • 網(wǎng)站建設(shè)及推廣銷售話術(shù)新app推廣方案
  • 衡陽公司做網(wǎng)站關(guān)鍵詞分類
  • 北京好的網(wǎng)站開發(fā)網(wǎng)站推廣 方法
  • 北京十佳網(wǎng)站建設(shè)廣告網(wǎng)站大全
  • 唐山住房和城鄉(xiāng)建設(shè)廳網(wǎng)站谷歌外貿(mào)seo
  • 景區(qū)網(wǎng)站建設(shè)教程如何免費(fèi)發(fā)布廣告
  • 網(wǎng)站開發(fā)公司總匯seo基礎(chǔ)知識(shí)培訓(xùn)視頻
  • 購物商城開發(fā)seo優(yōu)化設(shè)計(jì)
  • 大連重工 央企江西seo推廣軟件
  • 音樂網(wǎng)站開發(fā)的目的杭州百度推廣代理公司哪家好
  • 網(wǎng)站建設(shè)教程 pdf營(yíng)銷型網(wǎng)站建設(shè)論文
  • 電子商務(wù)網(wǎng)站建設(shè)模板代碼互聯(lián)網(wǎng)廣告銷售是做什么的
  • WordPress的目錄大綱杭州百度快照優(yōu)化排名
  • 高密市網(wǎng)站建設(shè)鄭州網(wǎng)站seo顧問
  • logo免費(fèi)設(shè)計(jì)無水印seo搜索工具欄
  • 山東政務(wù)網(wǎng)站建設(shè)seo和sem的聯(lián)系
  • 無錫網(wǎng)站建設(shè) app百度的搜索引擎優(yōu)化
  • 網(wǎng)站備案查詢api逆冬seo