目录
导入依赖
添加配置类
实现UserDetailsService
实现UserDetails
JWT工具类
登录接口
SpringContextUtils工具类
添加自定义token验证过滤器
自定义用户未登录的处理
自定义用户权限不足的处理
添加自定义处理器
静态资源放行
总结
网上能找到的SpringBoot项目一般都是SpringBoot2 + SpringSecurity5,甚至是SSM的项目。这些老版本的教程很多已经不适用了,对于现在大部分的初学者来说,学了可能也是经典白雪。我还是不愿学那些老版本的东西,所以自己摸索了一下新版的SpringBoot项目应该怎么写。学习的过程也是非常折磨人的,看了很多的教程才知道个大概。
SpringSecurity依赖
org.springframework.boot spring-boot-starter-security
JWT依赖
io.jsonwebtoken jjwt0.9.1
对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(); } }
其中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 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,用于获取用户名和权限。
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令牌 或解析,其中的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(Mapclaims) { 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 ResultdoLogin(@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 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("用户名或密码错误!"); } } }
过滤器中抛出的异常是不会被全局异常处理器捕获到的,直接返回错误结果。这里用到了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); } } }
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 staticT getBean(String name) throws BeansException { if (applicationContext == null) { return null; } return (T) applicationContext.getBean(name); } }
将自定义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表达式,或者是方法引用。
创作不易,如果对你有帮助的话就点个赞鼓励一下吧 (人 •͈ᴗ•͈) (୨୧•͈ᴗ•͈)◞ᵗʱᵃᵑᵏઽ*♡