diff --git a/pom.xml b/pom.xml index df9d2ea..70f1d0c 100644 --- a/pom.xml +++ b/pom.xml @@ -78,22 +78,4 @@ 2.15.3 - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - diff --git a/src/main/java/com/supervision/ai/service/hub/config/SecurityConfig.java b/src/main/java/com/supervision/ai/service/hub/config/SecurityConfig.java new file mode 100644 index 0000000..7266267 --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/config/SecurityConfig.java @@ -0,0 +1,73 @@ +package com.supervision.ai.service.hub.config; + +import com.supervision.ai.service.hub.filter.JwtAuthenticationFilter; +import com.supervision.ai.service.hub.service.impl.SysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +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.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + @Autowired + private SysUserService sysUserService; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) // 禁用CSRF + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + // 👇 正确禁用方式(推荐) + .formLogin(Customizer.withDefaults()) // 启用默认配置 + .httpBasic(Customizer.withDefaults()); // 启用默认配置 + return http.build(); + } + + + @Bean + public AuthenticationProvider authenticationProvider() { + // 使用DaoAuthenticationProvider,并注入自定义的UserDetailsService和PasswordEncoder + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(sysUserService); // 从数据库读取用户进行认证 + authProvider.setPasswordEncoder(passwordEncoder()); // 使用BCrypt密码器验证密码 + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + // 从AuthenticationConfiguration中获取AuthenticationManager实例 + return config.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + // 使用BCryptPasswordEncoder作为密码加密器 + return new BCryptPasswordEncoder(); + } + +} diff --git a/src/main/java/com/supervision/ai/service/hub/constant/UserConstant.java b/src/main/java/com/supervision/ai/service/hub/constant/UserConstant.java new file mode 100644 index 0000000..cbe5431 --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/constant/UserConstant.java @@ -0,0 +1,6 @@ +package com.supervision.ai.service.hub.constant; + +public class UserConstant { + public static final String USER_STATUS_ENABLED = "1"; // 用户状态启用 + public static final String USER_STATUS_DISABLED = "0"; // 用户状态禁用 +} diff --git a/src/main/java/com/supervision/ai/service/hub/controller/AuthController.java b/src/main/java/com/supervision/ai/service/hub/controller/AuthController.java new file mode 100644 index 0000000..0765983 --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/controller/AuthController.java @@ -0,0 +1,37 @@ +package com.supervision.ai.service.hub.controller; + +import com.supervision.ai.service.hub.service.impl.AuthService; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + try { + String token = authService.login(request.getUsername(), request.getPassword()); + Map response = new HashMap<>(); + response.put("token", token); + return ResponseEntity.ok(response); + } catch (RuntimeException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); + } + } + + @Data + public static class LoginRequest { + private String username; + private String password; + } +} diff --git a/src/main/java/com/supervision/ai/service/hub/controller/SysUserController.java b/src/main/java/com/supervision/ai/service/hub/controller/SysUserController.java new file mode 100644 index 0000000..b034138 --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/controller/SysUserController.java @@ -0,0 +1,39 @@ +package com.supervision.ai.service.hub.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.supervision.ai.service.hub.domain.SysUser; +import com.supervision.ai.service.hub.service.impl.SysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 用户控制器,用于处理用户相关接口 + */ +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class SysUserController { + + private final SysUserService sysUserService; + + /** + * 查看当前用户详情接口 + * + * @return 当前登录用户的详细信息,如果用户不存在,则返回 404 + */ + @GetMapping("/me") + public ResponseEntity getCurrentUserDetails() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + SysUser user = sysUserService.getOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username)); + if (user == null) { + return ResponseEntity.status(404).body("用户不存在"); + } + return ResponseEntity.ok(user); + } +} diff --git a/src/main/java/com/supervision/ai/service/hub/domain/SysUser.java b/src/main/java/com/supervision/ai/service/hub/domain/SysUser.java new file mode 100644 index 0000000..aff8073 --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/domain/SysUser.java @@ -0,0 +1,16 @@ +package com.supervision.ai.service.hub.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.IdType; +import lombok.Data; + +@Data +@TableName("sys_user") +public class SysUser { + @TableId(type = IdType.AUTO) + private Long id; // 主键ID + private String username; // 用户名 + private String password; // 密码 (加密存储) + private String status; // 状态 (1表示正常,0表示禁用) +} diff --git a/src/main/java/com/supervision/ai/service/hub/filter/JwtAuthenticationFilter.java b/src/main/java/com/supervision/ai/service/hub/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..e2c6fa9 --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/filter/JwtAuthenticationFilter.java @@ -0,0 +1,62 @@ +package com.supervision.ai.service.hub.filter; + +import com.supervision.ai.service.hub.service.impl.SysUserService; +import com.supervision.ai.service.hub.util.JwtUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtils jwtUtils; + private final SysUserService sysUserService; + + @Override + protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + // 提取JWT Token(去掉前缀"Bearer ") + String token = authHeader.substring(7); + String username; + try { + // 从JWT中解析用户名 + username = jwtUtils.getUsernameFromToken(token); + } catch (Exception e) { + // 如果JWT格式不正确或过期,直接放行(后续的过滤器会处理认证失败) + filterChain.doFilter(request, response); + return; + } + // 如果成功提取到用户名,并且当前没有已认证的用户 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // 根据用户名从数据库加载用户信息 + UserDetails userDetails = sysUserService.loadUserByUsername(username); + // 验证Token的有效性(是否未过期,用户名是否匹配) + if (jwtUtils.validateToken(token, userDetails)) { + // 将用户信息封装到Authentication对象中,标记为已认证 + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // 将Authentication对象放入SecurityContext,表示当前请求已通过认证 + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + } + filterChain.doFilter(request, response); + } + +} diff --git a/src/main/java/com/supervision/ai/service/hub/mapper/SysUserMapper.java b/src/main/java/com/supervision/ai/service/hub/mapper/SysUserMapper.java new file mode 100644 index 0000000..f3e1a7f --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/mapper/SysUserMapper.java @@ -0,0 +1,10 @@ +package com.supervision.ai.service.hub.mapper; + +import com.supervision.ai.service.hub.domain.SysUser; +import org.apache.ibatis.annotations.Mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +@Mapper +public interface SysUserMapper extends BaseMapper { + // 继承BaseMapper后,CRUD方法可直接使用,无需额外定义 +} \ No newline at end of file diff --git a/src/main/java/com/supervision/ai/service/hub/service/impl/AuthService.java b/src/main/java/com/supervision/ai/service/hub/service/impl/AuthService.java new file mode 100644 index 0000000..4ce4f1f --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/service/impl/AuthService.java @@ -0,0 +1,36 @@ +package com.supervision.ai.service.hub.service.impl; + +import com.supervision.ai.service.hub.util.JwtUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final AuthenticationManager authenticationManager; + private final JwtUtils jwtUtils; + + /** + * 登录认证,返回JWT Token + */ + public String login(String username, String password) { + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(username, password) + ); + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + return jwtUtils.generateToken(userDetails); + } catch (BadCredentialsException e) { + throw new RuntimeException("用户名或密码错误"); + } catch (DisabledException e) { + throw new RuntimeException("用户已被禁用"); + } + } +} diff --git a/src/main/java/com/supervision/ai/service/hub/service/impl/SysUserService.java b/src/main/java/com/supervision/ai/service/hub/service/impl/SysUserService.java new file mode 100644 index 0000000..d0993de --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/service/impl/SysUserService.java @@ -0,0 +1,48 @@ +package com.supervision.ai.service.hub.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.supervision.ai.service.hub.domain.SysUser; +import com.supervision.ai.service.hub.mapper.SysUserMapper; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +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 static com.supervision.ai.service.hub.constant.UserConstant.USER_STATUS_DISABLED; +import static com.supervision.ai.service.hub.constant.UserConstant.USER_STATUS_ENABLED; + +@Service +public class SysUserService extends ServiceImpl implements UserDetailsService { + + /** + * 根据用户名加载用户信息 + * Spring Security 会调用该方法来获取用户信息 + * + * @param username 用户名 + * @return UserDetails + * @throws UsernameNotFoundException 用户名未找到异常 + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser user = this.getOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username)); + if (user == null) { + throw new UsernameNotFoundException("用户不存在: " + username); + } + if (USER_STATUS_DISABLED.equals(user.getStatus())) { + throw new DisabledException("用户已被禁用"); + } + // 将查询到的用户信息组装成UserDetails对象 + // **扩展点**:如需加载用户角色权限,可在此处查询 sys_user_role 表关联的角色,并将角色加入 authorities 列表 + List authorities = Collections.emptyList(); + // 使用Spring Security提供的User对象作为UserDetails返回 + return new User(user.getUsername(), user.getPassword(), authorities); + } + +} diff --git a/src/main/java/com/supervision/ai/service/hub/util/JwtUtils.java b/src/main/java/com/supervision/ai/service/hub/util/JwtUtils.java new file mode 100644 index 0000000..d0814af --- /dev/null +++ b/src/main/java/com/supervision/ai/service/hub/util/JwtUtils.java @@ -0,0 +1,71 @@ +package com.supervision.ai.service.hub.util; + + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Base64; +import java.util.Date; + +@Component +public class JwtUtils { + + @Value("${jwt.secret}") + private String Secret; + + @Value("${jwt.expiration}") + private long Expiration; // 1天 + + private SecretKey secretKey; + + @PostConstruct + public void initKey() { + this.secretKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(Secret)); + } + + // 生成Token + public String generateToken(UserDetails userDetails) { + return Jwts.builder() + .subject(userDetails.getUsername()) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + Expiration)) + .signWith(secretKey, Jwts.SIG.HS256) + .compact(); + } + + // 从 Token 中解析用户名 + public String getUsernameFromToken(String token) { + Jws jws = Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + return jws.getPayload().getSubject(); + } + + + // 校验Token是否有效 + public boolean validateToken(String token, UserDetails userDetails) { + try { + String username = getUsernameFromToken(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } catch (Exception e) { + return false; + } + } + + // 判断是否过期 + private boolean isTokenExpired(String token) { + Jws jws = Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + return jws.getPayload().getExpiration().before(new Date()); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2d08bcd..34aa94c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,6 +17,10 @@ mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +jwt: + secret: "DlHaPUePiN6MyvpMpsMq/t6swzMHqtrRFd2YnofKz4k=" # JWT密钥 使用官方推荐方式生成 Base64.getEncoder().encodeToString(Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded()); + expiration: 3600000 # 1小时:3600000 1天:86400000 + heygem: server: ip: 192.168.10.96