- 完善Jwt
This commit is contained in:
yulinling 2025-06-15 20:08:16 +08:00
parent feb33c0fa3
commit 5a6402f7fd
10 changed files with 165 additions and 61 deletions

View File

@ -1,5 +1,6 @@
package asia.yulinling.workflow.config; package asia.yulinling.workflow.config;
import asia.yulinling.workflow.security.JwtAccessDeniedHandler;
import asia.yulinling.workflow.security.JwtAuthenticationEntryPoint; import asia.yulinling.workflow.security.JwtAuthenticationEntryPoint;
import asia.yulinling.workflow.security.JwtAuthenticationFilter; import asia.yulinling.workflow.security.JwtAuthenticationFilter;
import asia.yulinling.workflow.security.JwtUserDetailsService; import asia.yulinling.workflow.security.JwtUserDetailsService;
@ -28,20 +29,29 @@ public class SecurityConfig {
private final JwtUserDetailsService jwtUserDetailsService; private final JwtUserDetailsService jwtUserDetailsService;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtil jwtUtil, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtil jwtUtil,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtAccessDeniedHandler jwtAccessDeniedHandler) throws Exception {
http http
// 关闭CSRF
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
// 关闭session
.sessionManagement(AbstractHttpConfigurer::disable) .sessionManagement(AbstractHttpConfigurer::disable)
// 登录登出自定义实现
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
// 异常处理
.exceptionHandling(ex -> ex .exceptionHandling(ex -> ex
.authenticationEntryPoint(jwtAuthenticationEntryPoint) .authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler((request, response, accessDeniedException) -> .accessDeniedHandler(jwtAccessDeniedHandler)
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"))
) )
// 认证请求
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll() .requestMatchers("/login").permitAll()
.requestMatchers("/users", "/users/**").authenticated() .requestMatchers("/users", "/users/**").hasRole("ADMIN")
.anyRequest().authenticated() .anyRequest().authenticated()
) )
// JWT过滤器
.addFilterBefore( .addFilterBefore(
new JwtAuthenticationFilter(jwtUtil, jwtUserDetailsService), new JwtAuthenticationFilter(jwtUtil, jwtUserDetailsService),
UsernamePasswordAuthenticationFilter.class UsernamePasswordAuthenticationFilter.class

View File

@ -18,14 +18,10 @@ public enum Status {
/** 未知异常 */ /** 未知异常 */
UNKNOWN_ERROR(500, "Unknown error"); UNKNOWN_ERROR(500, "Unknown error");
/** /** 状态码 */
* 状态码
*/
private final Integer code; private final Integer code;
/** /** 内容 */
* 内容
*/
private final String message; private final String message;
Status(Integer code, String message) { Status(Integer code, String message) {

View File

@ -32,7 +32,8 @@ public class ApiResponse<T> {
/** /**
* 无参构造器 * 无参构造器
*/ */
private ApiResponse() {} private ApiResponse() {
}
/** /**
* 全参构造器 * 全参构造器
@ -88,6 +89,7 @@ public class ApiResponse<T> {
public static <T> ApiResponse<T> ofStatus(Status status) { public static <T> ApiResponse<T> ofStatus(Status status) {
return ofStatus(status, null); return ofStatus(status, null);
} }
/** /**
* 构造一个有状态且带数据的Api返回 * 构造一个有状态且带数据的Api返回
* *
@ -104,18 +106,19 @@ public class ApiResponse<T> {
* *
* @param t 异常 * @param t 异常
* @param data 返回数据 * @param data 返回数据
* @return ApiResponse
* @param <T> {@link BaseException} 子类 * @param <T> {@link BaseException} 子类
* @return ApiResponse
*/ */
public static <T extends BaseException> ApiResponse<T> ofException(T t, T data) { public static <T extends BaseException> ApiResponse<T> ofException(T t, T data) {
return of(t.getCode(), t.getMessage(), data); return of(t.getCode(), t.getMessage(), data);
} }
/** /**
* 构造一个异常且不带数据的Api返回 * 构造一个异常且不带数据的Api返回
* *
* @param t 异常 * @param t 异常
* @return ApiResponse
* @param <T> {@link BaseException} 子类 * @param <T> {@link BaseException} 子类
* @return ApiResponse
*/ */
public static <T extends BaseException> ApiResponse<T> ofException(T t) { public static <T extends BaseException> ApiResponse<T> ofException(T t) {
return ofException(t, null); return ofException(t, null);

View File

@ -0,0 +1,45 @@
package asia.yulinling.workflow.security;
import asia.yulinling.workflow.exception.BaseException;
import asia.yulinling.workflow.model.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
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;
/**
* <p>
* JWT权限不足处理器
* </p>
*
* @author YLL
* @since 2025/6/15
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
public JwtAccessDeniedHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
// 1. 设置res 403
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// 2. 构建通用response
BaseException baseException = new BaseException(HttpServletResponse.SC_FORBIDDEN, "权限不足");
ApiResponse<?> apiResponse = ApiResponse.ofException(baseException, null);
// 3. 将通用res写入res
response.getWriter().write(objectMapper.writeValueAsString(apiResponse));
}
}

View File

@ -1,5 +1,8 @@
package asia.yulinling.workflow.security; package asia.yulinling.workflow.security;
import asia.yulinling.workflow.exception.BaseException;
import asia.yulinling.workflow.model.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
@ -18,9 +21,25 @@ import java.io.IOException;
*/ */
@Component @Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
public JwtAuthenticationEntryPoint(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override @Override
public void commence(HttpServletRequest request, HttpServletResponse response, public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException { AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Missing or invalid token");
// 1. 设置res 401
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 2. 构建通用response
BaseException baseException = new BaseException(401, "账号未登录");
ApiResponse<?> apiResponse = ApiResponse.ofException(baseException, null);
// 3. 将通用res写入res
response.getWriter().write(objectMapper.writeValueAsString(apiResponse));
} }
} }

View File

@ -38,16 +38,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@NotNull HttpServletResponse response, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException { @NotNull FilterChain filterChain) throws ServletException, IOException {
log.info("request: {}", request.getHeader("Authorization")); // 1. 拿取Token
String token = getTokenFromRequest(request); String token = jwtUtil.getTokenFromRequest(request);
log.info("token: {}", token);
if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) { if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
// 解析token获取username // 2. 解析token获取username
String username = jwtUtil.parseToken(token).getSubject(); String username = jwtUtil.parseToken(token).getSubject();
log.info("username: {}", username);
// 根据token获取的username,加载当前登录中userDetails // 3. 根据username验证password
UserDetails userDetails = userDetailsService.loadUserByUsername(username); UserDetails userDetails = userDetailsService.loadUserByUsername(username);
log.info("userDetails: {}", userDetails); log.info("userDetails: {}", userDetails);
@ -61,16 +59,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
private @Nullable String getTokenFromRequest(HttpServletRequest request) {
// eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTc0OTk2MzA1OSwiZXhwIjoxNzQ5OTYzNjU5fQ.QxiZmycBGxfVfooh_T_lo9SibugLZ2bFt752UChHdtpNb6u__iXodQDK_s6hcz0R
// eyJhbGciOiJIUzI1NiJ9.e30.7QzwIJVh2WpbwTF5ce4crYy3kK2-4GOs0eYJqrGD8FU
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
} }

View File

@ -37,6 +37,7 @@ public class JwtUserDetailsService implements UserDetailsService {
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 构建查找sql
LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery(User.class) LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getUsername, username) .eq(User::getUsername, username)
.or() .or()
@ -44,12 +45,25 @@ public class JwtUserDetailsService implements UserDetailsService {
.or() .or()
.eq(User::getPhone, username); .eq(User::getPhone, username);
// 2. 查找用户
User user = userMapper.selectOne(queryWrapper); User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException("未找到用户信息:" + username);
}
// 3. 查找用户对应的角色
List<Role> roles = roleMapper.selectByIds(Collections.singleton(user.getId())); List<Role> roles = roleMapper.selectByIds(Collections.singleton(user.getId()));
if (roles.isEmpty()) {
throw new UsernameNotFoundException("未找到角色信息" + roles);
}
Set<GrantedAuthority> authorities = roles.stream() Set<GrantedAuthority> authorities = roles.stream()
.map((role) -> new SimpleGrantedAuthority(role.getName())) .map((role) -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
// 4. 返回User
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
username, username,
user.getPassword(), user.getPassword(),

View File

@ -2,8 +2,10 @@ package asia.yulinling.workflow.utils;
import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import io.jsonwebtoken.*; import io.jsonwebtoken.*;
@ -52,7 +54,7 @@ public class JwtUtil {
public String generateToken(Authentication authentication, boolean isRememberMe) { public String generateToken(Authentication authentication, boolean isRememberMe) {
// 1. 构建签名密钥 // 1. 构建签名密钥
Key key = key(); Key key = key();
log.info("key: {}", key);
// 2. 当前时间 // 2. 当前时间
Date now = new Date(); Date now = new Date();
@ -80,8 +82,10 @@ public class JwtUtil {
*/ */
public Claims parseToken(String token) { public Claims parseToken(String token) {
try { try {
// 1. 获取签名密钥
Key key = key(); Key key = key();
log.info("key: {}", key);
// 2. 解析Token
return Jwts.parserBuilder() return Jwts.parserBuilder()
.setSigningKey(key) .setSigningKey(key)
.build() .build()
@ -106,21 +110,51 @@ public class JwtUtil {
* @return true-token正确 false-token错误 * @return true-token正确 false-token错误
*/ */
public boolean validateToken(String token) { public boolean validateToken(String token) {
// 1. 获取签名密钥
Key key = key(); Key key = key();
log.info("token: {}; key: {}", token, key);
// 2. 根据密钥解析token
try { try {
Jwts.parserBuilder() Jwts.parserBuilder()
.setSigningKey(key) .setSigningKey(key)
.build() .build()
.parse(token); .parse(token);
// 3. 解析正确
return true; return true;
} catch (JwtException e) { } catch (JwtException e) {
log.error("Token <UNK>: {}", token, e); log.error("Token: {}, Error: {}", token, e.getMessage());
// 4. 解析错误
return false; return false;
} }
} }
/**
* 设置Token国企
*
* @param request 请求
*/
public void invalidateToken(HttpServletRequest request) {
String token = getTokenFromRequest(request);
String username = parseToken(token).getSubject();
}
/**
* 解析请求头的Authorization,拿取Token
*
* @param request 请求
* @return Token
*/
public @Nullable String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
// 返回Token
return bearerToken.substring(7);
}
return null;
}
/** /**
* 获取签名密钥 * 获取签名密钥
* *

View File

@ -3,13 +3,11 @@ package asia.yulinling.workflow.utils;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.List; import java.util.List;
@ -25,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.*;
*/ */
class JwtUtilTest { class JwtUtilTest {
private JwtUtil jwtUtil; private JwtUtil jwtUtil;
private AuthenticationManager authenticationManager;
@BeforeEach @BeforeEach
void setUp() { void setUp() {