From 418a30f0620097f832f743fda45c53a9356cf492 Mon Sep 17 00:00:00 2001 From: fanmuchen Date: Wed, 14 May 2025 09:26:48 +0800 Subject: [PATCH] Add auth protection sample and standardize json. --- AI_RULES.md | 5 ++++ backend/internal/handlers/logto.go | 30 ++++++++++---------- backend/internal/middleware/auth.go | 24 ++++++++++++++++ backend/internal/routes/router.go | 2 +- frontend/src/app/components/HealthStatus.tsx | 9 +++++- frontend/src/app/contexts/AuthContext.tsx | 21 ++++++++++++-- 6 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 backend/internal/middleware/auth.go diff --git a/AI_RULES.md b/AI_RULES.md index cf811d9..a8c5cc2 100644 --- a/AI_RULES.md +++ b/AI_RULES.md @@ -7,6 +7,7 @@ This is to kick start a web project with nextjs as frontend and go server as bac ``` starter/ ├── .env +├── .env.dev ├── sh/ # Shell scripts for development and deployment automation │ ├── dev.sh │ └── prod.sh @@ -42,3 +43,7 @@ All environmental variables should be managed in a centralized `.env` in the roo - API prefix: `/service/` - Next.js 15.3.2 (App Router, Turbopack enabled, Import alias: `@/*`) - Logto 1.27.0 + +## Rules + +- Frontend and backend should communicate with standard JSON. diff --git a/backend/internal/handlers/logto.go b/backend/internal/handlers/logto.go index 16e838b..e5baa9b 100644 --- a/backend/internal/handlers/logto.go +++ b/backend/internal/handlers/logto.go @@ -15,11 +15,11 @@ import ( // In production, use Redis/MongoDB type SessionStorage struct { - session sessions.Session + Session sessions.Session } func (s *SessionStorage) GetItem(key string) string { - value := s.session.Get(key) + value := s.Session.Get(key) if value == nil { return "" } @@ -31,12 +31,12 @@ func (s *SessionStorage) GetItem(key string) string { } func (s *SessionStorage) SetItem(key, value string) { - s.session.Set(key, value) - s.session.Save() + s.Session.Set(key, value) + s.Session.Save() } -// getLogtoConfig returns Logto config from environment variables -func getLogtoConfig() *client.LogtoConfig { +// GetLogtoConfig returns Logto config from environment variables +func GetLogtoConfig() *client.LogtoConfig { return &client.LogtoConfig{ Endpoint: os.Getenv("LOGTO_ENDPOINT"), AppId: os.Getenv("LOGTO_APP_ID"), @@ -48,7 +48,7 @@ func getLogtoConfig() *client.LogtoConfig { // Note: The /service/auth endpoint is for debugging purposes only func HomeHandler(ctx *gin.Context) { session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session}) + logtoClient := client.NewLogtoClient(GetLogtoConfig(), &SessionStorage{Session: session}) var debugInfo string debugInfo = "

Logto Auth Debugging Page

" @@ -141,11 +141,11 @@ func HomeHandler(ctx *gin.Context) { // SignInHandler starts the Logto sign-in flow func SignInHandler(ctx *gin.Context) { session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session}) + logtoClient := client.NewLogtoClient(GetLogtoConfig(), &SessionStorage{Session: session}) redirectUri := os.Getenv("LOGTO_REDIRECT_URI") signInUri, err := logtoClient.SignIn(redirectUri) if err != nil { - ctx.String(http.StatusInternalServerError, err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } ctx.Redirect(http.StatusTemporaryRedirect, signInUri) @@ -154,10 +154,10 @@ func SignInHandler(ctx *gin.Context) { // CallbackHandler handles the Logto sign-in callback func CallbackHandler(ctx *gin.Context) { session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session}) + logtoClient := client.NewLogtoClient(GetLogtoConfig(), &SessionStorage{Session: session}) err := logtoClient.HandleSignInCallback(ctx.Request) if err != nil { - ctx.String(http.StatusInternalServerError, err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Redirect to the frontend page instead of the backend auth page @@ -167,11 +167,11 @@ func CallbackHandler(ctx *gin.Context) { // SignOutHandler starts the Logto sign-out flow func SignOutHandler(ctx *gin.Context) { session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session}) + logtoClient := client.NewLogtoClient(GetLogtoConfig(), &SessionStorage{Session: session}) postSignOutRedirectUri := os.Getenv("LOGTO_POST_SIGN_OUT_REDIRECT_URI") signOutUri, err := logtoClient.SignOut(postSignOutRedirectUri) if err != nil { - ctx.String(http.StatusOK, err.Error()) + ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) return } ctx.Redirect(http.StatusTemporaryRedirect, signOutUri) @@ -180,10 +180,10 @@ func SignOutHandler(ctx *gin.Context) { // UserIdTokenClaimsHandler returns the user's ID token claims as JSON func UserIdTokenClaimsHandler(ctx *gin.Context) { session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session}) + logtoClient := client.NewLogtoClient(GetLogtoConfig(), &SessionStorage{Session: session}) idTokenClaims, err := logtoClient.GetIdTokenClaims() if err != nil { - ctx.String(http.StatusOK, err.Error()) + ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, idTokenClaims) diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go new file mode 100644 index 0000000..4655153 --- /dev/null +++ b/backend/internal/middleware/auth.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "net/http" + + "starter/backend/internal/handlers" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/logto-io/go/client" +) + +// AuthRequired 是 Logto 认证中间件,未认证用户返回 401 +func AuthRequired() gin.HandlerFunc { + return func(c *gin.Context) { + session := sessions.Default(c) + logtoClient := client.NewLogtoClient(handlers.GetLogtoConfig(), &handlers.SessionStorage{Session: session}) + if !logtoClient.IsAuthenticated() { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + c.Next() + } +} \ No newline at end of file diff --git a/backend/internal/routes/router.go b/backend/internal/routes/router.go index 6ee8085..a421a3f 100644 --- a/backend/internal/routes/router.go +++ b/backend/internal/routes/router.go @@ -16,7 +16,7 @@ func RegisterRoutes(rg *gin.RouterGroup) { rg.Use(middleware.Logger()) // Define routes - rg.GET("/health", handlers.HealthCheck) + rg.GET("/health", middleware.AuthRequired(), handlers.HealthCheck) // Logto authentication routes rg.GET("/auth/", handlers.HomeHandler) diff --git a/frontend/src/app/components/HealthStatus.tsx b/frontend/src/app/components/HealthStatus.tsx index 0b11df3..87b5d26 100644 --- a/frontend/src/app/components/HealthStatus.tsx +++ b/frontend/src/app/components/HealthStatus.tsx @@ -16,7 +16,14 @@ export default function HealthStatus() { throw new Error(`Server responded with status: ${response.status}`); } - const data = await response.json(); + let data; + try { + data = await response.json(); + } catch (jsonErr) { + setError("Response is not valid JSON"); + setHealth({}); + return; + } setHealth(data); } catch (err) { setError( diff --git a/frontend/src/app/contexts/AuthContext.tsx b/frontend/src/app/contexts/AuthContext.tsx index da23c36..4cfe38b 100644 --- a/frontend/src/app/contexts/AuthContext.tsx +++ b/frontend/src/app/contexts/AuthContext.tsx @@ -43,9 +43,24 @@ export function AuthProvider({ children }: { children: ReactNode }) { ); if (response.ok) { - const userData = await response.json(); - setUser(userData); - setIsAuthenticated(true); + let userData; + try { + userData = await response.json(); + } catch (jsonErr) { + // 不是 JSON,降级处理 + setUser(null); + setIsAuthenticated(false); + setLoading(false); + return; + } + // 处理后端返回 { error: ... } + if (userData && userData.error) { + setUser(null); + setIsAuthenticated(false); + } else { + setUser(userData); + setIsAuthenticated(true); + } } else { setUser(null); setIsAuthenticated(false);