SpringSecurity整合JWT实现身份认证
1.添加依赖
SpringSecurity以及JWT的相关依赖
<properties>
// 省略...
<jjwt.version>0.11.2</jjwt.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${
jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${
jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${
jjwt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.JWT工具类
这里就不详细讲解工具类的的使用了
@Component
public class JwtTokenHelper implements InitializingBean {
/**
* 签发人
*/
@Value("${jwt.issuer}")
private String issuer;
/**
* 秘钥
*/
private Key key;
/**
* JWT 解析
*/
private JwtParser jwtParser;
/**
* 解码配置文件中配置的 Base 64 编码 key 为秘钥
* @param base64Key
*/
@Value("${jwt.secret}")
public void setBase64Key(String base64Key) {
key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(base64Key));
}
/**
* 初始化 JwtParser
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
// 考虑到不同服务器之间可能存在时钟偏移,setAllowedClockSkewSeconds 用于设置能够容忍的最大的时钟误差
jwtParser = Jwts.parserBuilder().requireIssuer(issuer)
.setSigningKey(key).setAllowedClockSkewSeconds(10)
.build();
}
/**
* 生成 Token
* @param username
* @return
*/
public String generateToken(String username) {
LocalDateTime now = LocalDateTime.now();
// Token 一个小时后失效
LocalDateTime expireTime = now.plusHours(1);
return Jwts.builder().setSubject(username)
.setIssuer(issuer)
.setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
.setExpiration(Date.from(expireTime.atZone(ZoneId.systemDefault()).toInstant()))
.signWith(key)
.compact();
}
/**
* 解析 Token
* @param token
* @return
*/
public Jws<Claims> parseToken(String token) {
try {
return jwtParser.parseClaimsJws(token);
} catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
throw new BadCredentialsException("Token 不可用", e);
} catch (ExpiredJwtException e) {
throw new CredentialsExpiredException("Token 失效", e);
}
}
/**
* 生成一个 Base64 的安全秘钥
* @return
*/
private static String generateBase64Key() {
// 生成安全秘钥
Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
// 将密钥进行 Base64 编码
String base64Key = Base64.getEncoder().encodeToString(secretKey.getEncoded());
return base64Key;
}
public static void main(String[] args) {
String key = generateBase64Key();
System.out.println("key: " + key);
}
}
与之对应的,工具类中注入的一些参数,如 jwt
的签发人、秘钥,需要在 applicaiton.yml
中配置好:
jwt:
# 签发人
issuer: quanxiaoha
# 秘钥
secret: jElxcSUj38+Bnh73T68lNs0DfBSit6U3whQlcGO2XwnI+Bo3g4xsiCIPg8PV/L0fQMis08iupNwhe2PzYLB9Xg==
3.JWT认证的逻辑
- 当登录请求进来时,首先来到我们的自定义认证过滤器,将前端传来的账号和密码封装到**
UsernamePasswordAuthenticationToken
**对象中,然后使用getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);来进行身份验证
// 将用户名、密码封装到 Token 中
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
2.AuthenticationManager
委托给默认的 AuthenticationProvider
,如果不自定义AuthenticationProvider,默认情况下会AuthenticationManager
会委托给 DaoAuthenticationProvider
进行认证。DaoAuthenticationProvider
会使用 UserDetailsService
(数据库查询并返回一个 UserDetails
对象,包含用户的用户名、密码、权限等信息) 加载用户详细信息,并使用 PasswordEncoder
(可自定义) 比较密码。
3.在 Spring Security 中,AuthenticationFailureHandler
和 AuthenticationSuccessHandler
是用于处理身份验证失败和成功的接口。它们允许您在用户身份验证过程中自定义响应
4.开始配置 JWT
认证相关的配置
5.应用JWT配置
4.代码实现
根据认证逻辑一步一步来进行完成,
4.1实现自定义认证过滤器
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 指定用户登录的访问地址
*/
public JwtAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
// 解析提交的 JSON 数据
JsonNode jsonNode = mapper.readTree(request.getInputStream());
JsonNode usernameNode = jsonNode.get("username");
JsonNode passwordNode = jsonNode.get("password");
// 判断用户名、密码是否为空
if (Objects.isNull(usernameNode) || Objects.isNull(passwordNode)
|| StringUtils.isBlank(usernameNode.textValue()) || StringUtils.isBlank(passwordNode.textValue())) {
throw new UsernameOrPasswordNullException("用户名或密码不能为空");
}
String username = usernameNode.textValue();
String password = passwordNode.textValue();
// 将用户名、密码封装到 Token 中
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
}
}
4.2实现 UserDetailsService用户详情服务
UserDetailsService
是 Spring Security 提供的接口,用于从应用程序的数据源(如数据库、LDAP、内存等)中加载用户信息。它是一个用于将用户详情加载到 Spring Security 的中心机制。UserDetailsService
主要负责两项工作:

