[前后端分離]論壇項目搭建

本篇文章主要分享一下自己[github賬號]在搭建前后端分離的項目中所踩過的一些坑手销。內容包括:后端Spring Boot項目搭建扇谣、前端React搭建、Docker與CI/CD持續(xù)集成碎绎、Ningx解決跨域問題亿眠。
感謝組長[github賬號]對項目的付出

參考資料

1.1前端

1.1.1 React[官方文檔]項目框架。
1.1.2 Ant Design[官方文檔]開發(fā)工具会傲。
1.1.3 Axios[官方文檔]調用后端接口锅棕。
1.1.4 React前端[項目地址]
1.1.5 Ant Design[Icon地址]
1.1.6 React之a(chǎn)xios的基本用法[博客地址]

1.2 后端

1.2.1 Spring Boot后端[項目地址]
1.2.2 Spring Boot教程[視頻地址]

1.3支持工具

注意博主對于跨域的理解寫反了,Nginx正確配置可參考章節(jié)2.6

1.3.1 Nginx跨域配置 [參考資料]
1.3.2 Docker部署jar包運行[參考資料]
1.3.3 自動生成前后端交互文檔 [參考資料]

1.4 其它資料

1.4.1 React+SpringBoot項目部署[博客地址]
1.4.2 springboot2.x整合react部署到nginx完美結合博客地址
1.4.3 postman軟件 [官網(wǎng)地址]

前端項目集成

2.1 使用腳手架創(chuàng)建項目
#如果沒有安裝yarn 需要先安裝yarn
https://classic.yarnpkg.com/en/docs/install
# 克隆項目
$ git clone git@github.com:ZJU-Forum-Project/Forum-Project.git
# 安裝antd庫并啟動項目
$ yarn add antd
$ yarn start
# 生成項目打包文件
$ yarn build
2.2 配置路由
// router.js
// Login設置可參加章節(jié)2.3
import Register from './pages/register';
import Login from './pages/login';
import HomePage from './pages/homepage';
import Modifypwd from './pages/modifypwd';

export default class Routing extends React.Component {
  render() {
      return (
        <Router >
          <div>
            {/*使用exact 防止HomePage界面的重復渲染*/}
            <Route exact path="/" component={HomePage} />
            <Route path="/callback" component={HomePage} />
            <Route path="/login" component={Login} />
            <Route path="/register" component={Register} />
            <Route path="/modifypwd" component={Modifypwd}/>
          </div>
        </Router>
      );
  }
}
ReactDOM.render(<Routing />, document.getElementById("root"));
2.3 登錄界面
// login.js
import {Layout} from 'antd';
import NavigateBar from '../components/navigate';
import React from 'react';
import axios from 'axios';
import cookie from 'react-cookies';

const {Footer} = Layout;

class Login extends React.Component{
        render() {
        return(
        <Layout className="layout">
            <NavigateBar />
            <div>~~~登錄表單~~~~</div>
            <Footer style={{textAlign: 'center'}}>Design ?2020 by Group I</Footer>
        </Layout>
        );
    }
}
export default Login;
2.4 Axios前后端交互
// 等待后端返回數(shù)據(jù)使用異步操作
async function ToLogin(urlParam) {
    let code = urlParam.split("&")[0].split("=")[1];
    let state = urlParam.split("&")[1].split("=")[1];
    let formData = new FormData();
    formData.append('code',code);
    formData.append('state',state);
    let person_info = (await axios.post('/api/githubLogin',formData)).data;
    let success = person_info.state;
    if(success){
        let username = person_info.message.split(";")[0];
        let avatar_url = person_info.message.split(";")[1];
        let token = person_info.authorizeToken;
        cookie.save('username', username);
        cookie.save('avatarUrl', avatar_url);
        cookie.save('token',token);
    }
    return person_info;
}
2.5 導航組件
class NavigateBar extends React.Component {
    async componentWillMount() {
        let url = document.URL;
        if (url.search("callback") !== -1) {
            let urlParam = url.split("?")[1];
            await ToLogin(urlParam);
            this.forceUpdate();
        }
    }

