diff --git a/pom.xml b/pom.xml index 4191414..4f21cef 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,16 @@ 1.18.42 provided + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-web + diff --git a/src/main/java/com/chinaweal/youfool/course/config/SaTokenConfig.java b/src/main/java/com/chinaweal/youfool/course/config/SaTokenConfig.java new file mode 100644 index 0000000..faf76f9 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/course/config/SaTokenConfig.java @@ -0,0 +1,34 @@ +package com.chinaweal.youfool.course.config; + +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Sa-Token配置 + * + * @author lroyia + * @since 2025/10/24 + **/ +@Configuration +public class SaTokenConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册Sa-Token拦截器 + registry.addInterceptor(new SaInterceptor()) + .addPathPatterns("/**") + .excludePathPatterns( + "/login", + "/error", + "/webjars/**", + "/css/**", + "/js/**", + "/user/auth/**", + "/oauth2/**" + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/course/controller/LoginController.java b/src/main/java/com/chinaweal/youfool/course/controller/LoginController.java index 2bae687..b9aecaf 100644 --- a/src/main/java/com/chinaweal/youfool/course/controller/LoginController.java +++ b/src/main/java/com/chinaweal/youfool/course/controller/LoginController.java @@ -9,7 +9,10 @@ import com.chinaweal.youfool.framework.springboot.rest.RestResult; import com.chinaweal.youfool.framework.springboot.rest.ResultCode; import com.chinaweal.youfool.framework.springboot.user.entity.UserBase; import com.chinaweal.youfool.course.common.constants.SessionConstants; +import com.chinaweal.youfool.course.entity.SysUser; +import com.chinaweal.youfool.course.service.SysUserService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -30,6 +33,9 @@ import java.util.Set; @RequestMapping("/user/auth") public class LoginController extends BaseController { + @Autowired + private SysUserService sysUserService; + /** * 登录接口 * @@ -53,17 +59,26 @@ public class LoginController extends BaseController { } } - // TODO:与数据库匹配校验,具体按用户信息表 + // 查询用户信息 + SysUser sysUser = sysUserService.getUserByUsername(username); + if (sysUser == null || !sysUser.getPassword().equals(password)) { + return RestResult.error(ResultCode.BUSINESS_LOGIC_ERROR, "用户名或密码错误"); + } - // 匹配成功的话,登记登录信息 TODO:这里的userId是用户唯一号,应根据实际的数据库信息进行替换 - StpUtil.login("userId"); + // 匹配成功的话,登记登录信息 + StpUtil.login(sysUser.getUserId()); SaSession session = StpUtil.getSession(); - // 将用户信息存储至session TODO:登录信息存到这里 + // 将用户信息存储至session UserBase userBase = new UserBase(); + userBase.setUsername(sysUser.getUsername()); + userBase.setEmail(sysUser.getEmail()); + userBase.setAvatar(sysUser.getAvatar()); + userBase.setId(sysUser.getUserId()); + Set permissionSet = new HashSet<>(); permissionSet.add("admin"); userBase.setPermission(permissionSet); - session.set("user", userBase); + session.set(SessionConstants.USER_KEY, userBase); return RestResult.ok(); } diff --git a/src/main/java/com/chinaweal/youfool/course/controller/OAuth2Controller.java b/src/main/java/com/chinaweal/youfool/course/controller/OAuth2Controller.java new file mode 100644 index 0000000..c763dec --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/course/controller/OAuth2Controller.java @@ -0,0 +1,182 @@ +package com.chinaweal.youfool.course.controller; + +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; +import com.chinaweal.youfool.framework.springboot.user.entity.UserBase; +import com.chinaweal.youfool.course.common.constants.SessionConstants; +import com.chinaweal.youfool.course.entity.SysUser; +import com.chinaweal.youfool.course.service.SysUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.view.RedirectView; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * OAuth2控制器 + * + * @author lroyia + * @since 2025/10/24 + **/ +@Slf4j +@Controller +@RequestMapping("/oauth2") +public class OAuth2Controller { + + @Value("${gitea.client-id}") + private String clientId; + + @Value("${gitea.client-secret}") + private String clientSecret; + + @Value("${gitea.auth-url}") + private String authUrl; + + @Value("${gitea.token-url}") + private String tokenUrl; + + @Value("${gitea.user-url}") + private String userUrl; + + @Value("${gitea.redirect-uri}") + private String redirectUri; + + @Autowired + private SysUserService sysUserService; + + /** + * 跳转到Gitea授权页面 + * + * @return 重定向到Gitea授权页面 + */ + @GetMapping("/gitea/authorize") + public RedirectView authorize() { + String state = String.valueOf(System.currentTimeMillis()); + String url = String.format("%s?client_id=%s&redirect_uri=%s&response_type=code&state=%s&scope=read:user", + authUrl, clientId, redirectUri, state); + return new RedirectView(url); + } + + /** + * Gitea回调处理 + * + * @param code 授权码 + * @param state 状态 + * @return 重定向到首页 + */ + @GetMapping("/gitea/callback") + public RedirectView callback(@RequestParam String code, @RequestParam String state) { + try { + // 获取访问令牌 + Map tokenParams = new HashMap<>(); + tokenParams.put("client_id", clientId); + tokenParams.put("client_secret", clientSecret); + tokenParams.put("code", code); + tokenParams.put("grant_type", "authorization_code"); + tokenParams.put("redirect_uri", redirectUri); + + RestTemplate restTemplate = new RestTemplate(); + Map tokenResponse = restTemplate.postForObject(tokenUrl, tokenParams, Map.class); + + if (tokenResponse == null || !tokenResponse.containsKey("access_token")) { + throw new RuntimeException("获取access_token失败"); + } + + String accessToken = (String) tokenResponse.get("access_token"); + + // 获取用户信息 + Map userInfo = getUserInfo(accessToken); + + // 处理用户登录 + handleUserLogin(userInfo); + + return new RedirectView("/course/"); + + } catch (Exception e) { + log.error("Gitea OAuth2回调处理失败", e); + return new RedirectView("/course/login?error=true&message=" + e.getMessage()); + } + } + + /** + * 获取用户信息 + * + * @param accessToken 访问令牌 + * @return 用户信息 + */ + private Map getUserInfo(String accessToken) { + RestTemplate restTemplate = new RestTemplate(); + Map headers = new HashMap<>(); + headers.put("Authorization", "token " + accessToken); + + org.springframework.http.HttpEntity entity = new org.springframework.http.HttpEntity<>(headers); + Map userInfo = restTemplate.exchange(userUrl, + org.springframework.http.HttpMethod.GET, entity, Map.class).getBody(); + + return userInfo; + } + + /** + * 处理用户登录 + * + * @param userInfo 用户信息 + */ + private void handleUserLogin(Map userInfo) { + String username = (String) userInfo.get("username"); + String email = (String) userInfo.get("email"); + String avatarUrl = (String) userInfo.get("avatar_url"); + Integer id = (Integer) userInfo.get("id"); + + log.info("Gitea用户登录成功: username={}, email={}, id={}", username, email, id); + + // 生成Gitea Open ID + String giteaOpenId = String.valueOf(id); + + // 检查用户是否已存在 + SysUser sysUser = sysUserService.getUserByGiteaOpenId(giteaOpenId); + + // 如果用户不存在,则创建新用户 + if (sysUser == null) { + sysUser = new SysUser(); + sysUser.setUsername(username); + sysUser.setNickname(username); + sysUser.setEmail(email); + sysUser.setAvatar(avatarUrl); + sysUser.setGiteaOpenId(giteaOpenId); + sysUserService.save(sysUser); + } else { + // 更新用户信息 + sysUser.setEmail(email); + sysUser.setAvatar(avatarUrl); + sysUserService.updateById(sysUser); + } + + // 使用Sa-Token登录 + StpUtil.login(sysUser.getUserId()); + SaSession session = StpUtil.getSession(); + + // 构建用户信息 + UserBase userBase = new UserBase(); + userBase.setUsername(sysUser.getUsername()); + userBase.setEmail(sysUser.getEmail()); + userBase.setAvatarUrl(sysUser.getAvatar()); + userBase.setId(sysUser.getUserId()); + + // 设置权限 + Set permissionSet = new HashSet<>(); + permissionSet.add("user"); + userBase.setPermission(permissionSet); + + // 将用户信息存储至session + session.set(SessionConstants.USER_KEY, userBase); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/course/controller/PageController.java b/src/main/java/com/chinaweal/youfool/course/controller/PageController.java new file mode 100644 index 0000000..ac44ee7 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/course/controller/PageController.java @@ -0,0 +1,51 @@ +package com.chinaweal.youfool.course.controller; + +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * 页面控制器 + * + * @author lroyia + * @since 2025/10/24 + **/ +@Controller +public class PageController { + + /** + * 登录页面 + * + * @return 登录页面 + */ + @GetMapping("/login") + public String loginPage() { + return "login"; + } + + /** + * 首页 + * + * @return 首页 + */ + @GetMapping("/") + public String index() { + if (!StpUtil.isLogin()) { + return "redirect:/course/login"; + } + return "index"; + } + + /** + * 首页 + * + * @return 首页 + */ + @GetMapping("/index") + public String indexPage() { + if (!StpUtil.isLogin()) { + return "redirect:/course/login"; + } + return "index"; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/course/controller/TestController.java b/src/main/java/com/chinaweal/youfool/course/controller/TestController.java new file mode 100644 index 0000000..56faf97 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/course/controller/TestController.java @@ -0,0 +1,62 @@ +package com.chinaweal.youfool.course.controller; + +import cn.dev33.satoken.stp.StpUtil; +import com.chinaweal.youfool.framework.springboot.rest.RestResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试控制器 + * + * @author lroyia + * @since 2025/10/24 + **/ +@Slf4j +@RestController +@RequestMapping("/test") +public class TestController { + + /** + * 测试接口,需要登录 + * + * @return 测试结果 + */ + @GetMapping("/hello") + public RestResult hello() { + return RestResult.ok("Hello, " + StpUtil.getLoginIdAsString() + "!"); + } + + /** + * 测试管理员权限 + * + * @return 测试结果 + */ + @GetMapping("/admin") + public RestResult admin() { + StpUtil.checkPermission("admin"); + return RestResult.ok("Admin access granted!"); + } + + /** + * 测试用户权限 + * + * @return 测试结果 + */ + @GetMapping("/user") + public RestResult user() { + StpUtil.checkPermission("user"); + return RestResult.ok("User access granted!"); + } + + /** + * 获取当前登录信息 + * + * @return 登录信息 + */ + @GetMapping("/info") + public RestResult info() { + return RestResult.ok("Current login id: " + StpUtil.getLoginIdAsString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/course/entity/SysUser.java b/src/main/java/com/chinaweal/youfool/course/entity/SysUser.java index 010d210..0ade793 100644 --- a/src/main/java/com/chinaweal/youfool/course/entity/SysUser.java +++ b/src/main/java/com/chinaweal/youfool/course/entity/SysUser.java @@ -81,6 +81,15 @@ public class SysUser implements Serializable { @Schema(description = "邮箱", example = "user@example.com") private String email; + /** + * 密码 + * + *

用户的登录密码,存储时应该加密处理。

+ */ + @TableField("password") + @Schema(description = "密码", example = "encrypted_password") + private String password; + /** * Gitea Open ID * @@ -114,16 +123,18 @@ public class SysUser implements Serializable { * @param name 姓名 * @param avatar 头像 * @param email 邮箱 + * @param password 密码 * @param giteaOpenId Gitea Open ID */ public SysUser(String userId, String username, String nickname, String name, - String avatar, String email, String giteaOpenId) { + String avatar, String email, String password, String giteaOpenId) { this.userId = userId; this.username = username; this.nickname = nickname; this.name = name; this.avatar = avatar; this.email = email; + this.password = password; this.giteaOpenId = giteaOpenId; } @@ -136,6 +147,7 @@ public class SysUser implements Serializable { ", name='" + name + '\'' + ", avatar='" + avatar + '\'' + ", email='" + email + '\'' + + ", password='" + password + '\'' + ", giteaOpenId='" + giteaOpenId + '\'' + '}'; } diff --git a/src/main/java/com/chinaweal/youfool/course/service/SysUserService.java b/src/main/java/com/chinaweal/youfool/course/service/SysUserService.java index 6e64313..5c94dde 100644 --- a/src/main/java/com/chinaweal/youfool/course/service/SysUserService.java +++ b/src/main/java/com/chinaweal/youfool/course/service/SysUserService.java @@ -22,4 +22,20 @@ import com.chinaweal.youfool.course.entity.SysUser; * @since 1.0.0 */ public interface SysUserService extends IService { + + /** + * 根据用户名获取用户信息 + * + * @param username 用户名 + * @return 用户信息 + */ + SysUser getUserByUsername(String username); + + /** + * 根据Gitea Open ID获取用户信息 + * + * @param giteaOpenId Gitea Open ID + * @return 用户信息 + */ + SysUser getUserByGiteaOpenId(String giteaOpenId); } \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/course/service/impl/SysUserServiceImpl.java b/src/main/java/com/chinaweal/youfool/course/service/impl/SysUserServiceImpl.java index f93e41c..7108b05 100644 --- a/src/main/java/com/chinaweal/youfool/course/service/impl/SysUserServiceImpl.java +++ b/src/main/java/com/chinaweal/youfool/course/service/impl/SysUserServiceImpl.java @@ -1,5 +1,6 @@ package com.chinaweal.youfool.course.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.chinaweal.youfool.course.entity.SysUser; import com.chinaweal.youfool.course.mapper.SysUserMapper; @@ -28,4 +29,18 @@ import org.springframework.stereotype.Service; */ @Service public class SysUserServiceImpl extends ServiceImpl implements SysUserService { + + @Override + public SysUser getUserByUsername(String username) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysUser::getUsername, username); + return getOne(queryWrapper); + } + + @Override + public SysUser getUserByGiteaOpenId(String giteaOpenId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysUser::getGiteaOpenId, giteaOpenId); + return getOne(queryWrapper); + } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index db9bbd2..024daf9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,6 +7,21 @@ spring: active: dev application: name: youfool-lesson + # Thymeleaf配置 + thymeleaf: + cache: false + prefix: classpath:/templates/ + suffix: .html + encoding: UTF-8 + mode: HTML + # Gitea OAuth2配置 + gitea: + client-id: ${GITEA_CLIENT_ID:your-gitea-client-id} + client-secret: ${GITEA_CLIENT_SECRET:your-gitea-client-secret} + auth-url: ${GITEA_AUTH_URL:https://gitea.com/login/oauth/authorize} + token-url: ${GITEA_TOKEN_URL:https://gitea.com/login/oauth/access_token} + user-url: ${GITEA_USER_URL:https://gitea.com/api/v1/user} + redirect-uri: ${BASE_URL:http://localhost:8080}/course/oauth2/gitea/callback datasource: dynamic: primary: master #设置默认的数据源或者数据源组,默认值即为master diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..e93299d --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,100 @@ + + + + + + 首页 + + + +
+

课程管理系统

+ +
+ +
+
+

欢迎来到课程管理系统

+

+ 这里是一个基于Spring Boot + Thymeleaf + Gitea OAuth2的课程管理系统。 + 您可以使用用户名密码登录,也可以使用Gitea账号进行第三方登录。 +

+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..43ee642 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,203 @@ + + + + + + 登录系统 + + + + + + + + + + \ No newline at end of file