本篇文章主要分享一下自己[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" >
注冊/登錄 <DownSquareFilled />
</a>
</Dropdown>;
else
this.loginButton =
<Dropdown overlay={userCenter} className="dropdown">
<a className="ant-dropdown-link" onClick={e => e.preventDefault()}>
{cookie.load('username')} <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'}}> 校園論壇</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'