5

(十二).NET6 + React :升级!升级!还是***升级!!!+ IdentityServer4实战 - 冬...

 1 year ago
source link: https://www.cnblogs.com/WinterSir/p/16147929.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、前言#

此篇内容较多,我是一步一个脚印(坑),以至于写了好久,主要是这几部分:后台升级 .NET6  VS2022、前台升级Ant Design Pro V5 、前后台联调 IdentityServer4 实战,以及各部分在Linux环境下部署等等。

二、后台升级.NET6#

WebApi和类库都升级到.NET6,依赖包都升级到6.0以上最新,好像没什么感知,略感easy。(附一张写完后最新的项目结构图)

1780813-20220422101400332-1039947071.png

1780813-20220422100156239-58634689.png

1780813-20220530121416228-147030239.png

三、IdentityServer4实战#

1、用户管理#

还好上篇持久化已经做了90%的工作,不过是在Demo里面,现在搬到主项目里来,用户部分、客户端配置部分根据实际情况稍加改动。

这里需要解释一下,用户、角色管理这块可以用Identity进行管理,也可以在业务系统里管理,id4只做登录鉴权,这里只是举个例子,ApplicationUser继承IdentityUser,定义字段UserInfoId关联UserInfo表,具体需求根据项目实际情况来设计。

1780813-20220423135935026-1500801190.png

1780813-20220528164047845-1795345249.png

1780813-20220423162459503-1228161340.png

2、配置修改#

简化、授权码是给React前端用的,混合模式给 Mvc 客户端用的(一个空.NET6 Mvc项目,也搬到主项目了,具体的可以看代码)

1780813-20220528211459000-1170320134.png

3、数据迁移#

依次在程序包管理器控制台输入迁移命令,其他表结构数据相同就不贴了,上篇持久化过程都有详细步骤和结果。

1780813-20220528164605841-112275738.png

四、前台升级 Ant Design Pro V5#

前台升级 Ant Design Pro V5,之前用的是V5预览版,已经是一年前的事情了。。。我反思。。。😅

1、安装组件 oidc-client#

cnpm install oidc-client --save,新建认证服务类 auth.ts ,跟后台 IdentityServer4 认证服务配合使用。

import { Log, User, UserManager } from 'oidc-client';

export class AuthService {
    public userManager: UserManager;
    constructor() {
        // const clientRoot = 'http://localhost:8000/';
        const clientRoot = 'https://homejok.wintersir.com/';        
        const settings = {
            authority: 'https://login.wintersir.com',
            //client_id: 'antdview',
            //response_type: 'id_token token',
            client_id: 'antdviewcode',
            client_secret: 'antdviewcode',
            response_type: 'code',
            redirect_uri: `${clientRoot}callback`,
            post_logout_redirect_uri: `${clientRoot}`,
            // silent_redirect_uri: `${clientRoot}silent-renew.html`,
            scope: 'openid profile HomeJokScope'
        };
        this.userManager = new UserManager(settings);

        Log.logger = console;
        Log.level = Log.WARN;
    }

    public login(): Promise<void> {
        //记录跳转登录前的路由
        return this.userManager.signinRedirect({ state: window.location.href });
    }

    public signinRedirectCallback(): Promise<User> {
        return this.userManager.signinRedirectCallback();
    }

    public logout(): Promise<void> {
        return this.userManager.signoutRedirect();
    }

    public getUser(): Promise<User | null> {
        return this.userManager.getUser();
    }

    public renewToken(): Promise<User> {
        return this.userManager.signinSilent();
    }
}

2、登录认证设置#

修改app.tsx,初始化认证服务类,判断登录状态,设置请求后台拦截器,添加 headers 和 token

import type { Settings as LayoutSettings } from '@ant-design/pro-layout';
import { PageLoading } from '@/components/PageLoading';
import type { RequestConfig, RunTimeLayoutConfig } from 'umi';
import { history, Link } from 'umi';
import RightContent from '@/components/RightContent';
import { BookOutlined, LinkOutlined } from '@ant-design/icons';
import { AuthService } from '@/utils/auth';

