paint-brush
በFastAPI፣ Redis እና JWT የብዝሃ-መሣሪያ ማረጋገጫ ስርዓትን እንዴት መተግበር እንደሚቻል@emperorsixpacks
አዲስ ታሪክ

በFastAPI፣ Redis እና JWT የብዝሃ-መሣሪያ ማረጋገጫ ስርዓትን እንዴት መተግበር እንደሚቻል

Andrew David 10m2024/10/09
Read on Terminal Reader

በጣም ረጅም፤ ማንበብ

በዚህ መማሪያ ውስጥ፣ ጥያቄዎችን ለመፍቀድ JWT (JSON Web Tokens) እንጠቀማለን። አፕሊኬሽናችን ሀገር አልባ ስለሆነ (ያለፉት ጥያቄዎች ትውስታ የለውም ማለት ነው) የክፍለ ጊዜ እና የተጠቃሚ ውሂብ የምንልክበት መንገድ እንፈልጋለን። JWT አገር በሌለው አፕሊኬሽኖች ውስጥ ማረጋገጫን ለማስተዳደር ተስማሚ ነው እና በጣም ጥሩ ስራ ይሰራል።
featured image - በFastAPI፣ Redis እና JWT የብዝሃ-መሣሪያ ማረጋገጫ ስርዓትን እንዴት መተግበር እንደሚቻል
Andrew David  HackerNoon profile picture
0-item
1-item



ብዙ ትላልቅ የቴክኖሎጂ ኩባንያዎች ተጠቃሚዎች በበርካታ መሳሪያዎች ላይ እንዲገቡ የሚያስችል ጥሩ ባህሪ ያቀርባሉ. ተጠቃሚዎች መሣሪያዎቻቸውን ማስተዳደር፣ የትኛዎቹ በመለያ እንደገቡ ማየት፣ እና ወደ መለያ የገቡባቸውን ማንኛውንም መሣሪያ በመጠቀም ከማንኛውም መሣሪያ ዘግተው መውጣት ይችላሉ። ዛሬ፣ Redis እና JWTን በመጠቀም ተመሳሳይ የማረጋገጫ ዘዴን እንዴት ተግባራዊ ማድረግ እንደምችል ማሰስ እፈልጋለሁ።

ስለዚህ ይህ በትክክል እንዴት ይሠራል?

ለዚህም, ለመጠቀም ወስኛለሁ;

  • ፈጣን-ኤፒአይ ለኋላ-መጨረሻ

  • Redis ለመሸጎጫ


ጥያቄዎችን ለመፍቀድ JWT (JSON Web Tokens) እንጠቀማለን። አፕሊኬሽናችን ሀገር አልባ ስለሆነ (ያለፉት ጥያቄዎች ትውስታ የለውም ማለት ነው) የክፍለ ጊዜ እና የተጠቃሚ ውሂብ የምንልክበት መንገድ እንፈልጋለን። JWT ሀገር በሌለው አፕሊኬሽኖች ውስጥ ማረጋገጫን ለማስተዳደር ተስማሚ ነው፣ እና ጥሩ ስራ ይሰራል።


ነገር ግን፣ የJWT አንዱ ዝቅጠት በቶከን ውስጥ ብዙ የተከፈለ ጭነት ባካተትክ ቁጥር እሱ ይረዝማል። ለስርዓታችን፣ በቶከን ውስጥ ያለውን session_id እና username ብቻ ለማካተት ወስኛለሁ። ይህ መረጃ ማስመሰያው ከመጠን በላይ ትልቅ ሳያደርጉ ጥያቄዎችን ለመፍቀድ በቂ ነው ። ጥያቄዎችን ለመፍቀድ JWT (JSON Web Tokens) ይጠቀማል። አፕሊኬሽናችን ሀገር አልባ ስለሆነ (ያለፉት ጥያቄዎች ትውስታ የለውም ማለት ነው) የክፍለ ጊዜ እና የተጠቃሚ ውሂብ የምንልክበት መንገድ እንፈልጋለን። JWT ሀገር በሌለው አፕሊኬሽኖች ውስጥ ማረጋገጥን ለማስተዳደር ተስማሚ ነው፣ እና በዚህ ረገድ ጥሩ ስራ ይሰራል።