- 加载用户信息: 从数据源中加载用户的用户名、密码和角色等信息。
- 创建 UserDetails 对象: 根据加载的用户信息,创建一个 Spring Security 所需的
UserDetails
对象,包含用户名、密码、角色和权限等。
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中查询
UserDO userDO = userMapper.findByUsername(username);
// 判断用户是否存在
if (Objects.isNull(userDO)) {
throw new UsernameNotFoundException("该用户不存在");
}
// 用户角色
List<UserRoleDO> roleDOS = userRoleMapper.selectByUsername(username);
String[] roleArr = null;
// 转数组
if (!CollectionUtils.isEmpty(roleDOS)) {
List<String> roles = roleDOS.stream().map(p -> p.getRole()).collect(Collectors.toList());
roleArr = roles.toArray(new String[roles.size()]);
}
// authorities 用于指定角色
return User.withUsername(userDO.getUsername())
.password(userDO.getPassword())
.authorities(roleArr)
.build();
}
}
4.3PasswordEncoder 密码加密
@Component
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("qiheyehua"));
}
}
4.4自定义认证成功处理器 RestAuthenticationSuccessHandler
@Component
@Slf4j
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtTokenHelper jwtTokenHelper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 从 authentication 对象中获取用户的 UserDetails 实例,这里是获取用户的用户名
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 通过用户名生成 Token
String username = userDetails.getUsername();
String token = jwtTokenHelper.generateToken(username);
// 返回 Token
LoginRspVO loginRspVO = LoginRspVO.builder().token(token).build();
ResultUtil.ok(response, Response.success(loginRspVO));
}
}
4.5自定义认证失败处理器
@Component
@Slf4j
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.warn("AuthenticationException: ", exception);
if (exception instanceof UsernameOrPasswordNullException) {
// 用户名或密码为空
ResultUtil.fail(response, Response.fail(exception.getMessage()));
} else if (exception instanceof BadCredentialsException) {
// 用户名或密码错误
ResultUtil.fail(response, Response.fail(ResponseCodeEnum.USERNAME_OR_PWD_ERROR));
}
// 登录失败
ResultUtil.fail(response, Response.fail(ResponseCodeEnum.LOGIN_FAIL));
}
}
4.6自定义 JWT 认证功能配置
@Configuration
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler;
@Autowired
private RestAuthenticationFailureHandler restAuthenticationFailureHandler;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
// 自定义的用于 JWT 身份验证的过滤器
JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
// 设置登录认证对应的处理类(成功处理、失败处理)
filter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler);
filter.setAuthenticationFailureHandler(restAuthenticationFailureHandler);
// 直接使用 DaoAuthenticationProvider, 它是 Spring Security 提供的默认的身份验证提供者之一
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 设置 userDetailService,用于获取用户的详细信息
provider.setUserDetailsService(userDetailsService);
// 设置加密算法
provider.setPasswordEncoder(passwordEncoder);
httpSecurity.authenticationProvider(provider);
// 将这个过滤器添加到 UsernamePasswordAuthenticationFilter 之前执行
httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
4.7应用 JWT 认证功能配置
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(). // 禁用 csrf
formLogin().disable() // 禁用表单登录
.apply(jwtAuthenticationSecurityConfig) // 设置用户登录认证相关配置
.and()
.authorizeHttpRequests()
.mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源
.anyRequest().permitAll() // 其他都需要放行,无需认证
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 前后端分离,无需创建会话
}
}
SpringSecurity实现接口鉴权
实现Token校验过滤器
从token中获取信息然后将用户信息存入 authentication,方便后续校验,密码为null因为是token校验,在存进上下文中
@Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenHelper jwtTokenHelper;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 从请求头中获取 key 为 Authorization 的值
String header = request.getHeader("Authorization");
// 判断 value 值是否以 Bearer 开头
if (StringUtils.startsWith(header, "Bearer")) {
// 截取 Token 令牌
String token = StringUtils.substring(header, 7);
log.info("Token: {}", token);
// 判空 Token
if (StringUtils.isNotBlank(token)) {
try {
// 校验 Token 是否可用, 若解析异常,针对不同异常做出不同的响应参数
jwtTokenHelper.validateToken(token);
} catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
// 抛出异常,统一让 AuthenticationEntryPoint 处理响应参数
authenticationEntryPoint.commence(request, response, new AuthenticationServiceException("Token 不可用"));
return;
} catch (ExpiredJwtException e) {
authenticationEntryPoint.commence(request, response, new AuthenticationServiceException("Token 已失效"));
return;
}
// 从 Token 中解析出用户名
String username = jwtTokenHelper.getUsernameByToken(token);
if (StringUtils.isNotBlank(username)
&& Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
// 根据用户名获取用户详情信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 将用户信息存入 authentication,方便后续校验
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将 authentication 存入 ThreadLocal,方便后续获取用户信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
// 继续执行写一个过滤器
filterChain.doFilter(request, response);
}
}
将Token过滤器加入到配置中
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
@Autowired
private RestAuthenticationEntryPoint authEntryPoint;
@Autowired
private RestAccessDeniedHandler deniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(). // 禁用 csrf
formLogin().disable() // 禁用表单登录
.apply(jwtAuthenticationSecurityConfig) // 设置用户登录认证相关配置
.and()
.authorizeHttpRequests()
.mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源
.anyRequest().permitAll() // 其他都需要放行,无需认证
.and()
.httpBasic().authenticationEntryPoint(authEntryPoint) // 处理用户未登录访问受保护的资源的情况
.and()
.exceptionHandling().accessDeniedHandler(deniedHandler) // 处理登录成功后访问受保护的资源,但是权限不够的情况
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 前后端分离,无需创建会话
.and()
.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 将 Token 校验过滤器添加到用户认证过滤器之前
;
}
/**
* Token 校验过滤器
* @return
*/
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter();
}
}
用户未登录时,访问受保护的资源处理器
/**
* @author: 犬小哈
* @url: www.quanxiaoha.com
* @date: 2023-08-27 17:27
* @description: 用户未登录访问受保护的资源
**/
@Slf4j
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.warn("用户未登录访问受保护的资源: ", authException);
if (authException instanceof InsufficientAuthenticationException) {
ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(ResponseCodeEnum.UNAUTHORIZED));
}
ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(authException.getMessage()));
}
}
权限不够处理器
@Slf4j
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.warn("登录成功访问收保护的资源,但是权限不够: ", accessDeniedException);
// 预留,后面引入多角色时会用到
}
}
心得
需要明白一个事情就是,在springsecurity中,如果定义了全局异常处理器优先被全局异常处理器捕获,如果不定义的话就会被springsecurity交给相应的处理器去执行