diff --git a/pom.xml b/pom.xml index e2e5f18..f362cf0 100644 --- a/pom.xml +++ b/pom.xml @@ -71,12 +71,12 @@ jakarta.mail 2.0.1 - - org.springframework.boot - spring-boot-devtools - runtime - true - + + + + + + org.projectlombok lombok @@ -127,6 +127,11 @@ jjwt-jackson 0.11.5 + + org.mockito + mockito-core + test + diff --git a/src/main/java/asia/yulinling/workflow/WorkFlowMain.java b/src/main/java/asia/yulinling/workflow/WorkFlowMain.java index d7244bc..073e3d9 100644 --- a/src/main/java/asia/yulinling/workflow/WorkFlowMain.java +++ b/src/main/java/asia/yulinling/workflow/WorkFlowMain.java @@ -13,11 +13,5 @@ import org.springframework.context.ConfigurableApplicationContext; public class WorkFlowMain { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(WorkFlowMain.class, args); - int length = context.getBeanDefinitionCount(); - log.trace("Number of beans in application context: {}", length); - log.debug("Number of beans in application context: {}", length); - log.info("Number of beans in application context: {}", length); - log.warn("Number of beans in application context: {}", length); - log.error("Number of beans in application context: {}", length); } } \ No newline at end of file diff --git a/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java b/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java index 84a6cf9..00673c5 100644 --- a/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java +++ b/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java @@ -1,26 +1,61 @@ package asia.yulinling.workflow.config; + +import asia.yulinling.workflow.model.vo.user.UserPrincipal; +import asia.yulinling.workflow.security.AuthEntryPoint; +import asia.yulinling.workflow.security.JwtAuthenticationFilter; +import asia.yulinling.workflow.service.CustomerDetailsService; +import asia.yulinling.workflow.utils.JwtUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.Customizer; +import org.springframework.security.authentication.AuthenticationManager; +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.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import java.util.List; @Configuration @EnableWebSecurity +@RequiredArgsConstructor +@Slf4j public class SecurityConfig { + + private final CustomerDetailsService customerDetailService; + @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests(authorize -> authorize - .requestMatchers("/users", "/users/**").permitAll() - .anyRequest().authenticated()) - .formLogin(formLogin -> formLogin - .loginPage("/login") - .permitAll()) - .rememberMe(Customizer.withDefaults()); + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtil jwtUtil, AuthEntryPoint authEntryPoint, RequestMatcher requestMatcher) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .exceptionHandling(ex -> ex + .authenticationEntryPoint(authEntryPoint) + .accessDeniedHandler((request, response, accessDeniedException) -> response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied")) + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/login").permitAll() + .requestMatchers("/users", "/users/**").authenticated() + .anyRequest().authenticated() + ) + .sessionManagement(AbstractHttpConfigurer::disable) + .addFilterBefore( + new JwtAuthenticationFilter(jwtUtil, authEntryPoint, requestMatcher), + UsernamePasswordAuthenticationFilter.class + ); + return http.build(); } @@ -31,4 +66,32 @@ public class SecurityConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean RequestMatcher requestMatcher() { + return new AntPathRequestMatcher("/login", "POST"); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(customerDetailService); + return authProvider; + } + + @Bean + public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { + UserPrincipal user = new UserPrincipal( + 1L, + "admin", + passwordEncoder.encode("123456"), + List.of("ADMIN"), + List.of(new SimpleGrantedAuthority("ROLE_ADMIN")) + ); + return new InMemoryUserDetailsManager(user); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } } diff --git a/src/main/java/asia/yulinling/workflow/controller/LoginController.java b/src/main/java/asia/yulinling/workflow/controller/LoginController.java new file mode 100644 index 0000000..e6c9cad --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/controller/LoginController.java @@ -0,0 +1,51 @@ +package asia.yulinling.workflow.controller; + +import asia.yulinling.workflow.dto.request.LoginRequest; +import asia.yulinling.workflow.utils.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 登录控制层 + *

