Compare commits

...

2 Commits

Author SHA1 Message Date
yulinling
5a6402f7fd feat:
- 完善Jwt
2025-06-15 20:08:16 +08:00
yulinling
feb33c0fa3 fix:
- 修复jwtUtil解析
2025-06-15 13:22:34 +08:00
11 changed files with 185 additions and 57 deletions

View File

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

View File

@ -18,14 +18,10 @@ public enum Status {
/** 未知异常 */
UNKNOWN_ERROR(500, "Unknown error");
/**
* 状态码
*/
/** 状态码 */
private final Integer code;
/**
* 内容
*/
/** 内容 */
private final 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) {
return ofStatus(status, null);
}
/**
* 构造一个有状态且带数据的Api返回
*
@ -104,18 +106,19 @@ public class ApiResponse<T> {
*
* @param t 异常
* @param data 返回数据
* @return ApiResponse
* @param <T> {@link BaseException} 子类
* @return ApiResponse
*/
public static <T extends BaseException> ApiResponse<T> ofException(T t, T data) {
return of(t.getCode(), t.getMessage(), data);
}
/**
* 构造一个异常且不带数据的Api返回
*
* @param t 异常
* @return ApiResponse
* @param <T> {@link BaseException} 子类
* @return ApiResponse
*/
public static <T extends BaseException> ApiResponse<T> ofException(T t) {
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;
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.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
@ -18,9 +21,25 @@ import java.io.IOException;
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
public JwtAuthenticationEntryPoint(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
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,15 +38,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
log.info("token: {}", token);
// 1. 拿取Token
String token = jwtUtil.getTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
// 解析token获取username
// 2. 解析token获取username
String username = jwtUtil.parseToken(token).getSubject();
log.info("username: {}", username);
// 根据token获取的username,加载当前登录中userDetails
// 3. 根据username验证password
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
log.info("userDetails: {}", userDetails);
@ -60,13 +59,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
}
private @Nullable String getTokenFromRequest(HttpServletRequest request) {
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
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 构建查找sql
LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getUsername, username)
.or()
@ -44,12 +45,25 @@ public class JwtUserDetailsService implements UserDetailsService {
.or()
.eq(User::getPhone, username);
// 2. 查找用户
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException("未找到用户信息:" + username);
}
// 3. 查找用户对应的角色
List<Role> roles = roleMapper.selectByIds(Collections.singleton(user.getId()));
if (roles.isEmpty()) {
throw new UsernameNotFoundException("未找到角色信息" + roles);
}
Set<GrantedAuthority> authorities = roles.stream()
.map((role) -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toSet());
// 4. 返回User
return new org.springframework.security.core.userdetails.User(
username,
user.getPassword(),

View File

@ -4,6 +4,7 @@ import asia.yulinling.workflow.dto.request.LoginRequest;
import asia.yulinling.workflow.service.AuthService;
import asia.yulinling.workflow.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@ -20,6 +21,7 @@ import org.springframework.stereotype.Service;
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthServiceImpl implements AuthService {
private final AuthenticationManager authenticationManager;
@ -31,6 +33,8 @@ public class AuthServiceImpl implements AuthService {
loginRequest.getUsername(), loginRequest.getPassword()
));
SecurityContextHolder.getContext().setAuthentication(authentication);
return jwtUtil.generateToken(authentication, false);
String token =jwtUtil.generateToken(authentication, false);
log.info("generateToken: {}", token);
return token;
}
}

View File

@ -2,8 +2,10 @@ package asia.yulinling.workflow.utils;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import io.jsonwebtoken.*;
@ -52,7 +54,7 @@ public class JwtUtil {
public String generateToken(Authentication authentication, boolean isRememberMe) {
// 1. 构建签名密钥
Key key = key();
log.info("key: {}", key);
// 2. 当前时间
Date now = new Date();
@ -80,8 +82,10 @@ public class JwtUtil {
*/
public Claims parseToken(String token) {
try {
// 1. 获取签名密钥
Key key = key();
log.info("key: {}", key);
// 2. 解析Token
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
@ -106,21 +110,51 @@ public class JwtUtil {
* @return true-token正确 false-token错误
*/
public boolean validateToken(String token) {
// 1. 获取签名密钥
Key key = key();
log.info("token: {}; key: {}", token, key);
// 2. 根据密钥解析token
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
.parse(token);
// 3. 解析正确
return true;
} catch (JwtException e) {
log.error("Token <UNK>: {}", token, e);
log.error("Token: {}, Error: {}", token, e.getMessage());
// 4. 解析错误
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

@ -8,7 +8,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.List;
@ -44,6 +43,20 @@ class JwtUtilTest {
assertEquals("test", claims.getSubject());
}
@Test
void testGenerateTokenAndValidateToken() {
Authentication authentication = new UsernamePasswordAuthenticationToken(
new MockUserPrincipal(100L, "admin", List.of("ADMIN")),
null,
List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))
);
String token = jwtUtil.generateToken(authentication, false);
System.out.println(token);
assertNotNull(token);
assertTrue(jwtUtil.validateToken(token));
}
@Test
void testValidateToken() {
UserDetails user = User.withUsername("test")