feat: support channel level auth

This commit is contained in:
JustSong
2024-11-10 10:46:28 +08:00
parent 83a2bc9bd4
commit 08a8688aa7
5 changed files with 69 additions and 36 deletions

View File

@@ -214,6 +214,7 @@ proxy_send_timeout 300s;
15. `tencent_alarm`:通过腾讯云监控告警进行推送,仅支持 `description` 字段。 15. `tencent_alarm`:通过腾讯云监控告警进行推送,仅支持 `description` 字段。
16. `none`:仅保存到数据库,不做推送。 16. `none`:仅保存到数据库,不做推送。
5. `token`:如果你在后台设置了推送 token则此项必填。另外可以通过设置 HTTP `Authorization` 头部设置此项。 5. `token`:如果你在后台设置了推送 token则此项必填。另外可以通过设置 HTTP `Authorization` 头部设置此项。
* 注意令牌有两种,一种是全局鉴权令牌,一种是通道维度的令牌,前者可以鉴权任何通道,后者只能鉴权指定通道。
6. `url`:选填,如果不填则系统自动为消息生成 URL其内容为消息详情。 6. `url`:选填,如果不填则系统自动为消息生成 URL其内容为消息详情。
7. `to`:选填,推送给指定用户,如果不填则默认推送给自己,受限于具体的消息推送方式,有些推送方式不支持此项。 7. `to`:选填,推送给指定用户,如果不填则默认推送给自己,受限于具体的消息推送方式,有些推送方式不支持此项。
1. `@all`:推送给所有用户。 1. `@all`:推送给所有用户。

View File