const isDev = process.env.NODE_ENV === 'development';

const authService: AuthService = new AuthService();

/** 获取用户信息比较慢的时候会展示一个 loading */
export const initialStateConfig = {
    loading: <PageLoading />
};

/**
 * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
 * */
export async function getInitialState(): Promise<{
    settings?: Partial<LayoutSettings>;
}> {
    const init = localStorage.getItem('init');
    const token = localStorage.getItem('token');
    const user = localStorage.getItem('user');
    if (!token && !init && !user) {
        localStorage.setItem('init', 'true');
        authService.login();
    }
    return {
        settings: {}
    };
}

// ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
    const token = localStorage.getItem('token');
    const user = localStorage.getItem('user');
    return {
        rightContentRender: () => <RightContent />,
        disableContentMargin: false,
        waterMarkProps: {
            //content: initialState?.currentUser?.name,
        },
        // footerRender: () => <Footer />,
        onPageChange: () => {
            const { location } = history;
            if (!token && !user && location.pathname != "/callback") {
                authService.login();
            }
        },
        links: isDev
            ? [
                <Link to="/umi/plugin/openapi" target="_blank">
                    <LinkOutlined />
                    <span>OpenAPI 文档</span>
                </Link>,
                <Link to="/~docs">
                    <BookOutlined />
                    <span>业务组件文档</span>
                </Link>,
            ]
            : [],
        menuHeaderRender: undefined,
        // 自定义 403 页面
        // unAccessible: <div>unAccessible</div>,
        ...initialState?.settings,
        //防止未登录闪屏菜单问题
        pure: token ? false : true 
    }
};

const authHeaderInterceptor = (url: string, options: RequestOptionsInit) => {
    const token = localStorage.getItem('token');
    const authHeader = { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' + token };
    return {
        url: `${url}`,
        options: { ...options, interceptors: true, headers: authHeader },
    };
};
export const request: RequestConfig = {
    //timeout: 10000,
    // 新增自动添加AccessToken的请求前拦截器
    requestInterceptors: [authHeaderInterceptor],
};

3、登录回调、获取用户、登出#

(1)添加callback.tsx,这是登录成功的回调地址,保存登录状态token、user等,跳转页面。

import { message, Result, Spin } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import { AuthService } from '@/utils/auth';
import { useRequest } from 'umi';
import { login } from '@/services/accountService';

const authService: AuthService = new AuthService();

const callback: React.FC = () => {
    const [msg, setMsg] = useState('玩命登录中......');
    const authRedirect = useRef("/");
    const { run, loading } = useRequest(login, {
        manual: true,
        onSuccess: (result, params) => {
            if (result && result.responseData) {
                localStorage.setItem('user', JSON.stringify(result.responseData));
                window.location.href = authRedirect.current;
            } else {
                setMsg('登录失败,即将跳转重新登录......');
                setTimeout(() => {
                    localStorage.removeItem('init');
                    localStorage.removeItem('token');
                    window.location.href = authRedirect.current;
                }, 3000);
            }
        },
        onError: (error) => {
            message.error(error.message);
        }
    });
    useEffect(() => {
        authService.signinRedirectCallback().then(auth => {
            authRedirect.current = auth.state;
            localStorage.setItem('token', auth.access_token);
            run({ account: auth.profile.sub });
        })
    }, [])

    return (
        <>
            <Result status="success" title={<Spin spinning={loading} tip={msg}></Spin>} />
        </>
    )
};

export default callback;

(2)accountService.tsx Umi的useRequest、Request 配合使用

import { request } from 'umi';
export async function login(payload: { account: string }, options?: { [key: string]: any }) {
    return request('/api/Account/Login', {
        method: 'POST',
        params: payload,
        ...(options || {}),
    });
}

AvatarDropdown.tsx 登出核心方法

const onMenuClick = useCallback(
  (event: MenuInfo) => {
    const { key } = event;
    if (key === 'logout') {
      localStorage.removeItem('init');
      localStorage.removeItem('user');
      localStorage.removeItem('token');
      authService.logout();
      return;
    }
    history.push(`/account/${key}`);
  },
  [],
);

4、自定义其他配置#

如:PageLoading 等待框组件、defaultSettings.ts 颜色、图标等配置项、routes.ts 路由,详细见代码。

五、Linux服务器部署#

1、.NET6 环境#

坑1有CentOS7安装.NET6的步骤,其他linux版本参考

2、服务资源分配#

因为只有一台服务器,用nginx进行转发:React前端端口80,IdentityServer4端口5000,WebApi资源端口8000,Mvc网站端口9000

1780813-20220530144333924-463697186.png

启动命令分两步,举个栗子:

cd /指定目录
nohup dotnet ./BKYL.WEB.API.dll &

3、nginx配置https#

因为全部使用了https,所有域名、二级域名都搞了ssl证书,一一对应,通过 nginx 配置部署,贴上nginx.conf关键部分以供参考。

如何设置二级域名、申请ssl证书、nginx配置ssl证书,网上资料很多,不再赘述,有需要的可以私

server {
  listen       80;
  server_name  *.wintersir.com;
  
  rewrite ^(.*)$ https://$host$1;
  #charset koi8-r;
  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
      root   html;
  }     
}
 
