AntDesign Pro + .NET Core 實現基於JWT的登

funnyok發表於2021-09-09

很多同學說AgileConfig的UI實在是太醜了。我想想也是的,本來這個專案是我自己使用的,一開始甚至連UI都沒有,全靠手動在資料庫裡修改資料。後來加上了UI也是使用了老掉牙的bootstrap3做為基礎樣式。前臺框架也是使用了angularjs,同樣是老掉牙的東西。過年期間終於下決心翻新AgileConfig的前端UI。最後選擇的前端UI框架為AntDesign Pro + React。至於為啥選Ant-Design Pro是因為他好看,而且流行,選擇React是因為VUE跟Angular我都略知一二,乾脆趁此機會學一學React為何物,為何這麼流行。
登入的認證方案為JWT,其實本人對JWT不太感冒(請看這裡《》),無奈大家都喜歡,那我也只能隨大流。
其實基於ant-design pro的介面我已經翻的差不多了,因為它支援mock資料,所以我一行後臺程式碼都沒修改,已經把介面快寫完了。從現在開始要真正的跟後端程式碼進行聯調了。那麼我們先從登入開始吧。先看看後端asp.net core方面會如何進行修改。

修改ASP.NET Core後端程式碼

  "JwtSetting": {
    "SecurityKey": "xxxxxxxxxxxx", // 金鑰
    "Issuer": "agileconfig.admin", // 頒發者
    "Audience": "agileconfig.admin", // 接收者
    "ExpireSeconds": 20 // 過期時間 s
  }

在appsettings.json檔案新增jwt相關配置。

  public class JwtSetting
    {
        static JwtSetting()
        {
            Instance = new JwtSetting();
            Instance.Audience = Global.Config["JwtSetting:Audience"];
            Instance.SecurityKey = Global.Config["JwtSetting:SecurityKey"];
            Instance.Issuer = Global.Config["JwtSetting:Issuer"];
            Instance.ExpireSeconds = int.Parse(Global.Config["JwtSetting:ExpireSeconds"]);
        }

        public string SecurityKey { get; set; }

        public string Issuer { get; set; }

        public string Audience { get; set; }

        public int ExpireSeconds { get; set; }

        public static JwtSetting Instance
        {
            get;
        }
    }

定義一個JwtSetting類,用來讀取配置。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMemoryCache();
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                      .AddJwtBearer(options =>
                      {
                          options.TokenValidationParameters = new TokenValidationParameters
                          {
                              ValidIssuer = JwtSetting.Instance.Issuer,
                              ValidAudience = JwtSetting.Instance.Audience,
                              IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSetting.Instance.SecurityKey)),
                          };
                      });
            services.AddCors();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0).AddRazorRuntimeCompilation();
            services.AddFreeSqlDbContext();
            services.AddBusinessServices();
            services.AddAntiforgery(o => o.SuppressXFrameOptionsHeader = true);
        }

修改Startup檔案的ConfigureServices方法,修改認證Scheme為JwtBearerDefaults.AuthenticationScheme,在AddJwtBearer方法內配置jwt相關配置資訊。因為前後端分離專案所以有可能api跟ui部署在不同的域名下,所以開啟Cors。

     // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseMiddleware();
            }
            app.UseCors(op=> {
                op.AllowAnyOrigin();
                op.AllowAnyMethod();
                op.AllowAnyHeader();
            });
            app.UseWebSockets(new WebSocketOptions()
            {
                KeepAliveInterval = TimeSpan.FromSeconds(60),
                ReceiveBufferSize = 2 * 1024
            });
            app.UseMiddleware();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }

修改Startup的Configure方法,配置Cors為Any。

    public class JWT
    {
        public static string GetToken()
        {
            //建立使用者身份標識,可按需要新增更多資訊
            var claims = new Claim[]
            {
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    new Claim("id", "admin", ClaimValueTypes.String), // 使用者id
    new Claim("name", "admin"), // 使用者名稱
    new Claim("admin", true.ToString() ,ClaimValueTypes.Boolean) // 是否是管理員
            };
            var key = Encoding.UTF8.GetBytes(JwtSetting.Instance.SecurityKey);
            //建立令牌
            var token = new JwtSecurityToken(
              issuer: JwtSetting.Instance.Issuer,
              audience: JwtSetting.Instance.Audience,
              signingCredentials: new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
              claims: claims,
              notBefore: DateTime.Now,
              expires: DateTime.Now.AddSeconds(JwtSetting.Instance.ExpireSeconds)
            );

            string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);

            return jwtToken;
        }
    }

新增一個JWT靜態類用來生成jwt的token。因為agileconfig的使用者只有admin一個所以這裡使用者名稱,ID都直接寫死。

 [HttpPost("admin/jwt/login")]
        public async Task Login4AntdPro([FromBody] LoginVM model)
        {
            string password = model.password;
            if (string.IsNullOrEmpty(password))
            {
                return Json(new
                {
                    status = "error",
                    message = "密碼不能為空"
                });
            }

            var result = await _settingService.ValidateAdminPassword(password);
            if (result)
            {

                var jwt = JWT.GetToken();

                return Json(new { 
                    status="ok",
                    token=jwt,
                    type= "Bearer",
                    currentAuthority = "admin"
                });
            }

            return Json(new
            {
                status = "error",
                message = "密碼錯誤"
            });
        }

新增一個Action方法做為登入的入口。在這裡驗證完密碼後生成token,並且返回到前端。
到這裡.net core這邊後端程式碼改動的差不多了。主要是新增jwt相關的東西,這些內容網上已經寫了很多了,不在贅述。
下面開始修改前端程式碼。

修改AntDesign Pro的程式碼

AntDesign Pro已經為我們生成好了登入頁面,登入的邏輯等,但是原來的登入是假的,也不支援jwt token做為登入憑證,下面我們要修改多個檔案來完善這個登入。

export function setToken(token:string): void {
  localStorage.setItem('token', token);
}

export function getToken(): string {
  var tk = localStorage.getItem('token');
  if (tk) {
    return tk as string;
  }

  return '';
}


在utils/authority.ts檔案內新增2個方法,用來儲存跟獲取token。我們的jwt token儲存在localStorage裡。


/** 配置request請求時的預設引數 */
const request = extend({
  prefix: '',
  errorHandler, // 預設錯誤處理
  credentials: 'same-origin', // 預設請求是否帶上cookie,
});
const authHeaderInterceptor = (url: string, options: RequestOptionsInit) => {
  const authHeader = { Authorization: 'Bearer ' + getToken() };
  return {
    url: `${url}`,
    options: { ...options, interceptors: true, headers: authHeader },
  };
};

request.interceptors.request.use(authHeaderInterceptor);

修改utils/request.ts檔案,定義一個新增Authorization頭部的攔截器,並且使用這個攔截器,這有每次請求的時候自動會帶上這個頭部,把jwt token傳送到後臺。
設定prefix為這是我們的後端api的服務地址,真正生產的時候會替換為正式地址。
設定credentials為same-origin。

export async function accountLogin(params: LoginParamsType) {
  return request('/admin/jwt/login', {
    method: 'POST',
    data: params,
  });
}


在services/login.ts檔案內新增發起登入請求的方法。

 effects: {
    *login({ payload }, { call, put }) {
      const response = yield call(accountLogin, payload);
      yield put({
        type: 'changeLoginStatus',
        payload: response,
      });
      // Login successfully
      if (response.status === 'ok') {
        const urlParams = new URL(window.location.href);
        const params = getPageQuery();
        message.success('???? ???? ????  登入成功!');
        let { redirect } = params as { redirect: string };
        if (redirect) {
          console.log('redirect url ' , redirect);
          const redirectUrlParams = new URL(redirect);
          if (redirectUrlParams.origin === urlParams.origin) {
            redirect = redirect.substr(urlParams.origin.length);
            if (redirect.match(/^/.*#/)) {
              redirect = redirect.substr(redirect.indexOf('#') + 1);
            }
          } else {
            window.location.href = '/';
            return;
          }
        }
        history.replace(redirect || '/');
      }
    },


     reducers: {
    changeLoginStatus(state, { payload }) {
      setAuthority(payload.currentAuthority);
      setToken(payload.token)
      return {
        ...state,
        status: payload.status,
        type: payload.type,
      };
    },
  },

修改models/login.ts檔案,修改effects的login方法,在內部替換原來的fakeAccountLogin為accountLogin。同時修改reducers內部的changeLoginStatus方法,新增setToken的程式碼,這有修改後登入成功後token就會被儲存起來。


  effects: {
    *fetch(_, { call, put }) {
      const response = yield call(queryUsers);
      yield put({
        type: 'save',
        payload: response,
      });
    },
    *fetchCurrent(_, { call, put }) {
      const response = {
        name: '管理員',
        userid: 'admin'
      };
      yield put({
        type: 'saveCurrentUser',
        payload: response,
      });
    },
  },

修改models/user.ts檔案,修改effects的fetchCurrent方法為直接返回response。本來fetchCurrent是會去後臺拉當前使用者資訊的,因為agileconfig的使用者就admin一個,所以我直接寫死了。
圖片描述
讓我們試一下登入吧:)
原始碼在這: ????????????

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3137/viewspace-2807257/,如需轉載,請註明出處,否則將追究法律責任。

相關文章