    render() {
        if (!cookie.load('token'))
            this.loginButton = 
            <Dropdown overlay={notLogin} className="dropdown">
                <a className="ant-dropdown-link" className="ant-dropdown-link" >
                    注冊/登錄&nbsp;&nbsp;<DownSquareFilled />
                </a>
            </Dropdown>;
        else
            this.loginButton = 
            <Dropdown overlay={userCenter} className="dropdown">
                <a className="ant-dropdown-link" onClick={e => e.preventDefault()}>
                    {cookie.load('username')}&nbsp;&nbsp;<Avatar shape="square" size={28} src={cookie.load('avatarUrl')}/>
                </a>
            </Dropdown>;

        return (
            <Header>
                <div className="logo">
                    <a href="/index.html">
                        <HomeFilled twoToneColor/>
                        <Text style={{color: '#1890ff'}}>&nbsp;&nbsp;校園論壇</Text>
                    </a>
                </div>
                <div className="search">
                    <Search placeholder="搜索問題或找人" onSearch={value => console.log(value)} enterButton/>
                </div>
                <Menu size="small" theme="dark" mode="horizontal" defaultSelectedKeys={['2']}>
                    <Menu.Item className="menuItemStyle" key="1">版面列表</Menu.Item>
                    <Menu.Item className="menuItemStyle" key="2">新帖</Menu.Item>
                    <Menu.Item className="menuItemStyle" key="3">通知</Menu.Item>
                    {this.loginButton}
                </Menu>
            </Header>
        );
    }
}
export default NavigateBar;
2.6 Nginx配置 解決前端跨域問題

注意需修改為你的服務器地址和后端項目監(jiān)聽地址淌山。

// nginx.conf
server {
    listen       80;
    # 需將server_name替換為你的服務器地址
    server_name  106.**.**.104;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri /index.html;
    }
    # 使用proxy_pass解決前后端跨域問題
    # 任何以/api/開始的訪問都將分發(fā)給代理的地址
    location /api/ { 
        # 需將proxy_pass替換為后端項目監(jiān)聽的地址
        proxy_pass http://106.**.**.104:8080/;
    }
}
2.7 Docker配置
// docker-compose.yml
version: '3'
services:
  # 服務名稱
  nginx:
    # 鏡像:版本
    image: nginx:latest 
    container_name: my_nginx
    # 映射容器80端口到本地80端口
    ports:
     - "80:80"
    # 數(shù)據(jù)卷 映射本地文件到容器
    volumes:
    # 映射nginx.conf文件到容器的/etc/nginx/conf.d目錄并覆蓋default.conf文件
     - ./nginx.conf:/etc/nginx/conf.d/default.conf
    # 映射build文件夾到容器的/usr/share/nginx/html文件夾
     - ./build:/usr/share/nginx/html
    # 覆蓋容器啟動后默認執(zhí)行的命令裸燎。
    command: /bin/bash -c "nginx -g 'daemon off;'"
2.8 項目啟動配置
# 前端文件夾結構如下
# /root/back-end
├── docker-compose.yml
├── nginx.conf
# build文件夾說明參見章節(jié)2.1
├── build/

$ cd /root/back-end
$ docker-compose up
# CI/CD自動部署參見章節(jié)

后端項目集成

3.1 項目結構
├── config/
├── controller/
├── dto/
├── interceptor/
├── mapper/
├── provider/
├── service/
├── ForumBackendApplication.java
3.2 實現(xiàn)需登錄功能的驗證
3.2.1 攔截登錄請求
// AuthorizeInterceptor.java
// 配置攔截器攔截需登錄后使用的功能
// 驗證header是否包含Authorization字段
// 如果有且驗證通過則提供服務
// 如果沒有或驗證失敗則顯示401錯誤
@Service
public class AuthorizeInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisProvider redisProvider;

    private String httpHeaderName = "Authorization";

    private String unauthorizedErrorMessage = "401 unauthorized";

    private int unauthorizedErrorCode = HttpServletResponse.SC_UNAUTHORIZED;

    public static final String REQUEST_CURRENT_KEY = "REQUEST_CURRENT_KEY";

    private void sendUnAuthorizedInfo(HttpServletResponse response) throws IOException {
        response.setStatus(unauthorizedErrorCode);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", response.getStatus());
        jsonObject.put("message", HttpStatus.UNAUTHORIZED);
        response.getWriter().println(jsonObject);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod))
            return true;
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        if (method.getAnnotation(AuthToken.class) != null || handlerMethod.getBeanType().getAnnotation(AuthToken.class) != null) {
            String requestToken = request.getParameter(httpHeaderName);
            if (requestToken == null) {
                sendUnAuthorizedInfo(response);
                return false;
            }


            String authorizedName = redisProvider.getAuthorizedName(requestToken);
            if (authorizedName == null){
                sendUnAuthorizedInfo(response);
                return false;
            }
        }

        return true;
    }
}
3.2.2 配置Redis
@Component
public class RedisProvider {
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String redisPort;

