SpringBoot3+SpringSecurity6前后端分离的项目实践
作者:翰戈.summer
网上能找到的SpringBoot项目一般都是SpringBoot2 + SpringSecurity5,甚至是SSM的项目。这些老版本的教程很多已经不适用了,对于现在大部分的初学者来说,学了可能也是经典白雪。我还是不愿学那些老版本的东西,所以自己摸索了一下新版的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(); } }
实现UserDetailsService
其中UserMapper、AuthorityMapper需要自己创建,不是重点。这两个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()) ); } }
实现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令牌配置属性类)可以自己创建,不是重点。
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) //设置载荷内容 .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为统一响应的结果,UserLoginDTO用于封装用户登录信息,其中的UserDetails必须实现后才能获取到用户信息。
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(作用范围较小),全局异常处理器只能捕获到拦截器中的异常。在过滤器中无法初始化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: 用于创建上下文,实现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实现了基于用户名和密码的认证逻辑,我们利用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(); }
静态资源放行
SpringBoot3 中使用 Swagger3 接口文档,在整合了 SpringSecurity 后会出现无法访问的情况,需要给静态资源放行。
在 SecurityConfig 中添加
/** * 静态资源放行 */ @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前后端分离内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!