+ * + * @author YLL + * @since 2025/6/13 + */ +@RestController +@RequiredArgsConstructor +@Slf4j +public class LoginController { + + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + try { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); + + Authentication authentication = authenticationManager.authenticate(authToken); + + log.info("{}", authentication.getPrincipal()); + String token = jwtUtil.generateToken(authentication, false); // 可根据需要传 true/false + + return ResponseEntity.ok(token); + } catch (AuthenticationException ex) { + log.info("{}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误"); + } + } +} + diff --git a/src/main/java/asia/yulinling/workflow/controller/TestController.java b/src/main/java/asia/yulinling/workflow/controller/TestController.java index 6ba19e9..73e0ee0 100644 --- a/src/main/java/asia/yulinling/workflow/controller/TestController.java +++ b/src/main/java/asia/yulinling/workflow/controller/TestController.java @@ -8,10 +8,12 @@ import asia.yulinling.workflow.model.ApiResponse; import asia.yulinling.workflow.dto.response.PageResult; import asia.yulinling.workflow.model.vo.user.UserVO; import asia.yulinling.workflow.service.UserService; +import asia.yulinling.workflow.utils.JwtUtil; import cn.hutool.core.lang.Dict; import cn.hutool.json.JSONUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.web.bind.annotation.*; import java.util.Map; @@ -29,6 +31,8 @@ import java.util.Map; @RequiredArgsConstructor public class TestController { private final UserService userService; + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; /** * 测试方法 GET @@ -72,9 +76,4 @@ public class TestController { public ApiResponse> usersPage(PageParam pageParam) { return userService.getUserListByPage(pageParam); } - - @GetMapping("/login") - public ApiResponse login() { - return ApiResponse.ofSuccess("登录成功"); - } } diff --git a/src/main/java/asia/yulinling/workflow/dto/request/LoginRequest.java b/src/main/java/asia/yulinling/workflow/dto/request/LoginRequest.java new file mode 100644 index 0000000..0799ce4 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/dto/request/LoginRequest.java @@ -0,0 +1,17 @@ +package asia.yulinling.workflow.dto.request; + +import lombok.Data; + +/** + *

+ * 登录请求类 + *

+ * + * @author YLL + * @since 2025/6/13 + */ +@Data +public class LoginRequest { + private String username; + private String password; +} diff --git a/src/main/java/asia/yulinling/workflow/mapper/PermissionMapper.java b/src/main/java/asia/yulinling/workflow/mapper/PermissionMapper.java new file mode 100644 index 0000000..249b112 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/mapper/PermissionMapper.java @@ -0,0 +1,19 @@ +package asia.yulinling.workflow.mapper; + +import asia.yulinling.workflow.model.entity.Permission; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Component; + +/** + *

+ * + *

+ * + * @author YLL + * @since 2025/6/13 + */ +@Mapper +@Component +public interface PermissionMapper extends BaseMapper { +} diff --git a/src/main/java/asia/yulinling/workflow/mapper/RoleMapper.java b/src/main/java/asia/yulinling/workflow/mapper/RoleMapper.java new file mode 100644 index 0000000..62b0688 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/mapper/RoleMapper.java @@ -0,0 +1,19 @@ +package asia.yulinling.workflow.mapper; + +import asia.yulinling.workflow.model.entity.Role; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Component; + +/** + *

+ * 角色Mapper + *

+ * + * @author YLL + * @since 2025/6/13 + */ +@Mapper +@Component +public interface RoleMapper extends BaseMapper { +} diff --git a/src/main/java/asia/yulinling/workflow/mapper/UserMapper.java b/src/main/java/asia/yulinling/workflow/mapper/UserMapper.java index 9439345..9dd2112 100644 --- a/src/main/java/asia/yulinling/workflow/mapper/UserMapper.java +++ b/src/main/java/asia/yulinling/workflow/mapper/UserMapper.java @@ -3,8 +3,6 @@ package asia.yulinling.workflow.mapper; import asia.yulinling.workflow.model.entity.User; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Component; /** @@ -18,29 +16,4 @@ import org.springframework.stereotype.Component; @Mapper @Component public interface UserMapper extends BaseMapper { - - /** - * 根据id查询用户 - * - * @param id 主键id - * @return 当前id的用户,不存在则是{@code null} - */ - @Select("SELECT * FROM orm_user WHERE id = #{id}") - User selectUserById(@Param("id")Long id); - - /** - * 保存用户 - * - * @param user 用户 - * @return 成功 - {@code 1} 失败 - {@code 0} - */ - int saveUser(@Param("user") User user); - - /** - * 删除用户 - * - * @param id 主键id - * @return 成功 - {@code 1} 失败 - {@code 0} - */ - int deleteById(@Param("id")Long id); } diff --git a/src/main/java/asia/yulinling/workflow/model/vo/user/UserPrincipal.java b/src/main/java/asia/yulinling/workflow/model/vo/user/UserPrincipal.java index 1b926d5..8c92d2f 100644 --- a/src/main/java/asia/yulinling/workflow/model/vo/user/UserPrincipal.java +++ b/src/main/java/asia/yulinling/workflow/model/vo/user/UserPrincipal.java @@ -1,18 +1,23 @@ package asia.yulinling.workflow.model.vo.user; +import asia.yulinling.workflow.model.entity.Permission; +import asia.yulinling.workflow.model.entity.Role; +import asia.yulinling.workflow.model.entity.User; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; /** *

@@ -49,12 +54,6 @@ public class UserPrincipal implements UserDetails { @JsonIgnore private String password; - /** - * 加密使用盐 - */ - @JsonIgnore - private String salt; - /** * 邮箱 */ @@ -98,25 +97,80 @@ public class UserPrincipal implements UserDetails { /** * 用户角色列表 */ - private List roles; + private List roles; /** * 用户权限列表 */ private Collection authorities; + public UserPrincipal(Long id, String username, String password, String nickname, String phone, String email, String birthday, Integer sex, Integer status, Date createTime, Date updateTime, List roleNames, List authorities) { + this.id = id; + this.username = username; + this.password = password; + this.nickname = nickname; + this.phone = phone; + this.email = email; + this.birthday = birthday; + this.sex = sex; + this.status = status; + this.createTime = createTime; + this.updateTime = updateTime; + this.roles = roleNames; + this.authorities = authorities; + } + @Override public Collection getAuthorities() { return List.of(); } + public UserPrincipal(Long id, String username, String password, + List roles, Collection authorities) { + this.id = id; + this.username = username; + this.password = password; + this.roles = roles; + this.authorities = authorities; + this.status = 1; // 启用 + } + + public static UserPrincipal create(User user, List roles, List permissions) { + List roleNames = roles.stream().map(Role::getName).collect(Collectors.toList()); + + List authorities = permissions.stream().filter(permission -> StrUtil.isNotBlank(permission.getPermission())).map(permission -> new SimpleGrantedAuthority(permission.getPermission())).collect(Collectors.toList()); + + return new UserPrincipal(user.getId(), user.getUsername(), user.getPassword(), user.getNickname(), user.getPhone(), user.getEmail(), user.getBirthday(), user.getSex(), user.getStatus(), user.getCreateTime(), user.getUpdateTime(), roleNames, authorities); + } + + @Override public String getPassword() { - return ""; + return this.password; } @Override public String getUsername() { - return ""; + return this.username; + } + + @Override + public boolean isAccountNonExpired() { + return true; // 视业务逻辑可改 + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return this.status != null && this.status == 1; } } diff --git a/src/main/java/asia/yulinling/workflow/security/AuthEntryPoint.java b/src/main/java/asia/yulinling/workflow/security/AuthEntryPoint.java new file mode 100644 index 0000000..cb72d43 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/security/AuthEntryPoint.java @@ -0,0 +1,26 @@ +package asia.yulinling.workflow.security; + +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; + +/** + *

+ * 权限不足处理器 + *

+ * + * @author YLL + * @since 2025/6/13 + */ +@Component +public class AuthEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Missing or invalid token"); + } +} diff --git a/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..88e4bd4 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java @@ -0,0 +1,66 @@ +package asia.yulinling.workflow.security; + +import asia.yulinling.workflow.utils.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + *

+ * Jwt登录拦截器 + *

+ * + * @author YLL + * @since 2025/6/13 + */ +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final AuthenticationEntryPoint authenticationEntryPoint; + private final RequestMatcher requestMatcher; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) + throws ServletException, IOException { + log.info("{}", request.getRequestURI()); + if (requestMatcher.matches(request)) { + log.info("{}", request.getRequestURI()); + filterChain.doFilter(request, response); + return; + } + String header = request.getHeader("Authorization"); + if (header == null || !header.startsWith("Bearer ")) { + String token = null; + if (header != null) { + token = header.substring(7); + } + if (jwtUtil.validateToken(token)) { + String username = jwtUtil.parseToken(token).getSubject(); + String roles = jwtUtil.parseToken(token).get("roles").toString(); + Authentication auth = new JwtAuthenticationToken(username, roles); + SecurityContextHolder.getContext().setAuthentication(auth); + } else { + authenticationEntryPoint.commence(request, response, new AuthenticationException("") { + }); + return; + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationToken.java b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationToken.java new file mode 100644 index 0000000..62077e0 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationToken.java @@ -0,0 +1,37 @@ +package asia.yulinling.workflow.security; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collections; + +/** + *

+ * JWT认证 + *

+ * + * @author YLL + * @since 2025/6/13 + */ +public class JwtAuthenticationToken extends AbstractAuthenticationToken { + + private final String principal; + private final String role; + + public JwtAuthenticationToken(String username, String role) { + super(Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role))); + this.principal = username; + this.role = role; + setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Object getCredentials() { + return ""; + } +} diff --git a/src/main/java/asia/yulinling/workflow/service/CustomerDetailsService.java b/src/main/java/asia/yulinling/workflow/service/CustomerDetailsService.java new file mode 100644 index 0000000..24d979e --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/service/CustomerDetailsService.java @@ -0,0 +1,55 @@ +package asia.yulinling.workflow.service; + +import asia.yulinling.workflow.mapper.PermissionMapper; +import asia.yulinling.workflow.mapper.RoleMapper; +import asia.yulinling.workflow.mapper.UserMapper; +import asia.yulinling.workflow.model.entity.Permission; +import asia.yulinling.workflow.model.entity.Role; +import asia.yulinling.workflow.model.entity.User; +import asia.yulinling.workflow.model.vo.user.UserPrincipal; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +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 java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 自定义UserDetail查询 + *

+ * + * @author YLL + * @since 2025/6/13 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomerDetailsService implements UserDetailsService { + private final UserMapper userMapper; + private final RoleMapper roleMapper; + private final PermissionMapper permissionMapper; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(User.class) + .eq(User::getUsername, username) + .or() + .eq(User::getEmail, username) + .or() + .eq(User::getPhone, username); + + User user = userMapper.selectOne(queryWrapper); + List roles = roleMapper.selectByIds(Collections.singleton(user.getId())); + List roleIds = roles.stream().map(Role::getId).collect(Collectors.toList()); + List permissions = permissionMapper.selectByIds(roleIds); + return UserPrincipal.create(user, roles, permissions); + } +} + diff --git a/src/main/java/asia/yulinling/workflow/service/impl/UserServiceImpl.java b/src/main/java/asia/yulinling/workflow/service/impl/UserServiceImpl.java index 125f94d..cbaed26 100644 --- a/src/main/java/asia/yulinling/workflow/service/impl/UserServiceImpl.java +++ b/src/main/java/asia/yulinling/workflow/service/impl/UserServiceImpl.java @@ -7,7 +7,7 @@ import asia.yulinling.workflow.dto.response.PageResult; import asia.yulinling.workflow.model.entity.User; import asia.yulinling.workflow.model.vo.user.UserVO; import asia.yulinling.workflow.service.UserService; -import asia.yulinling.workflow.utils.PageUtils; +import asia.yulinling.workflow.utils.PageUtil; import cn.hutool.core.util.ArrayUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; @@ -74,7 +74,7 @@ public class UserServiceImpl implements UserService { } } - PageResult pageResult = PageUtils.buildPageResult(users, userVOList); + PageResult pageResult = PageUtil.buildPageResult(users, userVOList); return ApiResponse.ofSuccess(pageResult); } diff --git a/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java b/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java index ce552a6..59dd2ea 100644 --- a/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java +++ b/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java @@ -6,11 +6,12 @@ import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import io.jsonwebtoken.*; +import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Collection; import java.util.Date; @@ -25,8 +26,8 @@ import java.util.List; * @since 2025/6/11 */ @EnableConfigurationProperties(JwtConfig.class) -@Configuration @RequiredArgsConstructor +@Component @Slf4j public class JwtUtil { private final JwtConfig jwtConfig; @@ -34,30 +35,35 @@ public class JwtUtil { /** * 创建JWT * - * @param rememberMe 记住我 - * @param id 用户id - * @param subject 用户名 - * @param roles 用户角色 - * @param authorities 用户权限 + * @param isRememberMe 记住我 + * @param userId 用户id + * @param subject 用户名 + * @param roles 用户角色 + * @param authorities 用户权限 * @return JWT */ - public String createJWT(Boolean rememberMe, Long id, String subject, List roles, Collection authorities) { + public String generateToken(boolean isRememberMe, Long userId, String subject, List roles, Collection authorities) { + // 1. 构建签名密钥 + Key key = getSigningKey(); + + // 2. 当前时间 Date now = new Date(); - Key key = Keys.hmacShaKeyFor(jwtConfig.getKey().getBytes()); + // 3. 计算过期时间 + long ttl = isRememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl(); + Date expiration = new Date(now.getTime() + ttl); + // 4. 构建 JWT JwtBuilder builder = Jwts.builder() - .setId(id.toString()) + .setId(String.valueOf(userId)) .setSubject(subject) .setIssuedAt(now) - .signWith(key) + .setExpiration(expiration) .claim("roles", roles) - .claim("authorities", authorities); - - // 设置过期时间 - Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl(); - builder.setExpiration(new Date(now.getTime() + ttl)); + .claim("authorities", authorities) + .signWith(key); + // 5. 返回生成的 token 字符串 return builder.compact(); } @@ -65,13 +71,13 @@ public class JwtUtil { * 创建JWT * * @param authentication 用户认证信息 - * @param rememberMe 记住我 + * @param isRememberMe 记住我 * @return JWT */ - public String createJWT(Authentication authentication, Boolean rememberMe) { + public String generateToken(Authentication authentication, Boolean isRememberMe) { UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); - return createJWT( - rememberMe, + return generateToken( + isRememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), @@ -79,25 +85,58 @@ public class JwtUtil { ); } - public Claims parseJWT(String jwt) { + /** + * 解析Token + * + * @param token token信息 + * @return 解析信息 + */ + public Claims parseToken(String token) { try { - // 1. 构建密钥 - Key key = Keys.hmacShaKeyFor(jwtConfig.getKey().getBytes()); + Key key = getSigningKey(); - // 2. 使用 parserBuilder 构建解析器 - Jws jws = Jwts.parserBuilder() + return Jwts.parserBuilder() .setSigningKey(key) .build() - .parseClaimsJws(jwt); - - Claims claims = jws.getBody(); - String id = claims.getId(); - String username = claims.getSubject(); - - return claims; + .parseClaimsJws(token) + .getBody(); } catch (ExpiredJwtException e) { - log.error("ExpiredJwtException", e); - throw new JwtException("ExpiredJwtException"); + log.error("Token 已过期: {}", token, e); + throw new JwtException("Token 已过期", e); + } catch (JwtException e) { + log.error("Token 无效: {}", token, e); + throw new JwtException("Token 无效", e); + } catch (Exception e) { + log.error("Token 解析异常: {}", token, e); + throw new JwtException("Token 解析失败", e); } } + + /** + * 校验Token + * + * @param token token信息 + * @return true-token正确 false-token错误 + */ + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token); + return true; + } catch (JwtException e) { + return false; + } + } + + /** + * 获取签名密钥 + * + * @return 返回key + */ + private Key getSigningKey() { + String secret = jwtConfig.getKey(); + if (secret == null || secret.isEmpty()) { + throw new IllegalStateException("JWT 签名密钥未配置"); + } + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } } diff --git a/src/main/java/asia/yulinling/workflow/utils/PageUtils.java b/src/main/java/asia/yulinling/workflow/utils/PageUtil.java similarity index 93% rename from src/main/java/asia/yulinling/workflow/utils/PageUtils.java rename to src/main/java/asia/yulinling/workflow/utils/PageUtil.java index adfb9fb..07a2548 100644 --- a/src/main/java/asia/yulinling/workflow/utils/PageUtils.java +++ b/src/main/java/asia/yulinling/workflow/utils/PageUtil.java @@ -7,13 +7,13 @@ import java.util.List; /** *

- * 分页工具类 + * 分页工具类 *

* * @author YLL * @since 2025/6/9 */ -public class PageUtils { +public class PageUtil { public static PageResult buildPageResult(IPage iPage, List list) { PageResult result = new PageResult<>(); @@ -26,7 +26,7 @@ public class PageUtils { // 可选字段 int navigatePages = 5; result.setNavigatePages(navigatePages); - result.setNavigatePageNums(calculateNavigatePageNumbers((int) iPage.getCurrent(), (int)iPage.getPages(), navigatePages)); + result.setNavigatePageNums(calculateNavigatePageNumbers((int) iPage.getCurrent(), (int) iPage.getPages(), navigatePages)); result.setHasNext(iPage.getCurrent() < iPage.getPages()); result.setHasPrev(iPage.getCurrent() > 1); @@ -36,7 +36,7 @@ public class PageUtils { result.setFrom((int) ((iPage.getCurrent() - 1) * iPage.getSize() + 1)); result.setTo((int) (Math.min(iPage.getCurrent() * iPage.getSize(), iPage.getTotal()))); - return result; + return result; } private static int[] calculateNavigatePageNumbers(int currentPage, int totalPage, int navigatePages) { diff --git a/src/main/resources/db/data.sql b/src/main/resources/db/data.sql index cd95de0..e929152 100644 --- a/src/main/resources/db/data.sql +++ b/src/main/resources/db/data.sql @@ -46,14 +46,13 @@ COMMIT; BEGIN; INSERT INTO `wk_user` ( - id, username, nickname, password, salt, email, birthday, sex, phone, status, + id, username, nickname, password, email, birthday, sex, phone, status, create_time, update_time, last_login_time ) VALUES ( 1072806377661009920, 'admin', '管理员', 'ff342e862e7c3285cdc07e56d6b8973b', - '412365a109674b2dbb1981ed561a4c70', 'admin@xkcoding.com', '1994-11-28 00:00:00', -- birthday: 785433600000 → 1994-11-28 1, @@ -65,14 +64,13 @@ INSERT INTO `wk_user` ( ); INSERT INTO `wk_user` ( - id, username, nickname, password, salt, email, birthday, sex, phone, status, + id, username, nickname, password, email, birthday, sex, phone, status, create_time, update_time, last_login_time ) VALUES ( 1072806378780889088, 'user', '普通用户', '6c6bf02c8d5d3d128f34b1700cb1e32c', - 'fcbdd0e8a9404a5585ea4e01d0e4d7a0', 'user@xkcoding.com', '1994-11-28 00:00:00', -- birthday: 785433600000 → 1994-11-28 1, diff --git a/src/test/java/asia/yulinling/workflow/mapper/UserMapperTest.java b/src/test/java/asia/yulinling/workflow/mapper/UserMapperTest.java index 3251ab3..3af8861 100644 --- a/src/test/java/asia/yulinling/workflow/mapper/UserMapperTest.java +++ b/src/test/java/asia/yulinling/workflow/mapper/UserMapperTest.java @@ -59,7 +59,6 @@ public class UserMapperTest { User user = User.builder() .username("yulinling_test") .password(SecureUtil.md5("123456" + salt)) - .salt(salt) .email("2712495353@qq.com") .phone("17770898274") .status(1) diff --git a/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java b/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java new file mode 100644 index 0000000..2f1e869 --- /dev/null +++ b/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java @@ -0,0 +1,109 @@ +package asia.yulinling.workflow.utils; + +import asia.yulinling.workflow.config.JwtConfig; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + *

+ * Jwt工具类测试 + *

+ * + * @author YLL + * @since 2025/6/13 + */ +class JwtUtilTest { + private JwtUtil jwtUtil; + + @BeforeEach + void setUp() { + // 构造 JwtConfig 模拟配置 + JwtConfig config = new JwtConfig(); + config.setKey("your-256-bit-secret-key-which-needs-to-be-long-enough"); // 至少256位bit + config.setTtl(3600000L); // 1小时 + config.setRemember(604800000L); // 7天 + + jwtUtil = new JwtUtil(config); + } + + @Test + void testGenerateTokenAndParseToken() { + Long userId = 123L; + String username = "test_user"; + List roles = List.of("USER", "ADMIN"); + List authorities = List.of(new SimpleGrantedAuthority("ROLE_USER")); + + String token = jwtUtil.generateToken(false, userId, username, roles, authorities); + assertNotNull(token); + + Claims claims = jwtUtil.parseToken(token); + assertEquals(userId.toString(), claims.getId()); + assertEquals(username, claims.getSubject()); + assertEquals(roles, claims.get("roles", List.class)); + } + + @Test + void testValidateToken() { + String token = jwtUtil.generateToken(false, 1L, "valid_user", List.of("USER"), List.of()); + assertTrue(jwtUtil.validateToken(token)); + } + + @Test + void testValidateToken2() { + String invalidToken = "this.is.not.valid.jwt"; + assertFalse(jwtUtil.validateToken(invalidToken)); + } + + @Test + void testGenerateTokenFromAuthentication() { + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + new MockUserPrincipal(100L, "admin", List.of("ADMIN")), + null, + List.of(new SimpleGrantedAuthority("ROLE_ADMIN")) + ); + + String token = jwtUtil.generateToken(authentication, false); + assertNotNull(token); + + Claims claims = jwtUtil.parseToken(token); + assertEquals("admin", claims.getSubject()); + assertEquals("100", claims.getId()); + } + + // Mock UserPrincipal 用于测试 + static class MockUserPrincipal extends asia.yulinling.workflow.model.vo.user.UserPrincipal { + private final Long id; + private final String username; + private final List roles; + + public MockUserPrincipal(Long id, String username, List roles) { + super(); // 假设父类有默认构造函数 + this.id = id; + this.username = username; + this.roles = roles; + } + + @Override + public Long getId() { + return id; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public List getRoles() { + return roles; + } + } +}