server {
  listen       443 ssl;
  server_name  www.wintersir.com wintersir.com;
  ssl_certificate      /app/nginx/conf/index/cert.pem;
  ssl_certificate_key  /app/nginx/conf/index/cert.key;
  ssl_session_cache    shared:SSL:1m;
  ssl_session_timeout  5m;
  ssl_ciphers  HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers  on;
  location / {
    root   /app/wintersir/index;
    index  index.html index.htm;
  }
}

server {
  listen       443 ssl;
  server_name  homejok.wintersir.com;
  ssl_certificate      /app/nginx/conf/view/cert.pem;
  ssl_certificate_key  /app/nginx/conf/view/cert.key;
  ssl_session_cache    shared:SSL:1m;
  ssl_session_timeout  5m;
  ssl_ciphers  HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers  on;
  location / {
  
    proxy_set_header X-Forearded-Proto $scheme;
    proxy_set_header Host $http_host;		
    proxy_set_header X-Real-IP $remote_addr;		
    
    add_header 'Access-Control-Allow-Origin' "$http_origin";		
    add_header 'Access-Control-Allow-Credentials' 'true';		
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';		
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since,Content-Type';
  
    root   /app/wintersir/view;
    index  index.html index.htm;
  }
  
  location /api/ {
  
    proxy_set_header X-Forearded-Proto $scheme;
    proxy_set_header Host $http_host;		
    proxy_set_header X-Real-IP $remote_addr;		
    
    add_header 'Access-Control-Allow-Origin' "$http_origin";		
    add_header 'Access-Control-Allow-Credentials' 'true';		
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';		
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since,Content-Type';
    proxy_pass https://www.wintersir.com:8000;
  }
}
 
server {
  listen       443 ssl;
  server_name  mvc.wintersir.com;
  ssl_certificate      /app/nginx/conf/mvc/cert.pem;
  ssl_certificate_key  /app/nginx/conf/mvc/cert.key;
  ssl_session_cache    shared:SSL:1m;
  ssl_session_timeout  5m;
  ssl_ciphers  HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers  on;
  location / {
    proxy_set_header X-Forearded-Proto $scheme;
    proxy_set_header Host $http_host;		
    proxy_set_header X-Real-IP $remote_addr;		
    add_header 'Access-Control-Allow-Origin' "$http_origin";		
    add_header 'Access-Control-Allow-Credentials' 'true';		
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';		
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since,Content-Type';
    proxy_pass https://www.wintersir.com:9000/;
  }
}

