This commit is contained in:
2025-11-14 20:20:10 +08:00
commit f83bd845dc
31 changed files with 1395 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ski</groupId>
<artifactId>ski-dashboard</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>ski-dashboard-admin</artifactId>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.14</version>
</dependency>
<dependency>
<groupId>com.ski.lichuan</groupId>
<artifactId>ski-dashboard-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.ski.lichuan</groupId>
<artifactId>ski-dashboard-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.ski</groupId>
<artifactId>ski-dashboard-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
package com.ski.lichuan.admin;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"com.ski.lichuan"})
@MapperScan("com.ski.lichuan.mapper")
public class SkiDashboardAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SkiDashboardAdminApplication.class, args);
}
}

View File

@@ -0,0 +1,86 @@
package com.ski.lichuan.admin.config;
import com.ski.lichuan.admin.filter.JwtAuthenticationFilter;
import com.ski.lichuan.admin.handler.JwtAuthenticationEntryPoint;
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.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthFilter;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthEntryPoint; // 自定义认证失败处理器
// 密码加密方式BCrypt
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置认证管理器
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 关闭 CSRF前后端分离场景下禁用
.csrf(csrf -> csrf.disable())
// 配置跨域
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 配置认证失败处理器(返回 JSON 而非默认页面)
.exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthEntryPoint))
// 配置 URL 权限规则
.authorizeHttpRequests(auth -> auth
// 放行 Swagger UI 相关路径
.requestMatchers(
"/api/auth/login",
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs/**", // SpringDoc OpenAPI 3.0 文档接口
"/swagger-resources/**" // 旧版 Swagger 资源
).permitAll()
// 其他路径需要认证
.anyRequest().authenticated()
)
// 不使用 Session无状态
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 添加 JWT 过滤器(在 UsernamePasswordAuthenticationFilter 之前执行)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 跨域配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*"); // 允许所有源(生产环境需限制)
config.addAllowedHeader("*"); // 允许所有请求头
config.addAllowedMethod("*"); // 允许所有请求方法
config.setAllowCredentials(true); // 允许携带 Cookie
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}

View File

@@ -0,0 +1,19 @@
package com.ski.lichuan.admin.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringDocConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("SKI接口文档") // API 标题ApiFox 中显示)
.version("1.0.0") // 版本号
.description("用于用户登录、信息查询的接口文档,适配 ApiFox")); // 描述
}
}

View File

@@ -0,0 +1,95 @@
package com.ski.lichuan.admin.controller.auth;
import com.ski.lichuan.admin.controller.auth.dto.LoginRequest;
import com.ski.lichuan.common.utils.JwtUtils;
import com.ski.lichuan.model.SysUser;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "*") // 根据实际需要调整CORS策略
@Tag(name = "认证模块", description = "用户登录、登出、获取用户信息等接口")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
/**
* 用户登录接口
*
* @param loginRequest 登录请求参数
* @return 登录结果
*/
@PostMapping("/login")
@Operation(summary = "用户登录", description = "根据用户名和密码登录返回JWT Token")
public ResponseEntity<Map<String, Object>> login(@RequestBody LoginRequest loginRequest) {
// 1. 构造认证请求(用户名+密码)
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
// 2. 触发认证(会调用 UserDetailsService.loadUserByUsername 验证用户)
Authentication authentication = authenticationManager.authenticate(authToken);
// 3. 认证成功,生成 Token
SysUser user = (SysUser) authentication.getPrincipal();
String token = jwtUtils.generateToken(user.getUsername());
// 4. 构建响应体
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "登录成功");
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("nickname", user.getNickname());
response.put("data", data);
return ResponseEntity.ok(response);
}
/**
* 获取当前用户信息接口
*
* @return 用户信息
*/
@GetMapping("/userinfo")
@Operation(summary = "获取当前用户信息", description = "根据JWT Token获取当前登录用户的基本信息")
public ResponseEntity<Map<String, Object>> getUserInfo() {
log.info("获取用户信息请求");
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String nickname = ((SysUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getNickname();
return ResponseEntity.ok(Map.of("username", username, "nickname", nickname));
}
// 内部类:响应结果
@Data
public static class LoginVo {
private String token;
private String nickname;
public LoginVo(String token, String nickname) {
this.token = token;
this.nickname = nickname;
}
}
}

View File

@@ -0,0 +1,33 @@
package com.ski.lichuan.admin.controller.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "登录请求参数")
public class LoginRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 验证码(可选)
*/
private String captcha;
/**
* 记住我
*/
private Boolean rememberMe = false;
}

View File

@@ -0,0 +1,49 @@
package com.ski.lichuan.admin.filter;
import com.ski.lichuan.common.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头获取 Token格式Bearer <token>
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7); // 截取 "Bearer " 后的内容
// 2. 验证 Token 并获取用户名
if (jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsernameFromToken(token);
// 3. 加载用户信息并设置认证状态
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,21 @@
package com.ski.lichuan.admin.handler;
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;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 未授权
response.getWriter().write("{\"code\":401,\"message\":\"" + authException.getMessage() + "\"}");
}
}

View File

@@ -0,0 +1,13 @@
spring.application.name=ski-dashboard-admin
# ??????? postgresql
spring.datasource.url=jdbc:postgresql://localhost:5432/ski_dashboard
spring.datasource.username=postgres
spring.datasource.password=tanlifan
# Spring Session??
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always
spring.session.jdbc.table-name=SPRING_SESSION
JWT_SECRET=vf4JZhcyfdK7tJs0GZ3Qjf0dSv4BId9ITjsM2fol26gOBxM17nUySiMcV0Lo2u0Y

View File

@@ -0,0 +1,22 @@
CREATE TABLE SPRING_SESSION (
PRIMARY_ID CHAR(36) NOT NULL,
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INTEGER NOT NULL,
EXPIRY_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
);
CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_PRIMARY_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES BYTEA NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
);