ክፍለ_መታወቂያ ተናግረሃል? እኛ ግን JWT እና መተግበሪያችንን አገር አልባ በሆነ መንገድ እየተጠቀምን ነው።

በዚህ አውድ ውስጥ፣ "ክፍለ-ጊዜ" የሚያመለክተው አንድ ተጠቃሚ ከእኛ መተግበሪያ ጋር የሚገናኝበትን መሳሪያ ወይም ዘዴ ነው። በዋናነት ተጠቃሚው የገባበት መሳሪያ ነው። ተጠቃሚው የመግባት ጥያቄ ባቀረበ ቁጥር በስርዓታችን ውስጥ ሁሉንም ተዛማጅ መረጃዎችን የያዘ አዲስ ክፍለ ጊዜ (መሳሪያ) እንፈጥራለን። ይህ ውሂብ ለወደፊት ጥያቄዎች በRedis ውስጥ ይከማቻል።

እሺ ኮድ እንያዝ 😁

የመጀመሪያው ነገር በአካባቢዎ ማሽን ላይ Redis መጫኑን ማረጋገጥ ነው. Redisን ለመጫን ወደ https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/ ይሂዱ እና ለስርዓተ ክወናዎ ልዩ መመሪያዎችን ይከተሉ።


በመቀጠል ፓይቶን እንጭናለን. ለዚህም፣ እኔ python 3.11 እጠቀማለሁ (ወደ 3.12 ማሻሻል አስፈላጊ መሆኑን እስካሁን አላየሁም ፣ እውነቱን ለመናገር 3.11 የምጠቀምበት ብቸኛው ምክንያት በ StrEnum ምክንያት ነው ፣ ከዚያ ውጭ አሁንም 3.10 እወዳለሁ)


በመቀጠል, ግጥም መጫን አለብን, ይህ እኔ የምጠቀምበት የጥቅል አስተዳዳሪ ነው

 pip install poetry # or python3.11 -m install poetry


ያ ተስተካክሏል፣ ስለዚህ ይቀጥሉ እና ሪፖውን ይዝጉ

 git clone https://github.com/emperorsixpacks/multi_device_sign_in_with_redis.git && cd server poetry shell && poetry install # create a new virtual environment and install all dependanceies

የእኛን Redis ግንኙነት በማዘጋጀት ላይ

 import os from redis import Redis load_dotenv(".env") REDIS_HOST = os.getenv("REDIS_HOST", "localhost") REDIS_PORT = os.getenv("REDIS_PORT", "6379") redis_client = Redis(REDIS_HOST, int(REDIS_PORT))

የውሂብ ጎታ ማዋቀር

demo_users.json ውስጥ የማሳያ ዳታቤዝ ፈጠርኩ ይህም ለዚህ አጋዥ ስልጠና የምንጠቀመው ነው።

 { "user124": { "username": "user124", "email": "[email protected]", "password": "1234", "bio": "This is my brief bio" }, "user123": { "username": "user123", "email": "[email protected]", "password": "1234", "bio": "This is my brief bio" } }

አሁን፣ ለዳታ ቤታችን የኛን ንድፎች እና አጋዥ ተግባራቶችን ማከል አለብን። ለማጠቃለል፣ ሁሉንም ኮድ እዚህ አላስቀምጥም።

 @dataclass class Session: """ A class to represent a user's session. Attributes: session_id (str): A unique id for the session. device_name (str): The name of the device used for the session. ip_address (str): The ip address of the device used for the session. device_id (str): A unique id for the device. date_created (datetime): The date and time the session was created. """ session_id: str = field(default_factory=create_new_session_id) device_name: str = field(default=None) ip_address: str = field(default=None) device_id: str = field(default_factory=generate_new_device_id) date_created: datetime = field(default_factory=now_date_time_to_str) @dataclass class User: """ A class to represent a user. Attributes: username (str): The username of the user. email (str): The email of the user. password (str): The password of the user. bio (str): The bio of the user. sessions (List[Session] | None): A list of Session objects representing the user's sessions. """ username: str = field(default=None) email: str = field(default=None) password: str = field(default=None) bio: str = field(default=None) sessions: List[Session] | None = None @property def __dict__(self): """ Returns a dictionary representing the user. Returns: Dict[str, Any]: A dictionary representing the user """ return { "username": self.username, "email": self.email, "password": self.password, "bio": self.bio, "sessions": self.return_session_dict(), } def return_session_dict(self): """ Returns a list of dictionaries representing the user's sessions. If the sessions field is a list of Session objects, returns a list of dictionaries where each dictionary is the __dict__ of a Session object. If the sessions field is a list of dictionaries, returns the list as is. Returns: List[Dict[str, Any]]: A list of dictionaries representing the user's sessions """ try: return [session.__dict__ for session in self.sessions] except AttributeError: return [session for session in self.sessions] # Utiliy finctions def return_user_from_db(username) -> User | None: """ Retrieves a user from the database by their username. Args: username (str): The username of the user to be retrieved Returns: User | None: The user if found, None otherwise """ with open("demo_users.json", "r", encoding="utf-8") as file: user = json.load(file).get(str(username), None) return User(**user) or None

የአገልጋይ ማዋቀር

መተግበሪያችንን ለማስኬድ FastAPI እየተጠቀምን ነው፣ ስለዚህ እንሂድ እና ያንን እናዋቅረው።

 # Setting up server from fastapi import FastAPI from fastapi.responses import JSONResponse app = FastAPI( name="Multi device sign in with Redis", description="Multi device sign in with Redis in stateless applications", ) @app.get("/") def index_route(): return JSONResponse(content={"Message": "hello, this seems to be working :)"}) if __name__ == "__main__": import uvicorn uvicorn.run("server:app", host="0.0.0.0", port=8000, reload=True, use_colors=True)


እሺ ይህ ጥሩ ነው የእኛ መተግበሪያ በጥሩ ሁኔታ እየመጣ ያለ ይመስላል

ይግቡ / ይግቡ

ተጠቃሚው ወደ ስርዓቱ በገባ ቁጥር session_id ን ለመፍጠር እና ያንን ክፍለ ጊዜ በሬዲስ ውስጥ የምናከማችበት እና ከሌሎች ክፍለ-ጊዜዎቻቸው ጋር የምንይዝበት መንገድ እንፈልጋለን።


አንድ ተጠቃሚ ሲገባ መጀመሪያ ጥያቄውን ትክክለኛ መሆኑን ለማረጋገጥ እናረጋግጣለን። አንዴ ከተረጋገጠ በኋላ ሁሉንም የመሳሪያውን መረጃ ከጥያቄው ማውጣት እንችላለን። ከዚያ በኋላ፣ ይህን መረጃ በሬዲስ ውስጥ እናከማቻለን፣ አዲስ ቶከን እንፈጥራለን እና ያንን ማስመሰያ ለተጠቃሚው እንመልሳለን።

 @app.post("/login") def login_route( form: Annotated[LoginForm, Depends()], request: Request ) -> JSONResponse: """ Handles a login request. Args: form (Annotated[LoginForm, Depends()]): The form data containing the username and password request (Request): The request containing the User-Agent header and client host Returns: JSONResponse: A JSON response containing a JWT token if the login is successful, otherwise a JSONResponse with a 404 status code and a message indicating that the username or password is invalid """ username = form.username password = form.password # Authenticate the user user = authenticate_user(username, password) if user is None: return JSONResponse( status_code=404, content={"message": "Invalid username or password"} ) # Create a new session session = Session( device_name=request.headers.get("User-Agent"), ip_address=request.client.host ) # Get the user from the cache user_from_cache = get_user_from_cache(username) if user_from_cache is None: return JSONResponse(content={"message": "one minute"}, status_code=404) # Get the user's sessions user_sessions = get_sessions(userid=username) # Add the new session to the user's sessions try: user_sessions.append(session) except AttributeError: user_sessions = [session] # Update the user in the cache user_from_cache.sessions = user_sessions update_user_cache(userid=username, new_data=user_from_cache) # Create a JWT token token = create_token(Token(user=username, session_id=session.session_id)) # Return the JWT token return JSONResponse(content={"message": "logged in", "token": token})

ዘግተው መውጣት/ውጣ

ይህ ቀላሉ ክፍል ነው። ተጠቃሚው ለመተግበሪያችን በጠየቀ ቁጥር session_id እና username ለማግኘት የ Bearer tokenን እንፈታለን። ከዚያ username በመጠቀም Redisን መጠየቅ እንችላለን።


ተዛማጅ ካገኘን፣ ከክፍለ session_id ጋር የተገናኘውን ክፍለ ጊዜ ከዲኮድ ቶከን እናስወግደዋለን። ለምሳሌ፣ ክፍለ-ጊዜው ከሌለ፣ በቀላሉ ለተጠቃሚው መልእክት እንመልሳለን። ይህ የሚያመለክተው ተጠቃሚው ቀድሞውንም ከዚያ መሣሪያ ከሌላ መሣሪያ እንደወጣ ወይም ምልክቱ የተሳሳተ መሆኑን ነው።

 @app.post("/logout") def logout_route(request: Request): """ Handles a request to log out the user. This endpoint will delete the user's session from the cache and return a JSON response with a message indicating that the user has been logged out. Args: request (Request): The request containing the Authorization header with the JWT token Returns: JSONResponse: A JSON response containing the message "logged out" if the token is valid, otherwise a JSONResponse with a 404 status code and a message indicating that the token is invalid """ # Get the JWT token from the Authorization header _, token = get_authorization_scheme_param(request.headers.get("Authorization")) # Decode the JWT token payload = decode_token(token) # Check if the token is invalid if payload is None: return JSONResponse(content={"message": "Invalid token"}, status_code=404) # Check if the user or session does not exist if get_single_session(userid=payload.user, session_id=payload.session_id) is None or get_user_from_cache( userid=payload.user) is None: return JSONResponse(content={"message": "Invalid token"}, status_code=404) # Delete the session from the cache delete_session(payload.user, payload.session_id) # Return a JSON response with a message indicating that the user has been logged out return JSONResponse(content={"message": "logged out"})


አዎ፣ ያ በጣም ከባድ አልነበረም፣ አይደል? ይህንን ፕሮጀክት ለሁለት ሳምንታት በጭንቅላቴ ውስጥ አግኝቼዋለሁ፣ እና እሱን መሞከር ፈለግሁ። ምንም እንኳን ይህ ስርዓት ሙሉ በሙሉ ፍፁም ባይሆንም (ማለቴ፣ ምንም አይነት ስርዓት ከጉድለት የጸዳ አይደለም) ይህንን የተሻለ ማድረግ እንደምንችል ግልጽ ነው። ለምሳሌ፣ እንደ Curl ወይም የኮንሶል አፕ ወይም ፖስታተኛ ካሉ ቦታዎች የሚመጡ ጥያቄዎችን እንዴት ነው የምናስተዳድረው? ከእነዚህ ምንጮች የሚመጡ በርካታ ጥያቄዎች ወደ ብዙ ክፍለ ጊዜዎች ሊመሩ ይችላሉ፣ ስለዚህ የእኛን db አላስፈላጊ በሆነ መረጃ መሙላት። አዎ፣ ጥያቄው ከየት እንደመጣ ለማየት እና ያንን ለማስተናገድ አመክንዮአችን መፍጠር እንችላለን፣ ግን እውነቱን ለመናገር ያ ብዙ ስራ ነው። ለዚህም ነው እርስዎ እውነተኛ “agba” (ከፍተኛ መሐንዲስ) ካልሆኑ በስተቀር ለምርት መተግበሪያዎች የፈቃድ እና የማረጋገጫ ስርዓቶችን እንዲገነቡ የማልመክረው። OAuth 2.0 (Google ወይም Apple) ወይም እንደ Kinde ወይም Auth0 ያሉ ውጫዊ አቅራቢዎችን ብጠቀም እመርጣለሁ። እና እንደ እኔ ከተሰበሩ እና EdgeDB እየተጠቀሙ ከሆነ ከሳጥኑ ውጭ ለመጠቀም ዝግጁ ከሆነ የማረጋገጫ ስርዓት ጋር ይመጣል። በዚህ መንገድ፣ የሆነ ነገር ከተፈጠረ፣ ተለማማጅ ብቻ ሳይሆን ሌላ የሚወቅስ ሰው አለህ።