    @Value("${spring.redis.expire}")
    private String redisExpire;

    private JedisPool getRedisPool() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(10);
        return new JedisPool(jedisPoolConfig, redisHost, Integer.parseInt(redisPort));
    }

    public String getAuthorizedName(String token) {
        JedisPool pool = getRedisPool();
        Jedis jedis = pool.getResource();
        String username = jedis.get(token);
        if (username == null)
            return null;

        jedis.expire(token, Integer.parseInt(redisExpire));
        jedis.close();
        pool.close();
        return username;
    }

    public void setAuthorizeToken(String authorizeToken, String email) {
        JedisPool pool = getRedisPool();
        Jedis jedis = pool.getResource();
        System.out.println(authorizeToken);
        jedis.set(authorizeToken, email);
        jedis.expire(authorizeToken, Integer.parseInt(redisExpire));
        jedis.close();
        pool.close();
    }
}
3.2.3 添加注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthToken {
}
3.3 實現(xiàn)Github登錄
3.3.1 獲取登錄請求
// Github登錄實現(xiàn)原理可參考B站碼匠筆記https://www.bilibili.com/video/BV1r4411r7au?p=9

    @ApiOperation("Github登錄")
    @PostMapping(value = "/githubLogin")
    public Message callback(@RequestParam("code") String code,
                            @RequestParam("state") String state) throws IOException {
        Message message = new Message();

        AccessToken accessTokenDTO = new AccessToken();
        accessTokenDTO.setCode(code);
        accessTokenDTO.setState(state);
        accessTokenDTO.setClient_id(ClientId);
        accessTokenDTO.setClient_secret(ClientSecret);
        accessTokenDTO.setRedirect_uri(RedirectURI);

        String accessToken = githubProvider.getAccessToken(accessTokenDTO);
        if (accessToken == null) {
            message.setState(false);
            message.setMessage("登陸失敗");
        } else {
            User user = githubProvider.getUser(accessToken);
            if (user.getName() == null) {
                message.setState(false);
                message.setMessage("github上的用戶名為空");
                return message;
            }
            if (user.getEmail() == null) {
                message.setState(false);
                message.setMessage("github上的郵箱為空");
                return message;
            }
            if (userMapper.isUserExist(user) == 0) {
                userMapper.createUser(user);
            }

            String authorizeToken = encryptService.getMD5Code(user.getEmail());
            redisProvider.setAuthorizeToken(authorizeToken, user.getEmail());
            message.setState(true);
            message.setMessage(user.getName() + ";" + user.getAvatarUrl());
            message.setAuthorizeToken(authorizeToken);
        }
        return message;
    }
3.3.2 AccessToken
import lombok.Data;

@Data
public class AccessToken {
    private String client_id;
    private String client_secret;
    private String code;
    private String redirect_uri;
    private String state;
}
3.3.3 實現(xiàn)Github登錄機制
@Component
public class GithubProvider {
    public String getAccessToken(AccessToken accessToken) {

        MediaType mediaType = MediaType.get("application/json; charset=utf-8");
        OkHttpClient client = new OkHttpClient();
        RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(accessToken));
        Request request = new Request.Builder()
                .url("https://github.com/login/oauth/access_token")
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            String string = response.body().string();
            String token = string.split("&")[0].split("=")[1];
            return token;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public User getUser(String accessToken) throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("https://api.github.com/user?access_token=" + accessToken)
                .build();
        Response response = client.newCall(request).execute();
        String string = response.body().string();
        User user = JSON.parseObject(string, User.class);
        return user;
    }
}
3.4 配置Swagger
// swagger能為程序自動創(chuàng)建前后端交互的文檔,便于前端和后端開發(fā)人員之間進行協(xié)同合作
// 具體配置可參考1.3.2
@Configuration
@EnableSwagger2
@ComponentScan("zju.group1.forum.controller")
@ComponentScan("zju.group1.forum.dto")
public class SwaggerService {
    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
        @Bean
        public Docket api() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(PathSelectors.any())
                    .build()
                    .apiInfo(apiInfo());
        }

        private ApiInfo apiInfo() {
            return new ApiInfo(
                    "Forum交互文檔",
                    "本輪迭代人員-***-***",
                    "V1.0.0",
                    "https://github.com/orgs/ZJU-Forum-Project/dashboard",
                    new Contact("", "", ""),
                    "", "", Collections.emptyList());
        }
    }
}
3.5 發(fā)送郵件功能
@Service
public class MailService {


    @Value("${spring.email.password}")
    private String password;
    @Value("${spring.email.username}")
    private String username;
    @Value("${spring.email.host}")
    private String host;

    public void sendToken(String toEmail, String token) {
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
        simpleMailMessage.setFrom(username);
        simpleMailMessage.setTo(toEmail);
        simpleMailMessage.setSubject("Forum注冊驗證");
        simpleMailMessage.setText("您的驗證碼是:" + token + "\n驗證碼30min內有效");
        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        mailSender.setHost(host);
        mailSender.setUsername(username);
        mailSender.setPassword(password);
        mailSender.send(simpleMailMessage);
    }
}
3.6 配置Dockerfile
# 將后端項目打包移動至DockerFile文件夾
# $ docker-compose up
version: '3'
services:
  # 服務名稱
  java:
    # 鏡像:版本
    image: java:8
    container_name: backend
    # 映射容器8080端口到本地8080端口
    ports:
      - "8080:8080"
    volumes:
      - ./:/usr/share
    # 覆蓋容器啟動后默認執(zhí)行的命令泼疑。
    command: /bin/bash -c "java -jar /usr/share/demo-0.0.1-SNAPSHOT.jar"

CI/CD

CI/CD配置可參考文檔德绿,其中介紹的非常詳細博客連接

addons:
ssh_known_hosts:
  - ***.**.**.***
before_install:
  - openssl aes-256-cbc -K $encrypted_9b2d7e19d83c_key -iv $encrypted_9b2d7e19d83c_iv
    -in id_rsa.enc -out ~/.ssh/id_rsa -d
  - chmod 600 ~/.ssh/id_rsa
script:
  - ssh root@***.**.**.*** -o StrictHostKeyChecking=no 'cd /root/forum-backend && git pull && mvn clean && mvn install && mv -f /root/forum-backend/target/demo-0.0.1-SNAPSHOT.jar /root/back-end/ && cd /root/back-end && docker-compose stop && docker-compose up -d'

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末退渗,一起剝皮案震驚了整個濱河市移稳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌会油,老刑警劉巖个粱,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機命辖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胶征,“玉大人,你說我怎么就攤上這事桨仿【Φ停” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長钱雷。 經(jīng)常有香客問我骂铁,道長,這世上最難降的妖魔是什么急波? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任从铲,我火速辦了婚禮,結果婚禮上澄暮,老公的妹妹穿的比我還像新娘名段。我一直安慰自己,他們只是感情好泣懊,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布伸辟。 她就那樣靜靜地躺著,像睡著了一般馍刮。 火紅的嫁衣襯著肌膚如雪信夫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天卡啰,我揣著相機與錄音静稻,去河邊找鬼。 笑死匈辱,一個胖子當著我的面吹牛振湾,可吹牛的內容都是我干的。 我是一名探鬼主播亡脸,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼押搪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了浅碾?” 一聲冷哼從身側響起大州,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎垂谢,沒想到半個月后厦画,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡埂陆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年苛白,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焚虱。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖懂版,靈堂內的尸體忽然破棺而出鹃栽,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布民鼓,位于F島的核電站薇芝,受9級特大地震影響,放射性物質發(fā)生泄漏丰嘉。R本人自食惡果不足惜夯到,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饮亏。 院中可真熱鬧耍贾,春花似錦、人聲如沸路幸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽简肴。三九已至晃听,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間砰识,已是汗流浹背能扒。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辫狼,地道東北人初斑。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像予借,于是被迫代替她去往敵國和親越平。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內容