1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-02-13 03:50:07 +08:00

feat(system): Password encryption for login API. (#7825)

This commit is contained in:
zhengkunwang 2025-02-08 16:43:55 +08:00 committed by GitHub
parent a86d56bef5
commit 8b30308a4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 258 additions and 307 deletions

View File

@ -37,16 +37,11 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*d
if err != nil { if err != nil {
return nil, "", buserr.New("ErrRecordNotFound") return nil, "", buserr.New("ErrRecordNotFound")
} }
passwordSetting, err := settingRepo.Get(repo.WithByKey("Password")) if nameSetting.Value != info.Name {
if err != nil {
return nil, "", buserr.New("ErrRecordNotFound")
}
pass, err := encrypt.StringDecrypt(passwordSetting.Value)
if err != nil {
return nil, "ErrAuth", nil return nil, "ErrAuth", nil
} }
if !hmac.Equal([]byte(info.Password), []byte(pass)) || nameSetting.Value != info.Name { if err = checkPassword(info.Password); err != nil {
return nil, "ErrAuth", nil return nil, "ErrAuth", err
} }
entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
if err != nil { if err != nil {
@ -77,17 +72,12 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance strin
if err != nil { if err != nil {
return nil, "", buserr.New("ErrRecordNotFound") return nil, "", buserr.New("ErrRecordNotFound")
} }
passwordSetting, err := settingRepo.Get(repo.WithByKey("Password")) if nameSetting.Value != info.Name {
if err != nil {
return nil, "", buserr.New("ErrRecordNotFound")
}
pass, err := encrypt.StringDecrypt(passwordSetting.Value)
if err != nil {
return nil, "", err
}
if !hmac.Equal([]byte(info.Password), []byte(pass)) || nameSetting.Value != info.Name {
return nil, "ErrAuth", nil return nil, "ErrAuth", nil
} }
if err = checkPassword(info.Password); err != nil {
return nil, "ErrAuth", err
}
entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@ -216,3 +206,28 @@ func (u *AuthService) IsLogin(c *gin.Context) bool {
_, err := global.SESSION.Get(c) _, err := global.SESSION.Get(c)
return err == nil return err == nil
} }
func checkPassword(password string) error {
priKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PRIVATE_KEY"))
privateKey, err := encrypt.ParseRSAPrivateKey(priKey.Value)
if err != nil {
return err
}
loginPassword, err := encrypt.DecryptPassword(password, privateKey)
if err != nil {
return err
}
passwordSetting, err := settingRepo.Get(repo.WithByKey("Password"))
if err != nil {
return err
}
existPassword, err := encrypt.StringDecrypt(passwordSetting.Value)
if err != nil {
return err
}
if !hmac.Equal([]byte(loginPassword), []byte(existPassword)) {
return buserr.New("ErrAuth")
}
return nil
}

View File

@ -1,6 +1,8 @@
package service package service
import ( import (
"crypto/rand"
"crypto/rsa"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
@ -45,6 +47,8 @@ type ISettingService interface {
UpdateTerminal(req dto.TerminalInfo) error UpdateTerminal(req dto.TerminalInfo) error
UpdateSystemSSL() error UpdateSystemSSL() error
GenerateRSAKey() error
} }
func NewISettingService() ISettingService { func NewISettingService() ISettingService {
@ -497,3 +501,29 @@ func checkCertValid() error {
return nil return nil
} }
func (u *SettingService) GenerateRSAKey() error {
priKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PRIVATE_KEY"))
pubKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PUBLIC_KEY"))
if priKey.Value != "" && pubKey.Value != "" {
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
privateKeyPEM := encrypt.ExportPrivateKeyToPEM(privateKey)
publicKeyPEM, err := encrypt.ExportPublicKeyToPEM(&privateKey.PublicKey)
if err != nil {
return err
}
err = settingRepo.UpdateOrCreate("PASSWORD_PRIVATE_KEY", privateKeyPEM)
if err != nil {
return err
}
err = settingRepo.UpdateOrCreate("PASSWORD_PUBLIC_KEY", publicKeyPEM)
if err != nil {
return err
}
return nil
}

View File

@ -1,6 +1,7 @@
package hook package hook
import ( import (
"github.com/1Panel-dev/1Panel/core/app/service"
"strings" "strings"
"github.com/1Panel-dev/1Panel/core/app/repo" "github.com/1Panel-dev/1Panel/core/app/repo"
@ -29,6 +30,8 @@ func Init() {
} }
handleUserInfo(global.CONF.Base.ChangeUserInfo, settingRepo) handleUserInfo(global.CONF.Base.ChangeUserInfo, settingRepo)
generateKey()
} }
func handleUserInfo(tags string, settingRepo repo.ISettingRepo) { func handleUserInfo(tags string, settingRepo repo.ISettingRepo) {
@ -68,3 +71,9 @@ func handleUserInfo(tags string, settingRepo repo.ISettingRepo) {
sudo := cmd.SudoHandleCmd() sudo := cmd.SudoHandleCmd()
_, _ = cmd.Execf("%s sed -i '/CHANGE_USER_INFO=%v/d' /usr/local/bin/1pctl", sudo, global.CONF.Base.ChangeUserInfo) _, _ = cmd.Execf("%s sed -i '/CHANGE_USER_INFO=%v/d' /usr/local/bin/1pctl", sudo, global.CONF.Base.ChangeUserInfo)
} }
func generateKey() {
if err := service.NewISettingService().GenerateRSAKey(); err != nil {
global.LOG.Errorf("generate rsa key error : %s", err.Error())
}
}

View File

@ -167,6 +167,7 @@ func Routers() *gin.Engine {
PrivateGroup.Use(middleware.WhiteAllow()) PrivateGroup.Use(middleware.WhiteAllow())
PrivateGroup.Use(middleware.BindDomain()) PrivateGroup.Use(middleware.BindDomain())
PrivateGroup.Use(middleware.GlobalLoading()) PrivateGroup.Use(middleware.GlobalLoading())
PrivateGroup.Use(middleware.SetPasswordPublicKey())
for _, router := range rou.RouterGroupApp { for _, router := range rou.RouterGroupApp {
router.InitRouter(PrivateGroup) router.InitRouter(PrivateGroup)
} }

View File

@ -0,0 +1,22 @@
package middleware
import (
"encoding/base64"
"github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/gin-gonic/gin"
)
func SetPasswordPublicKey() gin.HandlerFunc {
return func(c *gin.Context) {
cookieKey, _ := c.Cookie("panel_public_key")
settingRepo := repo.NewISettingRepo()
key, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PUBLIC_KEY"))
base64Key := base64.StdEncoding.EncodeToString([]byte(key.Value))
if base64Key == cookieKey {
c.Next()
return
}
c.SetCookie("panel_public_key", base64Key, 7*24*60*60, "/", "", false, false)
c.Next()
}
}

View File

@ -5,26 +5,19 @@ import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/pem"
"errors"
"fmt" "fmt"
"io" "io"
"strings"
"github.com/1Panel-dev/1Panel/core/app/model" "github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/global"
) )
func StringEncryptWithBase64(text string) (string, error) {
base64Item, err := base64.StdEncoding.DecodeString(text)
if err != nil {
return "", err
}
encryptItem, err := StringEncrypt(string(base64Item))
if err != nil {
return "", err
}
return encryptItem, nil
}
func StringDecryptWithBase64(text string) (string, error) { func StringDecryptWithBase64(text string) (string, error) {
decryptItem, err := StringDecrypt(text) decryptItem, err := StringDecrypt(text)
if err != nil { if err != nil {
@ -136,3 +129,95 @@ func aesDecryptWithSalt(key, ciphertext []byte) ([]byte, error) {
ciphertext = unPadding(ciphertext) ciphertext = unPadding(ciphertext)
return ciphertext, nil return ciphertext, nil
} }
func ParseRSAPrivateKey(privateKeyPEM string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(privateKeyPEM))
if block == nil || block.Type != "RSA PRIVATE KEY" {
return nil, errors.New("failed to decode PEM block containing the private key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return privateKey, nil
}
func aesDecrypt(ciphertext, key, iv []byte) ([]byte, error) {
if len(key) != 16 && len(key) != 24 && len(key) != 32 {
return nil, errors.New("invalid AES key length: must be 16, 24, or 32 bytes")
}
if len(iv) != aes.BlockSize {
return nil, errors.New("invalid IV length: must be 16 bytes")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(ciphertext, ciphertext)
ciphertext = pkcs7Unpad(ciphertext)
return ciphertext, nil
}
func pkcs7Unpad(data []byte) []byte {
length := len(data)
padLength := int(data[length-1])
return data[:length-padLength]
}
func DecryptPassword(encryptedData string, privateKey *rsa.PrivateKey) (string, error) {
parts := strings.Split(encryptedData, ":")
if len(parts) != 3 {
return "", errors.New("encrypted data format error")
}
keyCipher := parts[0]
ivBase64 := parts[1]
ciphertextBase64 := parts[2]
encryptedAESKey, err := base64.StdEncoding.DecodeString(keyCipher)
if err != nil {
return "", errors.New("failed to decode keyCipher")
}
aesKey, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, encryptedAESKey)
if err != nil {
return "", errors.New("failed to decode AES Key")
}
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64)
if err != nil {
return "", errors.New("failed to decrypt the encrypted data")
}
iv, err := base64.StdEncoding.DecodeString(ivBase64)
if err != nil {
return "", errors.New("failed to decode the IV")
}
password, err := aesDecrypt(ciphertext, aesKey, iv)
if err != nil {
return "", err
}
return string(password), nil
}
func ExportPrivateKeyToPEM(privateKey *rsa.PrivateKey) string {
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: privateKeyBytes,
})
return string(privateKeyPEM)
}
func ExportPublicKeyToPEM(publicKey *rsa.PublicKey) (string, error) {
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return "", err
}
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: publicKeyBytes,
})
return string(publicKeyPEM), nil
}

View File

@ -20,8 +20,8 @@
"prettier": "prettier --write ." "prettier": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-php": "^6.0.1", "@codemirror/lang-php": "^6.0.1",
"@codemirror/language": "^6.10.2", "@codemirror/language": "^6.10.2",
"@codemirror/legacy-modes": "^6.4.0", "@codemirror/legacy-modes": "^6.4.0",
@ -35,11 +35,13 @@
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"crypto-js": "^4.2.0",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"element-plus": "^2.7.5", "element-plus": "^2.7.5",
"fit2cloud-ui-plus": "^1.2.0", "fit2cloud-ui-plus": "^1.2.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"md-editor-v3": "^2.11.3", "md-editor-v3": "^2.11.3",
"monaco-editor": "^0.50.0", "monaco-editor": "^0.50.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",

View File

@ -4,6 +4,8 @@ import useClipboard from 'vue-clipboard3';
const { toClipboard } = useClipboard(); const { toClipboard } = useClipboard();
import { MsgError, MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import JSEncrypt from 'jsencrypt';
import CryptoJS from 'crypto-js';
export function deepCopy<T>(obj: any): T { export function deepCopy<T>(obj: any): T {
let newObj: any; let newObj: any;
@ -695,3 +697,61 @@ export function getRuntimeLabel(type: string) {
} }
return ''; return '';
} }
function getCookie(name: string) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
function rsaEncrypt(data: string, publicKey: string) {
if (!data) {
return data;
}
const jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey(publicKey);
return jsEncrypt.encrypt(data);
}
function aesEncrypt(data: string, key: string) {
const keyBytes = CryptoJS.enc.Utf8.parse(key);
const iv = CryptoJS.lib.WordArray.random(16);
const encrypted = CryptoJS.AES.encrypt(data, keyBytes, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return iv.toString(CryptoJS.enc.Base64) + ':' + encrypted.toString();
}
function urlDecode(value: string): string {
return decodeURIComponent(value.replace(/\+/g, ' '));
}
function generateAESKey(): string {
const keyLength = 16;
const randomBytes = new Uint8Array(keyLength);
crypto.getRandomValues(randomBytes);
return Array.from(randomBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
export const encryptPassword = (password: string) => {
if (!password) {
return '';
}
let rsaPublicKeyText = getCookie('panel_public_key');
if (!rsaPublicKeyText) {
console.log('RSA public key not found');
return password;
}
rsaPublicKeyText = urlDecode(rsaPublicKeyText);
const aesKey = generateAESKey();
rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', '');
const rsaPublicKey = atob(rsaPublicKeyText);
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey);
const passwordCipher = aesEncrypt(password, aesKey);
return `${keyCipher}:${passwordCipher}`;
};

View File

@ -189,6 +189,7 @@ import { GlobalStore, MenuStore, TabsStore } from '@/store';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { getSettingInfo } from '@/api/modules/setting'; import { getSettingInfo } from '@/api/modules/setting';
import { encryptPassword } from '@/utils/util';
const i18n = useI18n(); const i18n = useI18n();
const themeConfig = computed(() => globalStore.themeConfig); const themeConfig = computed(() => globalStore.themeConfig);
@ -313,7 +314,7 @@ const login = (formEl: FormInstance | undefined) => {
} }
let requestLoginForm = { let requestLoginForm = {
name: loginForm.name, name: loginForm.name,
password: loginForm.password, password: encryptPassword(loginForm.password),
ignoreCaptcha: globalStore.ignoreCaptcha, ignoreCaptcha: globalStore.ignoreCaptcha,
captcha: loginForm.captcha, captcha: loginForm.captcha,
captchaID: captcha.captchaID, captchaID: captcha.captchaID,
@ -352,7 +353,6 @@ const login = (formEl: FormInstance | undefined) => {
globalStore.ignoreCaptcha = false; globalStore.ignoreCaptcha = false;
errCaptcha.value = false; errCaptcha.value = false;
errAuthInfo.value = true; errAuthInfo.value = true;
console.log('11111');
} }
} }
loginVerify(); loginVerify();
@ -368,7 +368,7 @@ const mfaLogin = async (auto: boolean) => {
if ((!auto && mfaLoginForm.code) || (auto && mfaLoginForm.code.length === 6)) { if ((!auto && mfaLoginForm.code) || (auto && mfaLoginForm.code.length === 6)) {
isLoggingIn = true; isLoggingIn = true;
mfaLoginForm.name = loginForm.name; mfaLoginForm.name = loginForm.name;
mfaLoginForm.password = loginForm.password; mfaLoginForm.password = encryptPassword(loginForm.password);
try { try {
await mfaLoginApi(mfaLoginForm); await mfaLoginApi(mfaLoginForm);
globalStore.setLogStatus(true); globalStore.setLogStatus(true);
@ -464,173 +464,3 @@ onMounted(() => {
padding: 0 !important; padding: 0 !important;
} }
</style> </style>
<!-- <style scoped lang='scss' > .login-form {
padding: 0 40px;
.hide {
width: 0;
border: 0;
position: absolute;
visibility: hidden;
}
.login-title {
font-size: 30px;
letter-spacing: 0;
text-align: center;
color: #646a73;
margin-bottom: 30px;
}
.no-border {
:deep(.el-input__wrapper) {
background: none !important;
box-shadow: none !important;
border-radius: 0 !important;
border-bottom: 1px solid #dcdfe6;
}
}
.el-input {
height: 44px;
}
.login-captcha {
margin-top: 10px;
:deep(.el-input__wrapper) {
background: none !important;
box-shadow: none !important;
border-radius: 0 !important;
border-bottom: 1px solid #dcdfe6;
}
.el-input {
width: 50%;
height: 44px;
}
img {
width: 45%;
height: 44px;
margin-left: 5%;
}
}
.login-msg {
margin-top: 10px;
padding: 0 40px;
text-align: center;
}
.login-image {
width: 480px;
height: 480px;
@media only screen and (max-width: 1280px) {
height: 380px;
}
}
.submit {
width: 100%;
border-radius: 0;
}
.forget-password {
margin-top: 40px;
padding: 0 40px;
float: right;
@media only screen and (max-width: 1280px) {
margin-top: 20px;
}
}
.login-button {
width: 100%;
height: 45px;
margin-top: 10px;
background-color: #005eeb;
border-color: #005eeb;
color: #ffffff;
&:hover {
--el-button-hover-border-color: #005eeb;
}
}
.demo {
text-align: center;
span {
color: red;
}
}
.login-form-header {
display: flex;
margin-bottom: 30px;
justify-content: space-between;
align-items: center;
.title {
color: #646a73;
font-size: 25px;
}
}
.agree {
white-space: pre-wrap;
line-height: 14px;
color: #005eeb;
}
:deep(a) {
color: #005eeb;
&:hover {
color: #005eeb95;
}
}
:deep(.el-checkbox__input .el-checkbox__inner) {
background-color: #fff !important;
border-color: #fff !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #005eeb !important;
border-color: #005eeb !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner::after) {
border-color: #fff !important;
}
.agree-helper {
min-height: 20px;
margin-top: -20px;
margin-left: 20px;
}
:deep(.el-input__inner) {
color: #000 !important;
}
}
.cursor-pointer {
outline: none;
}
.el-dropdown:focus-visible {
outline: none;
}
.el-tooltip__trigger:focus-visible {
outline: none;
}
:deep(.el-dropdown-menu__item:not(.is-disabled):hover) {
color: #005eeb !important;
background-color: #e5eefd !important;
}
:deep(.el-dropdown-menu__item:not(.is-disabled):focus) {
color: #005eeb !important;
background-color: #e5eefd !important;
}
.login-footer-btn {
.el-button--primary {
border-color: #005eeb !important;
background-color: #005eeb !important;
}
}
</style>
-->

View File

@ -19,23 +19,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- <div>
<div class="login-background" v-loading="loading">
<div class="login-wrapper">
<div :class="screenWidth > 1110 ? 'left inline-block' : ''">
<div class="login-title">
<span>{{ gStore.themeConfig.title || $t('setting.description') }}</span>
</div>
<img src="@/assets/images/1panel-login.png" alt="" v-if="screenWidth > 1110" />
</div>
<div :class="screenWidth > 1110 ? 'right inline-block' : ''">
<div class="login-container">
<LoginForm ref="loginRef"></LoginForm>
</div>
</div>
</div>
</div>
</div> -->
</template> </template>
<script setup lang="ts" name="login"> <script setup lang="ts" name="login">
@ -48,7 +31,7 @@ import { getXpackSettingForTheme } from '@/utils/xpack';
const gStore = GlobalStore(); const gStore = GlobalStore();
const loading = ref(); const loading = ref();
const backgroundOpacity = ref(1); const backgroundOpacity = ref(0.8);
const backgroundImage = ref(new URL('', import.meta.url).href); const backgroundImage = ref(new URL('', import.meta.url).href);
const logoImage = ref(new URL('@/assets/images/1panel-login.png', import.meta.url).href); const logoImage = ref(new URL('@/assets/images/1panel-login.png', import.meta.url).href);
@ -91,89 +74,3 @@ onMounted(() => {
}; };
}); });
</script> </script>
<!-- <style scoped lang="scss">
@mixin login-center {
display: flex;
justify-content: center;
align-items: center;
}
.login-background {
height: 100vh;
background: url(@/assets/images/1panel-login-bg.png) no-repeat,
radial-gradient(153.25% 257.2% at 118.99% 181.67%, rgba(50, 132, 255, 0.2) 0%, rgba(82, 120, 255, 0) 100%)
/* warning: gradient uses a rotation that is not supported by CSS and may not behave as expected */,
radial-gradient(123.54% 204.83% at 25.87% 195.17%, rgba(111, 76, 253, 0.15) 0%, rgba(122, 76, 253, 0) 78.85%)
/* warning: gradient uses a rotation that is not supported by CSS and may not behave as expected */,
linear-gradient(0deg, rgba(0, 94, 235, 0.03), rgba(0, 94, 235, 0.03)),
radial-gradient(109.58% 109.58% at 31.53% -36.58%, rgba(0, 94, 235, 0.3) 0%, rgba(0, 94, 235, 0) 100%)
/* warning: gradient uses a rotation that is not supported by CSS and may not behave as expected */,
rgba(0, 57, 142, 0.05);
.login-wrapper {
padding-top: 8%;
width: 80%;
margin: 0 auto;
// @media only screen and (max-width: 1440px) {
// width: 100%;
// padding-top: 6%;
// }
.left {
vertical-align: middle;
text-align: right;
width: 60%;
img {
object-fit: contain;
width: 100%;
@media only screen and (min-width: 1440px) {
width: 85%;
}
}
}
.right {
vertical-align: middle;
width: 40%;
}
}
.login-title {
text-align: right;
margin-right: 10%;
span:first-child {
color: $primary-color;
font-size: 40px;
font-family: pingFangSC-Regular;
font-weight: 600;
@media only screen and (max-width: 768px) {
font-size: 35px;
}
}
@media only screen and (max-width: 1110px) {
margin-bottom: 20px;
font-size: 35px;
text-align: center;
margin-right: 0;
}
}
.login-container {
margin-top: 40px;
padding: 40px 0;
width: 390px;
box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.55);
border-radius: 4px;
box-shadow: 2px 4px 22px rgba(0, 94, 235, 0.2);
@media only screen and (max-width: 1440px) {
margin-top: 60px;
}
@media only screen and (max-width: 1110px) {
margin: 60px auto 0;
}
@media only screen and (max-width: 768px) {
width: 100%;
}
}
}
</style> -->