《Flask Web開發:基於Python的Web應用開發實戰》學習筆記(二)
轉載於:http://pdf.us/2017/10/05/492.html,感謝這位大神
《Flask Web開發:基於Python的Web應用開發實戰》學習筆記
這裡是第二部分的學習筆記。第二部分:例項:社交部落格程式
第八章 使用者認證
用到的擴充套件
Werkzeug:計算密碼雜湊並核對
istdangerous:生成並核對加密安全令牌Flask-Mail:傳送與認證相關的密碼
Flask-Bootstrap:HTML模板
Flask-WTF:Web表單
使用Werkzeug實現密碼雜湊
Werkzeug的security模組可以實現計算密碼雜湊。主要用於使用者註冊和驗證使用者。
generate_password_hash(password,method=pbkdf2:sha1,salt_length=8) 以密碼作為輸入,輸出密碼的雜湊值
check_password_hash(hash,password) 返回True即表示驗證通過
程式從7a版本開始推進。資料庫改用mysql。先不要建表,db init;db migrate;db upgrade生成當前資料庫。
對app/models.py中User模型做改造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | fromwerkzeug.securityimportgenerate_password_hash,check_password_hash #... #class User(db.Model): # __tablename__ = 'users' # id = db.Column(db.Integer, primary_key=True) # username = db.Column(db.String(64), unique=True, index=True) password_hash=db.Column(db.String(128)) # role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) # def __repr__(self): # return '<User %r>' % self.username @property defpassword(self): raiseAttributeError('password is not a readable attribute') @password.setter defpassword(self,password): self.password_hash=generate_password_hash(password) defverify_password(self,password): returncheck_password_hash(self.password_hash,password) |
把一個getter方法變成屬性,只需要加上@property就可以了,此時,@property本身又建立了另一個裝飾器@password.setter,負責把一個setter方法變成屬性賦值
簡單講,@property附加到那個方法上,該方法變為同名屬性,並只具有讀屬性
而要設定屬性的值,需要使用另外一個方法,並附加@方法名.setter,這樣提供了寫屬性
一句話就是對屬性讀寫分別處理,如果沒有setter,則屬性為只讀
hash後的加密串,即使相同的密碼加密,hash串也不相同
該功能的單元測試用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | importunittest fromapp.modelsimportUser classUserModelTestCase(unittest.TestCase): deftest_password_setter(self): u=User(password='cat') self.assertTrue(u.password_hashisnotNone) deftest_password_getter(self): u=User(password='cat') withself.assertRaises(AttributeError): u.password deftest_password_verfication(self): u=User(password='cat') self.assertTrue(u.verify_password('cat')) self.assertFalse(u.verify_password('dog')) deftest_password_salts_are_random(self): u1=User(password='cat') u2=User(password='cat') self.assertTrue(u1.password_hash!=u2.password_hash) |
建立認證藍本
對於不同的程式功能,使用不同的藍本,這樣可以使程式碼保持整齊有序。
藍本:auth/__init__.py
1 2 3 | fromflaskimportBlueprint auth=Blueprint('auth',__name__) from.importviews |
auth/views.py
1 2 3 4 5 6 | fromflaskimportrender_template from.importauth @auth.route('/login') deflogin(): returnrender_template('auth/login.html') |
auth/login.html位於app/templates/目錄下。當然,藍本也可以定義自己的模板資料夾,此時,render_template()會先搜尋程式資料夾,再搜尋藍本配置的模板資料夾。
在create_app函式中附加藍本auth到程式:app/__init__.py
1 2 3 4 5 6 7 8 9 10 | #... #def create_app(config_name): #... # from .main import main as main_blueprint # app.register_blueprint(main_blueprint) from.authimportauthasauth_blueprint app.register_blueprint(auth_blueprint,url_prefix='/auth') # return app |
url_prefix是可選引數,使用該引數後,藍本中定義的所有路由都會加上指定字首,這裡,/login變成了/auth/login。
使用Flask-Login認證使用者
pip install flask-login
使用Flask-Login擴充套件,User模型需要實現如下幾個方法:
屬性/方法 | 說明 |
is_authenticated | 若使用者已登入,則返回True,否則返回False |
is_active | 若允許使用者登入,則返回True,否則返回False;禁用使用者,可返回False |
is_anonymous | 對普通使用者返回False |
get_id() | 必須返回使用者唯一識別符號,使用Unicode編碼 |
這四個方法可以直接在User類中實現,更簡單的方法是使用Flask-Login提供的UserMixin類。
app/modles.py
1 2 3 4 5 6 7 8 9 10 11 | #... fromflask_loginimportUserMixin #... classUser(UserMixin,db.Model): # __tablename__ = 'users' # id = db.Column(db.Integer, primary_key=True) email=db.Column(db.String(64),unique=True,index=True) # username = db.Column(db.String(64), unique=True, index=True) # password_hash=db.Column(db.String(128)) # role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) #... |
初始化:app/__init__.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #from flask import Flask #from flask_bootstrap import Bootstrap #from flask_mail import Mail #from flask_moment import Moment #from flask_sqlalchemy import SQLAlchemy #from config import config fromflask_loginimportLoginManager #bootstrap = Bootstrap() #mail = Mail() #moment = Moment() #db = SQLAlchemy() login_manager=LoginManager() login_manager.session_protection='strong' login_manager.login_view='auth.login' #def create_app(config_name): # app = Flask(__name__) # app.config.from_object(config[config_name]) # config[config_name].init_app(app) # bootstrap.init_app(app) # mail.init_app(app) # moment.init_app(app) # db.init_app(app) login_manager.init_app(app) # from .main import main as main_blueprint # app.register_blueprint(main_blueprint) # from .auth import auth as auth_blueprint # app.register_blueprint(auth_blueprint,url_prefix='/auth') # return app |
session_protection可設定為None,'basic','strong',當設定為‘strong'時,會記錄客戶端IP和瀏覽器使用者代理資訊,發現異動就登出使用者。
Flask-Login要求實現一個回撥函式,使用指定的識別符號載入使用者:app/models.py
1 2 3 4 5 | from.importlogin_manager #... @login_manager.user_loader defload_user(user_id): returnUser.query.get(int(user_id)) |
回撥函式接收以Unicode字串形式表示的使用者識別符號,若存在該使用者,則返回使用者物件,否則返回None
保護路由
讓一個路由僅讓認證的使用者能訪問,未認證使用者訪問,Flask-Login會攔截請求,把使用者發往登入頁面,示例如下:
1 2 3 4 5 6 | fromflask_loginimportlogin_required @app.route('/secret') @login_required defsecret(): return'only authenticated users are allowed!' |
新增登入表單
app/auth/forms.py
1 2 3 4 5 6 7 8 9 | fromflask_wtfimportForm fromwtformsimportStringField,PasswordField,BooleanField,SubmitField fromwtforms.validatorsimportRequired,Length,Email classLoginForm(Form): email=StringField('Email',validators=[Required(),Length(1,64),Email()]) password=PasswordField('Password',validators=[Required()]) remeber_me=BooleanField('Keep me logged in') submit=SubmitField('Log in') |
app/templates/base.html
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }} ">Sign Out</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Sign In</a></li>
{% endif %}
</ul>
current_user由Flask-Login定義, 在檢視函式和模板中自動可用,這個變數的值是當前登入的使用者,若未登入,則是匿名使用者代理對像。
登入使用者
app/auth/views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | fromflaskimportrender_template,redirect,request,url_for,flash fromflask_loginimportlogin_user from.importauth from..modelsimportUser from.formsimportLoginForm @auth.route('/login',methods=['GET','POST']) deflogin(): form=LoginForm() ifform.validate_on_submit(): user=User.query.filter_by(email=form.email.data).first() ifuserisnotNoneanduser.verify_password(form.password.data): login_user(user,form.remember_me.data) returnredirect(request.args.get('next')orurl_for('main.index')) flash('Invalid username or password.') returnrender_template('auth/login.html',form=form) |
app/templates/auth/login.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
登出使用者
app/auth/views.py
1 2 3 4 5 6 7 8 | fromflask_loginimportlogout_user,login_required #... @auth.route('/logout') @login_required #保護路由 deflogout(): logout_user() flash('You have been logged out.') returnredirect(url_for('main.index')) |
測試登入
shell中註冊新使用者
>>> db.session.add(u)
>>> db.session.commit()
{{ current_user.username }}
{% else %}
Stranger
{% endif %}
註冊新使用者
使用者登錄檔單:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | fromflask_wtfimportFlaskForm fromwtformsimportStringField,PasswordField,SubmitField fromwtforms.validatorsimportRequired,Length,Email,Regexp,EqualTo fromwtformsimportValidationError from..modelsimportUser #... classRegistrationForm(FlaskForm): email=StringField('Email',validators=[Required(),Length(1,64),Email()]) username=StringField('Username',validators=[Required(),Length(1,64),Regexp('^[A-Za-z0-9_.]*$',0,'Usernames must have only letters,numbers,dots or underscores')]) password=PasswordField('Password',validators=[Required(),EqualTo('password2',message='Passwords must match.')]) password2=PasswordField('Confirm password',validators=[Required()]) submit=SubmitField('Register') defvalidate_email(self,field): ifUser.query.filter_by(email=field.data).first(): raiseValidationError('Email already registed.') defvalidate_username(self,field): ifUser.query.filter_by(username=field.data).first(): raiseValidationError('Username already in use.') |
注意,validator是複數:validators
其中,驗證函式Regexp是正規表示式驗證,第一個引數是正規表示式(包含字母、數字、下劃線和點),第二個是表示式的旗標(通常為0),第三個是匹配失敗時的錯誤訊息。
密碼的驗證使用EqualTo,放到任意一個就可以,另一個欄位做為引數傳入。
自定義的驗證函式:以validate_開頭,後面跟欄位名的方法。該方法會和常規驗證函式一起呼叫。
登錄檔單的渲染:
{{ wtf.quick_form(form) }}
註冊新使用者的檢視函式:
1 2 3 4 5 6 7 8 9 10 11 12 | from.formsimportRegistrationForm from..importdb #... @auth.route('/register',methods=['GET','POST']) defregister(): form=RegistrationForm() ifform.validate_on_submit(): user=User(email=form.email.data,username=form.username.data,password=form.password.data) db.session.add(user) flash('You can now login.') returnredirect(url_for('auth.login')) returnrender_template('auth/register.html',form=form) |
確認帳戶
驗證郵箱,通過點選包含令牌的URL,修改標記狀態。
itsdangerous提供多種生成令牌方法,其中TimedJSONWebSignatureSerializer類生成具有過期時間的JSON Web簽名,該類建構函式接收引數是一個金鑰和過期時間(秒)。dumps方法為指定資料生成加密的令牌字串,load方法解碼令牌。
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
s=Serializer(app.config['SECRET_KEY'],expires_in=3600)
token=s.dumps({'confirm':23}) #生成token,簽名字串
data=s.loads(token) #data={u'confirm':23}
修改模型:app/models.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | fromitsdangerousimportTimedJSONWebSignatureSerializerasSerializer fromflaskimportcurrent_app from.importdb #... classUser(UserMixin,db.Model): #... confirmed=db.Column(db.Boolean,default=False) defgenerate_confirmation_token(self,expiration=3600): s=Serializer(current_app.config['SECRET_KEY'],expiration) returns.dumps({'confirm':self.id}) defconfirm(self,token): s=Serializer(current_app.config['SECRET_KEY']) try: data=s.loads(token) except: returnFalse ifdata.get('confirm')!=self.id: returnFalse self.confirmed=True db.session.add(self) returnTrue |
傳送確認郵件
app/auth/views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #... from..emailimportsend_email #... #@auth.route('/register',methods=['GET','POST']) #def register(): # form = RegistrationForm() # if form.validate_on_submit(): # user=User(email=form.email.data,username=form.username.data,password=form.password.data) # db.session.add(user) db.session.commit() token=user.generate_confirmation_token() #send_email(user.email,'Confirm your account','auth/email/confirm',user=user,token=token) 傳送郵件不好模擬,暫用print替代 #print url_for('auth.confirm',token=token,_external=True) flash('A confirmation email has been sent to you by email') # return redirect(url_for('auth.login')) # return render_template('auth/register.html',form=form) |
因為只有提交資料庫後才能夠得到新使用者id,而生成token需要用到使用者id,所以需要新增db.session.commit()
模板:{{ url_for('auth.confirm',token=token,_external=True) }}
確認token:
1 2 3 4 5 6 7 8 9 10 11 12 | fromflask_loginimportcurrent_user #... @auth.route('/confirm/<token>') @login_required defconfirm(token): ifcurrent_user.confirmed: returnredirect(url_for('main.index')) ifcurrent_user.confirm(token): flash('You have confirmed your account.Thanks!') else: flash('The confirmation link is ivalid or has expired.') returnredirect(url_for('main.index')) |
藍本中的程式全域性請求鉤子-before_app_request
1 2 3 4 5 6 7 8 9 10 11 12 | @auth.before_app_request defbefore_request(): ifcurrent_user.is_authenticatedandnotcurrent_user.confirmedand\ request.endpoint[:5]!='auth.'andrequest.endpoint!='static': returnredirect(url_for('auth.unconfirmed')) #如果當前使用者已登入and帳號未啟用and請求端點不在認證藍本中and不是靜態檔案,則跳轉 @auth.route('/unconfirmed') defunconfirmed(): ifcurrent_user.is_anonymousorcurrent_user.confirmed: returnredirect(url_for('main.index')) returnrender_template('auth/unconfirmed.html') #如果是匿名使用者or帳號已啟用則正常跳轉 |
重新傳送確認郵件
1 2 3 4 5 6 7 8 | @auth.route('/confirm') @login_required defresend_confirmation(): token=current_user.generate_confirmation_token() #send_email(current_user.email,'Confirm Your Account','auth/email/confirm',user=current_user,token=token) printurl_for('auth.confirm',token=token,_external=True) flash('A new confirmation email has been sent to you by email.') returnredirect(url_for('main.index')) |
管理帳戶
修改密碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #表單 classChangePasswordForm(FlaskForm): old_password=PasswordField('Old password',validators=[Required()]) password=PasswordField('New password',validators=[ Required(),EqualTo('password2',message='Passwords must match')]) password2=PasswordField('Confirm new password',validators=[Required()]) submit=SubmitField('Update Password') #檢視 @auth.route('/change-password',methods=['GET','POST']) @login_required defchange_password(): form=ChangePasswordForm() ifform.validate_on_submit(): ifcurrent_user.verify_password(form.old_password.data): current_user.password=form.password.data db.session.add(current_user) flash('Your password has been updated.') returnredirect(url_for('main.index')) else: flash('Invalid password.') returnrender_template("auth/change_password.html",form=form) |
重設密碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #模型: defreset_password(self,token,new_password): s=Serializer(current_app.config['SECRET_KEY']) try: data=s.loads(token) except: returnFalse ifdata.get('reset')!=self.id: returnFalse self.password=new_password db.session.add(self) returnTrue #表單 classPasswordResetRequestForm(FlaskForm): email=StringField('Email',validators=[Required(),Length(1,64),Email()]) submit=SubmitField('Reset Password') classPasswordResetForm(FlaskForm): email=StringField('Email',validators=[Required(),Length(1,64),Email()]) password=PasswordField('New Password',validators=[Required(),EqualTo('password2',message='Passwords must match')]) password2=PasswordField('Confirm password',validators=[Required()]) submit=SubmitField('Reset Password') defvalidate_email(self,field): ifUser.query.filter_by(email=field.data).first()isNone: raiseValidationError('Unknown email address.') #檢視 @auth.route('/reset',methods=['GET','POST']) defpassword_reset_request(): ifnotcurrent_user.is_anonymous: returnredirect(url_for('main.index')) form=PasswordResetRequestForm() ifform.validate_on_submit(): user=User.query.filter_by(email=form.email.data).first() ifuser: token=user.generate_reset_token() send_email(user.email,'Reset Your Password', 'auth/email/reset_password', user=user,token=token, next=request.args.get('next')) flash('An email with instructions to reset your password has been ' 'sent to you.') returnredirect(url_for('auth.login')) returnrender_template('auth/reset_password.html',form=form) @auth.route('/reset/<token>',methods=['GET','POST']) defpassword_reset(token): ifnotcurrent_user.is_anonymous: returnredirect(url_for('main.index')) form=PasswordResetForm() ifform.validate_on_submit(): user=User.query.filter_by(email=form.email.data).first() ifuserisNone: returnredirect(url_for('main.index')) ifuser.reset_password(token,form.password.data): flash('Your password has been updated.') returnredirect(url_for('auth.login')) else: returnredirect(url_for('main.index')) returnrender_template('auth/reset_password.html',form=form) |
修改電子郵件
先確認郵件進行確認,輸入新郵件地址後,向該郵件地址傳送一封包含令牌的郵件。伺服器傳送令牌前,可先將郵件地址存到臨時表或者是直接存到token中。
第九章 使用者角色
角色在資料庫中的表示
app/models.py,新增兩個屬性
permissions = db.Column(db.Integer)
其中permissions欄位使用二進位制位表示不同的許可權。
許可權常量:
FOLLOW=0x01 #0b00000001關注其它使用者
COMMENT=0x02 #0b00000010在他人文章後發表評論
WRITE_ARTICLES=0x04 #0b00000100寫文章
MODERATE_COMMENTS=0x08 #0b00001000管理他人發表的評論
ADMINISTER=0x80 #0b10000000管理員
使用者角色:
使用者角色 | 許可權 | 許可權 | 說明 |
匿名 | 0b00000000 | 0x00 | 未登入使用者,僅閱讀許可權 |
使用者 | 0b00000111 | 0x07 | 寫文章,寫評論,關注其他使用者 |
協管員 | 0b00001111 | 0x0f | 增加管理他人評論功能 |
管理員 | 0b11111111 | 0xff | 所有許可權,包括修改其它使用者許可權 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #class Role(db.Model): # __tablename__ = 'roles' # id = db.Column(db.Integer, primary_key=True) # name = db.Column(db.String(64), unique=True) default=db.Column(db.Boolean,default=False,index=True) permissions=db.Column(db.Integer) # users = db.relationship('User', backref='role', lazy='dynamic') # def __repr__(self): # return '<Role %r>' % self.name @staticmethod definsert_roles(): roles={ 'User':(Permission.FOLLOW|Permission.COMMENT|Permission.WRITE_ARTICLES,True), 'Moderator':(Permission.FOLLOW|Permission.COMMENT|Permission.WRITE_ARTICLES|Permission.MODERATE_COMMENTS,False), 'Administrator':(0xff,False) } forrinroles: role=Role.query.filter_by(name=r).first() ifroleisNone: role=Role(name=r) role.permissions=roles[r][0] role.default=roles[r][1] db.session.add(role) db.session.commit() |
通過insert_roles方法新增角色,使用shell操作,Role.insert_roles()
賦予角色
app/models.py
1 2 3 4 5 6 7 8 9 10 | classUser(UserMixin,db.Model): #... def__init__(self,**kwargs): super(User,self).__init__(**kwargs) ifself.roleisNone: ifself.email==current_app.config['FLASKY_ADMIN']: self.role=Role.query.filter_by(permissions=0xff).first() ifself.roleisNone: self.role=Role.query.filter_by(default=True).first() #... |
角色驗證
新增輔助方法:app/models.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #... fromflask_loginimportUserMixin,AnonymousUserMixin #... classUser(UserMixin,db.Model): #... defcan(self,permissions): returnself.roleisnotNoneand(self.role.permissions&permissions)==permissions defis_administrator(self): returnself.can(Permission.ADMINISTER) #... classAnonymousUser(AnonymousUserMixin): defcan(self,permissions): returnFalse defis_administrator(self): returnFalse #... login_manager.anonymous_user=AnonymousUser |
can方法使用位與操作,檢查使用者許可權。Anonymous類出於一致性考慮,無論使用者是否登入,均可使用current_user.can()和current_user.is_administrator()方法來驗證使用者許可權。
檢查使用者許可權的自定義修飾器
app/decorators.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | fromfunctoolsimportwraps fromflaskimportabort fromflask_loginimportcurrent_user defpermission_required(permission): defdecorator(f): @wraps(f) defdecorated_function(*args,**kwargs): ifnotcurrent_user.can(permission): abort(403) returnf(*args,**kwargs) returndecorated_function returndecorator defadmin_required(f): returnpermission_required(Permission.ADMINISTER)(f) |
自定義修飾器的使用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | fromdecoratorsimportadmin_required,permission_required from.modelsimportPermission @main.route('/admin') @login_required @admin_required deffor_admins_only(): return"For administrator" @main.route('/moderator') @login_required @permission_required(Permission.MODERATE_COMMENTS) deffor_moderators_only(): return"For comment moderators!" |
模板中也需要檢查許可權,為避免每次呼叫render_template()時都多新增一個模板引數,可以使用上下文處理器,上下文處理器能讓變數在所有模板中全域性可訪問。
app/main/__init__.py
def inject_permissions():
return dict(Permission=Permission)
第十章 使用者資料
資料資訊
相關文章
- 《Flask Web開發 基於Python的Web應用開發實戰》簡評FlaskWebPython
- Flask Web開發學習之“HelloWorld”FlaskWeb
- Web開發學習筆記——HTTP 概述Web筆記HTTP
- Python Web 開發學習 - 學習筆記(2)- 啟動PythonPythonWeb筆記
- 《Python web開發》筆記 一:網頁開發基礎PythonWeb筆記網頁
- PHP 轉 Node 筆記(二. 基礎的Web開發)PHP筆記Web
- 《Flask Web開發》讀書筆記【Windows環境】FlaskWeb筆記Windows
- 《flask Web 開發》讀書筆記 & chapter6FlaskWeb筆記APT
- Web應用的元件化開發(二)Web元件化
- 聊天室應用開發實踐(二):實現基於 Web 的聊天室Web
- Web 開發學習筆記——關於網際網路和網際網路應用Web筆記
- 基於gin的golang web開發:路由二GolangWeb路由
- python web開發-flask中日誌的使用PythonWebFlask
- Web 開發學習筆記(1) — 搭建你的第一個 Web ServerWeb筆記Server
- Web開發學習Web
- 關於 Flask Web 開發的個人小結FlaskWeb
- 開發Web應用Web
- Go基礎學習記錄 – 編寫Web應用程 – Web開發輸入驗證(三)GoWeb
- 開始使用 Python 開發 Web 應用PythonWeb
- Flask web開發(3):模板FlaskWeb
- dubbo學習筆記---dubbo開發實戰筆記
- Web 開發學習筆記(4) — 重定向與HSTSWeb筆記
- Web 開發學習筆記(5) — 抽象出 Page 類Web筆記抽象
- web開發實戰教程:Apache Shiro在web專案中的應用WebApache
- Python Web開發需要學習什麼?Python基礎!PythonWeb
- 【Python】基於Django Web開發清單PythonDjangoWeb
- web開發學習之旅Web
- Web開發學習筆記——瀏覽器是如何工作的Web筆記瀏覽器
- java web 動態web開發基礎入門學習 eclipse版(二)JavaWebEclipse
- Flask web 開發(1):安裝FlaskWeb
- Web快速開發平臺,基於二次開發平臺Web
- python中web開發框架Django的學習PythonWeb框架Django
- 學習web前端開發的原因Web前端
- WEB應用開發中的ServletWebServlet
- 基於JavaScript的現代Web應用全棧開發:MEANJavaScriptWeb全棧
- 《Python Web開發實戰》隨書原始碼PythonWeb原始碼
- 學python可以做Web開發嗎?python適合Web開發嗎?PythonWeb
- 如何學習Java Web開發JavaWeb