目录
导入依赖
添加配置类
实现UserDetailsService
实现UserDetails
JWT工具类
登录接口
自定义token过滤器
SpringContextUtils工具类
添加自定义token验证过滤器
自定义用户未登录的处理
自定义用户权限不足的处理
添加自定义处理器
静态资源放行
总结
网上能找到的SpringBoot项目一般都是SpringBoot2 + SpringSecurity5,甚至是SSM的项目。这些老版本的教程很多已经不适用了,对于现在大部分的初学者来说,学了可能也是经典白雪。我还是不愿学那些老版本的东西,所以自己摸索了一下新版的SpringBoot项目应该怎么写。学习的过程也是非常折磨人的,看了很多的教程才知道个大概。
SpringSecurity依赖
org.springframework.boot
spring-boot-starter-security
JWT依赖
io.jsonwebtoken
jjwt
0.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(Map 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 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 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 static T 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表达式,或者是方法引用。
创作不易,如果对你有帮助的话就点个赞鼓励一下吧 (人 •͈ᴗ•͈) (୨୧•͈ᴗ•͈)◞ᵗʱᵃᵑᵏઽ*♡