SpringBoot3+SpringSecurity6前后端分離的項目實踐
網(wǎng)上能找到的SpringBoot項目一般都是SpringBoot2 + SpringSecurity5,甚至是SSM的項目。這些老版本的教程很多已經(jīng)不適用了,對于現(xiàn)在大部分的初學者來說,學了可能也是經(jīng)典白雪。我還是不愿學那些老版本的東西,所以自己摸索了一下新版的SpringBoot項目應該怎么寫。學習的過程也是非常折磨人的,看了很多的教程才知道個大概。
導入依賴
SpringSecurity依賴
<!--SpringSecurity起步依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>JWT依賴
<!--jwt令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>添加配置類
對Security進行配置,Security中很多的默認配置都可以用自定義的替換。
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
/**
* @Description: SpringSecurity配置類
* @Author: 翰戈.summer
* @Date: 2023/11/17
* @Param:
* @Return:
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
/**
* 加載用戶信息
*/
@Bean
public UserDetailsService userDetailsService() {
return userDetailsService;
}
/**
* 密碼編碼器
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份驗證管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
/**
* 處理身份驗證
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
return daoAuthenticationProvider;
}
/**
* @Description: 配置SecurityFilterChain過濾器鏈
* @Author: 翰戈.summer
* @Date: 2023/11/17
* @Param: HttpSecurity
* @Return: SecurityFilterChain
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
.requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登錄放行
.anyRequest().authenticated()
);
httpSecurity.authenticationProvider(authenticationProvider());
//禁用登錄頁面
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
//禁用登出頁面
httpSecurity.logout(AbstractHttpConfigurer::disable);
//禁用session
httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
//禁用httpBasic
httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
//禁用csrf保護
httpSecurity.csrf(AbstractHttpConfigurer::disable);
return httpSecurity.build();
}
}實現(xiàn)UserDetailsService
其中UserMapper、AuthorityMapper需要自己創(chuàng)建,不是重點。這兩個Mapper的作用是獲取用戶信息(用戶名、密碼、用戶權限),封裝到User中返回給Security。
import com.demo.mapper.AuthorityMapper;
import com.demo.mapper.UserMapper;
import com.demo.pojo.AuthorityEntity;
import com.demo.pojo.UserEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.AuthorityUtils;
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.List;
import java.util.StringJoiner;
/**
* @Description: 用戶登錄
* @Author: 翰戈.summer
* @Date: 2023/11/16
* @Param:
* @Return:
*/
@Service
@RequiredArgsConstructor
public class UserLoginDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
private final AuthorityMapper authorityMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userMapper.selectUserByUsername(username);
List<AuthorityEntity> authorities = authorityMapper.selectAuthorityByUsername(username);
StringJoiner stringJoiner = new StringJoiner(",", "", "");
authorities.forEach(authority -> stringJoiner.add(authority.getAuthorityName()));
return new User(userEntity.getUsername(), userEntity.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList(stringJoiner.toString())
);
}
}實現(xiàn)UserDetails
登錄操作會用到UserDetails,用于獲取用戶名和權限。
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @Description: SpringSecurity用戶實體類
* @Author: 翰戈.summer
* @Date: 2023/11/18
* @Param:
* @Return:
*/
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsEntity implements UserDetails {
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String toString() {
return "UserDetailsEntity{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", authorities=" + authorities +
'}';
}
}JWT工具類
生成 jwt令牌 或解析,其中的JwtProperties(jwt令牌配置屬性類)可以自己創(chuàng)建,不是重點。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
/**
* @Description: 生成和解析jwt令牌
* @Author: 翰戈.summer
* @Date: 2023/11/16
* @Param:
* @Return:
*/
@Component
@RequiredArgsConstructor
public class JwtUtils {
private final JwtProperties jwtProperties;
/**
* @Description: 生成令牌
* @Author: 翰戈.summer
* @Date: 2023/11/16
* @Param: Map
* @Return: String jwt
*/
public String getJwt(Map<String, Object> claims) {
String signingKey = jwtProperties.getSigningKey();
Long expire = jwtProperties.getExpire();
return Jwts.builder()
.setClaims(claims) //設置載荷內(nèi)容
.signWith(SignatureAlgorithm.HS256, signingKey) //設置簽名算法
.setExpiration(new Date(System.currentTimeMillis() + expire)) //設置有效時間
.compact();
}
/**
* @Description: 解析令牌
* @Author: 翰戈.summer
* @Date: 2023/11/16
* @Param: String jwt
* @Return: Claims claims
*/
public Claims parseJwt(String jwt) {
String signingKey = jwtProperties.getSigningKey();
return Jwts.parser()
.setSigningKey(signingKey) //指定簽名密鑰
.parseClaimsJws(jwt) //開始解析令牌
.getBody();
}
}登錄接口
用戶登錄成功并返回 jwt令牌,Result為統(tǒng)一響應的結果,UserLoginDTO用于封裝用戶登錄信息,其中的UserDetails必須實現(xiàn)后才能獲取到用戶信息。
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
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.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 用戶登錄操作相關接口
* @Author: 翰戈.summer
* @Date: 2023/11/20
* @Param:
* @Return:
*/
@RestController
@RequestMapping("/api/user/login")
@RequiredArgsConstructor
public class UserLoginController {
private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
@PostMapping
public Result<String> doLogin(@RequestBody UserLoginDTO userLoginDTO) {
try {
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userLoginDTO.getUsername(), userLoginDTO.getPassword());
Authentication authentication = authenticationManager.authenticate(auth);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
//獲取用戶權限信息
String authorityString = "";
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
for (GrantedAuthority authority : authorities) {
authorityString = authority.getAuthority();
}
//用戶身份驗證成功,生成并返回jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
claims.put("authorityString", authorityString);
String jwtToken = jwtUtils.getJwt(claims);
return Result.success(jwtToken);
} catch (Exception ex) {
//用戶身份驗證失敗,返回登陸失敗提示
return Result.error("用戶名或密碼錯誤!");
}
}
}自定義token過濾器
過濾器中拋出的異常是不會被全局異常處理器捕獲到的,直接返回錯誤結果。這里用到了SpringContextUtils通過上下文來獲取Bean組件,下面會提供。
過濾器屬于Servlet(作用范圍更大),攔截器屬于SpringMVC(作用范圍較?。之惓L幚砥髦荒懿东@到攔截器中的異常。在過濾器中無法初始化Bean組件,可以通過上下文來獲取。
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.Collections;
/**
* @Description: 自定義token驗證過濾器,驗證成功后將用戶信息放入SecurityContext上下文
* @Author: 翰戈.summer
* @Date: 2023/11/18
* @Param:
* @Return:
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
try {
//獲取請求頭中的token
String jwtToken = request.getHeader("token");
if (!StringUtils.hasLength(jwtToken)) {
//token不存在,交給其他過濾器處理
filterChain.doFilter(request, response);
return; //結束方法
}
//過濾器中無法初始化Bean組件,使用上下文獲取
JwtUtils jwtUtils = SpringContextUtils.getBean("jwtUtils");
if (jwtUtils == null) {
throw new RuntimeException();
}
//解析jwt令牌
Claims claims;
try {
claims = jwtUtils.parseJwt(jwtToken);
} catch (Exception ex) {
throw new RuntimeException();
}
//獲取用戶信息
String username = (String) claims.get("username"); //用戶名
String authorityString = (String) claims.get("authorityString"); //權限信息
Authentication authentication = new UsernamePasswordAuthenticationToken(
username, null,
Collections.singleton(new SimpleGrantedAuthority(authorityString))
);
//將用戶信息放入SecurityContext上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (Exception ex) {
//過濾器中拋出的異常無法被全局異常處理器捕獲,直接返回錯誤結果
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
String value = new ObjectMapper().writeValueAsString(Result.error("用戶未登錄!"));
response.getWriter().write(value);
}
}
}SpringContextUtils工具類
import jakarta.annotation.Nonnull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @Description: 用于創(chuàng)建上下文,實現(xiàn)ApplicationContextAware接口
* @Author: 翰戈.summer
* @Date: 2023/11/17
* @Param:
* @Return:
*/
@Component
public class SpringContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@Override
public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
if (applicationContext == null) {
return null;
}
return (T) applicationContext.getBean(name);
}
}添加自定義token驗證過濾器
將自定義token驗證過濾器,添加到UsernamePasswordAuthenticationFilter前面。
UsernamePasswordAuthenticationFilter實現(xiàn)了基于用戶名和密碼的認證邏輯,我們利用token進行身份驗證,所以用不到這個過濾器。
/**
* @Description: 配置SecurityFilterChain過濾器鏈
* @Author: 翰戈.summer
* @Date: 2023/11/17
* @Param: HttpSecurity
* @Return: SecurityFilterChain
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
.requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登錄放行
.anyRequest().authenticated()
);
httpSecurity.authenticationProvider(authenticationProvider());
//禁用登錄頁面
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
//禁用登出頁面
httpSecurity.logout(AbstractHttpConfigurer::disable);
//禁用session
httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
//禁用httpBasic
httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
//禁用csrf保護
httpSecurity.csrf(AbstractHttpConfigurer::disable);
//通過上下文獲取AuthenticationManager
AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager");
//添加自定義token驗證過濾器
httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}自定義用戶未登錄的處理
用戶請求未攜帶token的處理,替換AuthenticationEntryPoint
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @Description: 自定義用戶未登錄的處理(未攜帶token)
* @Author: 翰戈.summer
* @Date: 2023/11/19
* @Param:
* @Return:
*/
@Component
public class AuthEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
String value = new ObjectMapper().writeValueAsString(Result.error("未攜帶token!"));
response.getWriter().write(value);
}
}自定義用戶權限不足的處理
用戶權限不足的處理,替換AccessDeniedHandler
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @Description: 自定義用戶權限不足的處理
* @Author: 翰戈.summer
* @Date: 2023/11/19
* @Param:
* @Return:
*/
@Component
public class AuthAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
String value = new ObjectMapper().writeValueAsString(Result.error("權限不足!"));
response.getWriter().write(value);
}
}添加自定義處理器
修改 SecurityConfig 配置類,注入 AuthAccessDeniedHandler 和 AuthEntryPointHandler
/**
* @Description: 配置SecurityFilterChain過濾器鏈
* @Author: 翰戈.summer
* @Date: 2023/11/17
* @Param: HttpSecurity
* @Return: SecurityFilterChain
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
.requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登錄放行
.anyRequest().authenticated()
);
httpSecurity.authenticationProvider(authenticationProvider());
//禁用登錄頁面
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
//禁用登出頁面
httpSecurity.logout(AbstractHttpConfigurer::disable);
//禁用session
httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
//禁用httpBasic
httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
//禁用csrf保護
httpSecurity.csrf(AbstractHttpConfigurer::disable);
//通過上下文獲取AuthenticationManager
AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager");
//添加自定義token驗證過濾器
httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
//自定義處理器
httpSecurity.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler(authAccessDeniedHandler) //處理用戶權限不足
.authenticationEntryPoint(authEntryPointHandler) //處理用戶未登錄(未攜帶token)
);
return httpSecurity.build();
}靜態(tài)資源放行
SpringBoot3 中使用 Swagger3 接口文檔,在整合了 SpringSecurity 后會出現(xiàn)無法訪問的情況,需要給靜態(tài)資源放行。
在 SecurityConfig 中添加
/**
* 靜態(tài)資源放行
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers(
"/doc.html",
"/doc.html/**",
"/v3/api-docs",
"/v3/api-docs/**",
"/webjars/**",
"/authenticate",
"/swagger-ui.html/**",
"/swagger-resources",
"/swagger-resources/**"
);
}總結
SpringSecurity6 的用法和以前版本的有較大差別,比如WebSecurityConfigurerAdapter的廢除,看到配置類繼承了這個的都是過時的教程。因為不再繼承,所以不能通過重寫方法的方式去配置。另外很多配置的方式都變成使用Lambda表達式,或者是方法引用。
到此這篇關于SpringBoot3+SpringSecurity6前后端分離的項目實踐的文章就介紹到這了,更多相關SpringBoot3+SpringSecurity6前后端分離內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
IDEA中Spring Initializr沒有Java8選項的解決辦法
在使用IDEA中的Spring Initializr創(chuàng)建新項目時,Java 版本近可選擇Java17,21 ,不能選擇Java8;SpringBoot 版本也只有 3.x,所以本文給大家介紹了IDEA中Spring Initializr沒有Java8選項的解決辦法,需要的朋友可以參考下2024-06-06
Spring Boot + MyBatis Plus 高效開發(fā)實戰(zhàn)從入
本文將詳細介紹 Spring Boot + MyBatis Plus 的完整開發(fā)流程,并深入剖析分頁查詢、批量操作、動態(tài) SQL、樂觀鎖、代碼優(yōu)化等實戰(zhàn)技巧,感興趣的朋友一起看看吧2025-04-04
Mybatis如何傳入多個參數(shù)(實體類型和基本類型)
這篇文章主要介紹了Mybatis如何傳入多個參數(shù)(實體類型和基本類型),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06
JVM---jstack分析Java線程CPU占用,線程死鎖的解決
這篇文章主要介紹了JVM---jstack分析Java線程CPU占用,線程死鎖的解決,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09

