feat(security): implement JWT-based authentication and authorization
- Configured JWT token validation filter in security chain - Added user role mapping with new t_user_role table and UserRole entity - Implemented custom authentication entry point and access denied handler - Updated UserDetailService to load user roles from database - Added @PreAuthorize annotation support for method-level security - Refactored build scripts to use java-library plugin and proper dependency scope - Enhanced SQL schema with user role table and improved table comments - Added global exception handler for AccessDeniedException - Introduced ResponseCodeEnum constants for unauthorized and forbidden access - Integrated TokenAuthenticationFilter into Spring Security filter chain
This commit is contained in:
@@ -2,12 +2,10 @@ plugins {
|
||||
`java-library`
|
||||
}
|
||||
|
||||
group = "com.hanserwei.jwt"
|
||||
version = project.parent?.version ?: "0.0.1-SNAPSHOT"
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
api("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation(project(":weblog-module-common"))
|
||||
implementation("org.apache.commons:commons-lang3:3.20.0")
|
||||
|
||||
// jwt
|
||||
api(libs.jjwt.api)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.hanserwei.jwt.config;
|
||||
|
||||
import com.hanserwei.jwt.filter.JwtAuthenticationFilter;
|
||||
import com.hanserwei.jwt.filter.TokenAuthenticationFilter;
|
||||
import com.hanserwei.jwt.handler.RestAuthenticationFailureHandler;
|
||||
import com.hanserwei.jwt.handler.RestAuthenticationSuccessHandler;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -20,6 +21,9 @@ public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<D
|
||||
@Resource
|
||||
private RestAuthenticationFailureHandler restAuthenticationFailureHandler;
|
||||
|
||||
@Resource
|
||||
private TokenAuthenticationFilter tokenAuthenticationFilter;
|
||||
|
||||
@Override
|
||||
public void configure(HttpSecurity httpSecurity) {
|
||||
// 1. 实例化自定义过滤器
|
||||
@@ -35,5 +39,8 @@ public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<D
|
||||
// 4. 将过滤器添加到 UsernamePasswordAuthenticationFilter 之前
|
||||
// (JWT 校验通常在用户名密码校验之前,或者用来替换它,addFilterBefore 是最稳妥的)
|
||||
httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
// 5. 在链路中加入 Token 校验过滤器,确保携带 Token 的请求被识别为已登录
|
||||
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.hanserwei.jwt.filter;
|
||||
|
||||
import com.hanserwei.jwt.utils.JwtTokenHelper;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Resource
|
||||
private JwtTokenHelper jwtTokenHelper;
|
||||
|
||||
@Resource
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@Resource
|
||||
private AuthenticationEntryPoint authenticationEntryPoint;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
@Nonnull HttpServletResponse response,
|
||||
@Nonnull FilterChain filterChain) throws ServletException, IOException {
|
||||
// 1. 从请求头中获取 Authorization
|
||||
String header = request.getHeader("Authorization");
|
||||
|
||||
// 2. 校验头格式 (必须以 Bearer 开头)
|
||||
if (StringUtils.startsWith(header, "Bearer ")) {
|
||||
String token = StringUtils.substring(header, 7);
|
||||
log.info("JWT Token: {}", token);
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
try {
|
||||
// 3. 解析 Token (核心步骤)
|
||||
// 注意:JwtTokenHelper.parseToken 方法内部已经处理了 JWT 格式校验和过期校验,
|
||||
// 并将 JJWT 异常转换为了 Spring Security 的 AuthenticationException 抛出。
|
||||
Jws<Claims> claimsJws = jwtTokenHelper.parseToken(token);
|
||||
|
||||
// 4. 获取用户名
|
||||
// JJWT 0.12+ 建议使用 getPayload() 替代 getBody()
|
||||
// 在 Helper 中生成 Token 时使用的是 .subject(username),所以这里取 Subject
|
||||
String username = claimsJws.getPayload().getSubject();
|
||||
|
||||
// 5. 组装认证信息 (如果当前上下文没有认证信息)
|
||||
if (StringUtils.isNotBlank(username) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
|
||||
|
||||
// 查询数据库获取用户完整信息 (包含权限)
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
|
||||
// 构建 Spring Security 的认证 Token
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
// 6. 将认证信息存入 SecurityContext
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
} catch (AuthenticationException e) {
|
||||
// 7. 异常处理
|
||||
// 捕获 JwtTokenHelper 抛出的 BadCredentialsException 或 CredentialsExpiredException
|
||||
// 如果 Token 存在但是无效/过期,直接交给 EntryPoint 处理响应 (通常返回 401)
|
||||
// 并 return 结束当前过滤器,不再往下执行
|
||||
authenticationEntryPoint.commence(request, response, e);
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
// 处理其他未预料到的异常
|
||||
log.error("JWT处理过程中发生未知错误", e);
|
||||
authenticationEntryPoint.commence(request, response, new AuthenticationException("系统内部认证错误") {
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 放行请求 (如果没有 Token 或者 Token 校验通过,继续执行下一个过滤器)
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.hanserwei.jwt.handler;
|
||||
|
||||
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||
import com.hanserwei.common.utils.Response;
|
||||
import com.hanserwei.jwt.utils.ResultUtil;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RestAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
|
||||
log.warn("登录成功访问收保护的资源,但是权限不够: ", accessDeniedException);
|
||||
// 预留,后面引入多角色时会用到
|
||||
ResultUtil.fail(response, Response.fail(ResponseCodeEnum.FORBIDDEN));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.hanserwei.jwt.handler;
|
||||
|
||||
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||
import com.hanserwei.common.utils.Response;
|
||||
import com.hanserwei.jwt.utils.ResultUtil;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@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));
|
||||
return;
|
||||
}
|
||||
|
||||
ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(authException.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
package com.hanserwei.jwt.service;
|
||||
|
||||
import com.hanserwei.common.domain.dataobject.User;
|
||||
import com.hanserwei.common.domain.dataobject.UserRole;
|
||||
import com.hanserwei.common.domain.repository.UserRepository;
|
||||
import com.hanserwei.common.domain.repository.UserRoleRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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 org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
@@ -18,6 +22,9 @@ public class UserDetailServiceImpl implements UserDetailsService {
|
||||
@Resource
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Resource
|
||||
private UserRoleRepository userRoleRepository;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
// 从数据库查询用户信息
|
||||
@@ -27,10 +34,19 @@ public class UserDetailServiceImpl implements UserDetailsService {
|
||||
throw new UsernameNotFoundException("用户不存在");
|
||||
}
|
||||
|
||||
List<UserRole> userRoles = userRoleRepository.queryAllByUsername(username);
|
||||
// 转换为数组
|
||||
String[] roleArr = new String[0];
|
||||
if (!CollectionUtils.isEmpty(userRoles)) {
|
||||
roleArr = userRoles.stream()
|
||||
.map(UserRole::getRoleName)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
// authorities 用于指定角色,这里写死为 ADMIN 管理员
|
||||
return org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
|
||||
.password(user.getPassword())
|
||||
.authorities("ADMIN")
|
||||
.authorities(roleArr)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user