@@ -135,6 +135,7 @@ func AddChannel(c *gin.Context) {
URL: channel_.URL, URL: channel_.URL,
Other: channel_.Other, Other: channel_.Other,
CreatedTime: common.GetTimestamp(), CreatedTime: common.GetTimestamp(),
Token: channel_.Token,
} }
err = cleanChannel.Insert() err = cleanChannel.Insert()
if err != nil { if err != nil {
@@ -206,6 +207,7 @@ func UpdateChannel(c *gin.Context) {
cleanChannel.AccountId = channel_.AccountId cleanChannel.AccountId = channel_.AccountId
cleanChannel.URL = channel_.URL cleanChannel.URL = channel_.URL
cleanChannel.Other = channel_.Other cleanChannel.Other = channel_.Other
cleanChannel.Token = channel_.Token
} }
err = cleanChannel.Update() err = cleanChannel.Update()
if err != nil { if err != nil {

View File

@@ -10,6 +10,7 @@ import (
"message-pusher/model" "message-pusher/model"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -46,7 +47,7 @@ func GetPushMessage(c *gin.Context) {
func PostPushMessage(c *gin.Context) { func PostPushMessage(c *gin.Context) {
var message model.Message var message model.Message
if c.Request.Header.Get("Content-Type") == "application/json" { if strings.Contains(strings.ToLower(c.Request.Header.Get("Content-Type")), "application/json") {
// Looks like the user is using JSON // Looks like the user is using JSON
message = model.Message{} message = model.Message{}
err := json.NewDecoder(c.Request.Body).Decode(&message) err := json.NewDecoder(c.Request.Body).Decode(&message)
@@ -110,28 +111,26 @@ func pushMessageHelper(c *gin.Context, message *model.Message) {
}) })
return return
} }
if user.Token != "" && user.Token != " " { if message.Token == "" {
if message.Token == "" { message.Token = strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer ")
message.Token = c.Request.Header.Get("Authorization")
if message.Token == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "token 为空",
})
return
}
}
if user.Token != message.Token {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的 token",
})
return
}
} }
processMessage(c, message, &user) processMessage(c, message, &user)
} }
func authMessage(messageToken string, userToken string, channelToken *string) bool {
if userToken != "" {
if messageToken == userToken {
return true
}
}
if channelToken != nil && *channelToken != "" {
if messageToken != *channelToken {
return false
}
}
return true
}
func processMessage(c *gin.Context, message *model.Message, user *model.User) { func processMessage(c *gin.Context, message *model.Message, user *model.User) {
if message.Title == "" { if message.Title == "" {
message.Title = common.SystemName message.Title = common.SystemName
@@ -150,6 +149,20 @@ func processMessage(c *gin.Context, message *model.Message, user *model.User) {
}) })
return return
} }
if !authMessage(message.Token, user.Token, channel_.Token) {
if message.Token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "通道维度或用户维度设置了鉴权令牌,需要提供鉴权令牌",
})
return
}
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "无效的 token",
})
return
}
err = saveAndSendMessage(user, message, channel_) err = saveAndSendMessage(user, message, channel_)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{

View File

@@ -25,18 +25,19 @@ const (
) )
type Channel struct { type Channel struct {
Id int `json:"id"` Id int `json:"id"`
Type string `json:"type" gorm:"type:varchar(32)"` Type string `json:"type" gorm:"type:varchar(32)"`
UserId int `json:"user_id" gorm:"uniqueIndex:name_user_id;index"` UserId int `json:"user_id" gorm:"uniqueIndex:name_user_id;index"`
Name string `json:"name" gorm:"type:varchar(32);uniqueIndex:name_user_id"` Name string `json:"name" gorm:"type:varchar(32);uniqueIndex:name_user_id"`
Description string `json:"description"` Description string `json:"description"`
Status int `json:"status" gorm:"default:1"` // enabled, disabled Status int `json:"status" gorm:"default:1"` // enabled, disabled
Secret string `json:"secret" gorm:"index"` Secret string `json:"secret" gorm:"index"`
AppId string `json:"app_id"` AppId string `json:"app_id"`
AccountId string `json:"account_id"` AccountId string `json:"account_id"`
URL string `json:"url" gorm:"column:url"` URL string `json:"url" gorm:"column:url"`
Other string `json:"other"` Other string `json:"other"`
CreatedTime int64 `json:"created_time" gorm:"bigint"` CreatedTime int64 `json:"created_time" gorm:"bigint"`
Token *string `json:"token" gorm:"token"`
} }
type BriefChannel struct { type BriefChannel struct {
@@ -120,7 +121,7 @@ func (channel *Channel) UpdateStatus(status int) error {
// Update Make sure your token's fields is completed, because this will update non-zero values // Update Make sure your token's fields is completed, because this will update non-zero values
func (channel *Channel) Update() error { func (channel *Channel) Update() error {
var err error var err error
err = DB.Model(channel).Select("type", "name", "description", "secret", "app_id", "account_id", "url", "other", "status").Updates(channel).Error err = DB.Model(channel).Select("type", "name", "description", "secret", "app_id", "account_id", "url", "other", "status", "token").Updates(channel).Error
return err return err
} }

View File

@@ -1,6 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Header, Message } from 'semantic-ui-react'; import { Button, Form, Grid, Header, Message } from 'semantic-ui-react';
import { API, showError, showSuccess, testChannel } from '../helpers'; import {
API,
generateToken,
showError,
showSuccess,
testChannel,
} from '../helpers';
import { loadUser, loadUserChannels } from '../helpers/loader'; import { loadUser, loadUserChannels } from '../helpers/loader';
const PushSetting = () => { const PushSetting = () => {
@@ -70,15 +76,25 @@ const PushSetting = () => {
options={channels} options={channels}
value={user.channel} value={user.channel}
onChange={handleInputChange} onChange={handleInputChange}
width={6} width={5}
/> />
<Form.Input <Form.Input
label='全局鉴权令牌' label='全局鉴权令牌'
placeholder='未设置渠道维度令牌时,会检查该令牌,如果该令牌也没有设置,则不检查' placeholder='优先级高于通道维度令牌,但为了安全期间建议使用通道维度的令牌'
value={user.token} value={user.token}
name='token' name='token'
onChange={handleInputChange} onChange={handleInputChange}
width={10} width={9}
action={{
content: '随机生成',
onClick: () => {
console.log('generate token');
setUser((inputs) => ({
...inputs,
token: generateToken(16),
}));
},
}}
/> />
</Form.Group> </Form.Group>
<Button onClick={() => submit('general')} loading={loading}> <Button onClick={() => submit('general')} loading={loading}>