SpringBoot3+SpringSecurity6前后端分離的項(xiàng)目實(shí)踐
網(wǎng)上能找到的SpringBoot項(xiàng)目一般都是SpringBoot2 + SpringSecurity5,甚至是SSM的項(xiàng)目。這些老版本的教程很多已經(jīng)不適用了,對(duì)于現(xiàn)在大部分的初學(xué)者來說,學(xué)了可能也是經(jīng)典白雪。我還是不愿學(xué)那些老版本的東西,所以自己摸索了一下新版的SpringBoot項(xiàng)目應(yīng)該怎么寫。學(xué)習(xí)的過程也是非常折磨人的,看了很多的教程才知道個(gè)大概。
導(dǎo)入依賴
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>
添加配置類
對(duì)Security進(jìn)行配置,Security中很多的默認(rèn)配置都可以用自定義的替換。
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(); } /** * 身份驗(yàn)證管理器 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } /** * 處理身份驗(yàn)證 */ @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保護(hù) httpSecurity.csrf(AbstractHttpConfigurer::disable); return httpSecurity.build(); } }
實(shí)現(xiàn)UserDetailsService
其中UserMapper、AuthorityMapper需要自己創(chuàng)建,不是重點(diǎn)。這兩個(gè)Mapper的作用是獲取用戶信息(用戶名、密碼、用戶權(quán)限),封裝到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()) ); } }
實(shí)現(xiàn)UserDetails
登錄操作會(huì)用到UserDetails,用于獲取用戶名和權(quán)限。
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用戶實(shí)體類 * @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)建,不是重點(diǎn)。
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) //設(shè)置載荷內(nèi)容 .signWith(SignatureAlgorithm.HS256, signingKey) //設(shè)置簽名算法 .setExpiration(new Date(System.currentTimeMillis() + expire)) //設(shè)置有效時(shí)間 .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)一響應(yīng)的結(jié)果,UserLoginDTO用于封裝用戶登錄信息,其中的UserDetails必須實(shí)現(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: 用戶登錄操作相關(guān)接口 * @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(); //獲取用戶權(quán)限信息 String authorityString = ""; Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); for (GrantedAuthority authority : authorities) { authorityString = authority.getAuthority(); } //用戶身份驗(yàn)證成功,生成并返回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) { //用戶身份驗(yàn)證失敗,返回登陸失敗提示 return Result.error("用戶名或密碼錯(cuò)誤!"); } } }
自定義token過濾器
過濾器中拋出的異常是不會(huì)被全局異常處理器捕獲到的,直接返回錯(cuò)誤結(jié)果。這里用到了SpringContextUtils通過上下文來獲取Bean組件,下面會(huì)提供。
過濾器屬于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驗(yàn)證過濾器,驗(yàn)證成功后將用戶信息放入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 { //獲取請(qǐng)求頭中的token String jwtToken = request.getHeader("token"); if (!StringUtils.hasLength(jwtToken)) { //token不存在,交給其他過濾器處理 filterChain.doFilter(request, response); return; //結(jié)束方法 } //過濾器中無法初始化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"); //權(quán)限信息 Authentication authentication = new UsernamePasswordAuthenticationToken( username, null, Collections.singleton(new SimpleGrantedAuthority(authorityString)) ); //將用戶信息放入SecurityContext上下文 SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); } catch (Exception ex) { //過濾器中拋出的異常無法被全局異常處理器捕獲,直接返回錯(cuò)誤結(jié)果 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)建上下文,實(shí)現(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驗(yàn)證過濾器
將自定義token驗(yàn)證過濾器,添加到UsernamePasswordAuthenticationFilter前面。
UsernamePasswordAuthenticationFilter實(shí)現(xiàn)了基于用戶名和密碼的認(rèn)證邏輯,我們利用token進(jìn)行身份驗(yàn)證,所以用不到這個(gè)過濾器。
/** * @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保護(hù) httpSecurity.csrf(AbstractHttpConfigurer::disable); //通過上下文獲取AuthenticationManager AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager"); //添加自定義token驗(yàn)證過濾器 httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); }
自定義用戶未登錄的處理
用戶請(qǐng)求未攜帶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); } }
自定義用戶權(quán)限不足的處理
用戶權(quán)限不足的處理,替換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: 自定義用戶權(quán)限不足的處理 * @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("權(quán)限不足!")); 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保護(hù) httpSecurity.csrf(AbstractHttpConfigurer::disable); //通過上下文獲取AuthenticationManager AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager"); //添加自定義token驗(yàn)證過濾器 httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class); //自定義處理器 httpSecurity.exceptionHandling(exceptionHandling -> exceptionHandling .accessDeniedHandler(authAccessDeniedHandler) //處理用戶權(quán)限不足 .authenticationEntryPoint(authEntryPointHandler) //處理用戶未登錄(未攜帶token) ); return httpSecurity.build(); }
靜態(tài)資源放行
SpringBoot3 中使用 Swagger3 接口文檔,在整合了 SpringSecurity 后會(huì)出現(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/**" ); }
總結(jié)
SpringSecurity6 的用法和以前版本的有較大差別,比如WebSecurityConfigurerAdapter的廢除,看到配置類繼承了這個(gè)的都是過時(shí)的教程。因?yàn)椴辉倮^承,所以不能通過重寫方法的方式去配置。另外很多配置的方式都變成使用Lambda表達(dá)式,或者是方法引用。
到此這篇關(guān)于SpringBoot3+SpringSecurity6前后端分離的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)SpringBoot3+SpringSecurity6前后端分離內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA中Spring Initializr沒有Java8選項(xiàng)的解決辦法
在使用IDEA中的Spring Initializr創(chuàng)建新項(xiàng)目時(shí),Java 版本近可選擇Java17,21 ,不能選擇Java8;SpringBoot 版本也只有 3.x,所以本文給大家介紹了IDEA中Spring Initializr沒有Java8選項(xiàng)的解決辦法,需要的朋友可以參考下2024-06-06Spring Boot + MyBatis Plus 高效開發(fā)實(shí)戰(zhàn)從入
本文將詳細(xì)介紹 Spring Boot + MyBatis Plus 的完整開發(fā)流程,并深入剖析分頁查詢、批量操作、動(dòng)態(tài) SQL、樂觀鎖、代碼優(yōu)化等實(shí)戰(zhàn)技巧,感興趣的朋友一起看看吧2025-04-04Mybatis如何傳入多個(gè)參數(shù)(實(shí)體類型和基本類型)
這篇文章主要介紹了Mybatis如何傳入多個(gè)參數(shù)(實(shí)體類型和基本類型),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06JVM---jstack分析Java線程CPU占用,線程死鎖的解決
這篇文章主要介紹了JVM---jstack分析Java線程CPU占用,線程死鎖的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09Java的線程與進(jìn)程以及線程的四種創(chuàng)建方式
這篇文章主要為大家詳細(xì)介紹了Java的線程與進(jìn)程以及線程的四種創(chuàng)建方式,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03