server {
  isten       443 ssl;
  server_name  login.wintersir.com;
 
  ssl_certificate      /app/nginx/conf/login/cert.pem;
  ssl_certificate_key  /app/nginx/conf/login/cert.key;
 
  ssl_session_cache    shared:SSL:1m;
  ssl_session_timeout  5m;
 
  ssl_ciphers  HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers  on;
 
  location / {
  
    if ($request_method = 'OPTIONS') { 
       add_header 'Access-Control-Allow-Origin' "$http_origin";
       add_header 'Access-Control-Allow-Credentials' 'true';
       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
       add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since,Content-Type';
       return 200; 
    }
  
    proxy_set_header X-Forearded-Proto $scheme;
    proxy_set_header Host $http_host;		
    proxy_set_header X-Real-IP $remote_addr;		
    add_header 'Access-Control-Allow-Origin' "$http_origin";		
    add_header 'Access-Control-Allow-Credentials' 'true';		
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';		
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since,Content-Type';
    proxy_pass https://www.wintersir.com:5000/;
  }
}

六、效果图#

1、React前端#

1780813-20220530143323826-348525557.gif

2、Mvc网站测试#

1780813-20220530143329034-2012384505.gif

七、各种踩坑#

1、CentOS8不支持 .NET6#

当初阿里云装了CentOS8,现在想哭,边哭边装CentOS7.9,把数据库、nginx等环境又都装一遍(前几篇都有)😂
CentOS7 安装 .NET6 官方教程 ,分别执行以下命令,中间输入了两次y

sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
sudo yum install dotnet-sdk-6.0

2、nginx传输数据过大、转发跨域#

简单模式 token 等信息带在了回调页面url里,nginx 502 无法跳转,解决参考,nginx http 添加以下代码:

1780813-20220427224436345-683266032.png
proxy_buffer_size   128k;
proxy_buffers   4 256k;
proxy_busy_buffers_size   256k;
large_client_header_buffers 4 16k;

处理proxy_pass转发:

proxy_set_header X-Forearded-Proto $scheme;
proxy_set_header Host $http_host;		
proxy_set_header X-Real-IP $remote_addr;
add_header 'Access-Control-Allow-Origin' "$http_origin";		
add_header 'Access-Control-Allow-Credentials' 'true';		
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';		
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since,Content-Type';

处理OPTIONS预检

if ($request_method = 'OPTIONS') { 
  add_header 'Access-Control-Allow-Origin' "$http_origin";
  add_header 'Access-Control-Allow-Credentials' 'true';
  add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
  add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since,Content-Type';
  return 200; 
}

3、js客户端token传输、授权#

js(react、vue等)客户端需要在授权服务配置时,设置允许将token通过浏览器传递 AllowAccessTokensViaBrowser = true,客户端设置禁用了授权页面,登录鉴权后直接跳回对应 RequireConsent = false

1780813-20220428093759500-353565832.png

4、前端统一接口返回格式#

Umi框架默认格式与后台接口返回不一致就会导致获取不到,config.ts 加上以下代码,就可以使用后台自定义返回的数据格式了

request: {
    dataField: ""  //忽略框架处理
}

5、PostLogoutRedirectUris无效#

本以为配置了退出跳转路由就OK了,结果还是得自己加代码手动跳转,在授权服务中AccountController,Logout方法

1780813-20220528221038792-482411485.png

6、EFCore连接数据库偶尔异常:An exception has been raised that is likely due to a transient failure.#

查资料说是ssl问题,连接字符串server改成https的,不太确定,大佬懂的可以说一下

八、前人栽树,后人乘凉#

https://github.com/skoruba/react-oidc-client-js

九、代码已上传#

https://github.com/wintersir

十、后续#

后面学习要往容器、微服务方向走了,Docker、Jenkins、DDD 等等,想学哪个学哪个,写了也不一定学,哈哈~~~,还是那句话:
(个人学习记录分享,坚持更新,也有可能烂尾,最终解释权归本人所有,哈哈哈哈,嗝~~~)

写在最后#

博客名:WinterSir,学习目录 https://www.cnblogs.com/WinterSir/p/13942849.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK