RSA加密登录时账号和密码

This commit is contained in:
WrV 2022-02-09 16:50:22 +08:00
parent 612c4293d1
commit 94c81296d0
11 changed files with 560 additions and 375 deletions

View File

@ -1,7 +1,15 @@
package com.ruoyi.web.controller.system; package com.ruoyi.web.controller.system;
import java.security.*;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.ruoyi.common.annotation.DecryptLogin;
import com.ruoyi.common.annotation.RateLimiter;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.enums.LimitType;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -34,12 +42,33 @@ public class SysLoginController
@Autowired @Autowired
private SysPermissionService permissionService; private SysPermissionService permissionService;
/**
* 登录前先生成一个RSA密钥LoginBody中的账号密码进行加密
* 公钥返回给前端加密使用
* 私钥存入redis等待登陆时解密使用
*
* 这个接口需要加入白名单所以使用限流器 RateLimiter 对IP限制每秒请求次数
* @return public key
*/
@RateLimiter(time = 1, count = 5, limitType = LimitType.IP)
@GetMapping("/preLogin")
public AjaxResult preLogin() {
String publicKey;
try {
publicKey = loginService.generateRSA();
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
return AjaxResult.error("生成RSA密钥对失败");
}
return AjaxResult.success("", publicKey);
}
/** /**
* 登录方法 * 登录方法
* *
* @param loginBody 登录信息 * @param loginBody 登录信息
* @return 结果 * @return 结果
*/ */
@DecryptLogin
@PostMapping("/login") @PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody) public AjaxResult login(@RequestBody LoginBody loginBody)
{ {

View File

@ -0,0 +1,16 @@
package com.ruoyi.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* RSA解密注解
* 使用该注解可以对已经加密的参数进行解密
* @author wrw
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptLogin {
}

View File

@ -39,6 +39,11 @@ public class Constants
*/ */
public static final String FAIL = "1"; public static final String FAIL = "1";
/**
* 预登录 redis key
*/
public static final String PRE_LOGIN_KEY = "pre_login_key:";
/** /**
* 登录成功 * 登录成功
*/ */

View File

@ -27,6 +27,11 @@ public class LoginBody
*/ */
private String uuid = ""; private String uuid = "";
/**
* RSA公钥
*/
private String publicKey;
public String getUsername() public String getUsername()
{ {
return username; return username;
@ -66,4 +71,8 @@ public class LoginBody
{ {
this.uuid = uuid; this.uuid = uuid;
} }
public String getPublicKey() {
return publicKey;
}
} }

View File

@ -0,0 +1,61 @@
package com.ruoyi.framework.aspectj;
import com.ruoyi.common.annotation.DecryptLogin;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.user.UserException;
import com.ruoyi.common.utils.StringUtils;
import org.apache.commons.codec.binary.Base64;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
/**
* RSA解密
*
* @author wrw
*/
@Aspect
@Component
public class DecryptParameters {
@Autowired
private RedisCache redisCache;
@Before("@annotation(decrypt)")
public void doBefore(JoinPoint point, DecryptLogin decrypt) throws IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, NoSuchPaddingException {
LoginBody pointArg = (LoginBody) point.getArgs()[0];
//从缓存里获取私钥
String loginPrivateKey = redisCache.getCacheObject(Constants.PRE_LOGIN_KEY + pointArg.getPublicKey());
if (StringUtils.isEmpty(loginPrivateKey)) {
throw new UserException("RSA密钥对已过期", point.getArgs());
}
//初始化解密密钥
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(loginPrivateKey));
//创建密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
//解析密钥
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//解密
byte[] username = cipher.doFinal(Base64.decodeBase64(pointArg.getUsername()));
byte[] password = cipher.doFinal(Base64.decodeBase64(pointArg.getPassword()));
pointArg.setUsername(new String(username));
pointArg.setPassword(new String(password));
}
}

View File

@ -97,7 +97,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
// 过滤请求 // 过滤请求
.authorizeRequests() .authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").anonymous() .antMatchers("/preLogin", "/login", "/register", "/captchaImage").anonymous()
.antMatchers( .antMatchers(
HttpMethod.GET, HttpMethod.GET,
"/", "/",

View File

@ -1,6 +1,8 @@
package com.ruoyi.framework.web.service; package com.ruoyi.framework.web.service;
import javax.annotation.Resource; import javax.annotation.Resource;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
@ -24,6 +26,9 @@ import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserService; import com.ruoyi.system.service.ISysUserService;
import java.security.*;
import java.util.concurrent.TimeUnit;
/** /**
* 登录校验方法 * 登录校验方法
* *
@ -130,4 +135,25 @@ public class SysLoginService
sysUser.setLoginDate(DateUtils.getNowDate()); sysUser.setLoginDate(DateUtils.getNowDate());
userService.updateUserProfile(sysUser); userService.updateUserProfile(sysUser);
} }
/**
* 生成RSA密钥对
* @return 公钥
*/
public String generateRSA() throws NoSuchAlgorithmException, NoSuchProviderException {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA", "SunRsaSign");
gen.initialize(512, new SecureRandom());
KeyPair pair = gen.generateKeyPair();
byte[] privateKey = pair.getPrivate().getEncoded();
byte[] publicKey = pair.getPublic().getEncoded();
privateKey = Base64.encodeBase64(privateKey);
publicKey = Base64.encodeBase64(publicKey);
//私钥存入缓存
redisCache.setCacheObject(Constants.PRE_LOGIN_KEY + new String(publicKey), new String(privateKey), 30, TimeUnit.SECONDS);
//返回公钥
return new String(publicKey);
}
} }

View File

@ -1,59 +1,75 @@
import request from '@/utils/request' import request from '@/utils/request'
// 登录方法 // 预登陆获取RSA密钥
export function login(username, password, code, uuid) { export function preLogin() {
const data = { return new Promise(resolve => {
username, request({
password, url: '/preLogin',
code, headers: {
uuid isToken: false
} },
return request({ method: 'get'
url: '/login', }).then(res =>{
headers: { resolve(res.data)
isToken: false })
}, })
method: 'post', }
data: data
}) // 登录方法
} export function login(username, password, code, uuid, publicKey) {
const data = {
// 注册方法 username,
export function register(data) { password,
return request({ code,
url: '/register', uuid,
headers: { publicKey
isToken: false }
}, return request({
method: 'post', url: '/login',
data: data headers: {
}) isToken: false
} },
method: 'post',
// 获取用户详细信息 data: data
export function getInfo() { })
return request({ }
url: '/getInfo',
method: 'get' // 注册方法
}) export function register(data) {
} return request({
url: '/register',
// 退出方法 headers: {
export function logout() { isToken: false
return request({ },
url: '/logout', method: 'post',
method: 'post' data: data
}) })
} }
// 获取验证码 // 获取用户详细信息
export function getCodeImg() { export function getInfo() {
return request({ return request({
url: '/captchaImage', url: '/getInfo',
headers: { method: 'get'
isToken: false })
}, }
method: 'get',
timeout: 20000 // 退出方法
}) export function logout() {
} return request({
url: '/logout',
method: 'post'
})
}
// 获取验证码
export function getCodeImg() {
return request({
url: '/captchaImage',
headers: {
isToken: false
},
method: 'get',
timeout: 20000
})
}

View File

@ -1,96 +1,97 @@
import { login, logout, getInfo } from '@/api/login' import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth' import { getToken, setToken, removeToken } from '@/utils/auth'
const user = { const user = {
state: { state: {
token: getToken(), token: getToken(),
name: '', name: '',
avatar: '', avatar: '',
roles: [], roles: [],
permissions: [] permissions: []
}, },
mutations: { mutations: {
SET_TOKEN: (state, token) => { SET_TOKEN: (state, token) => {
state.token = token state.token = token
}, },
SET_NAME: (state, name) => { SET_NAME: (state, name) => {
state.name = name state.name = name
}, },
SET_AVATAR: (state, avatar) => { SET_AVATAR: (state, avatar) => {
state.avatar = avatar state.avatar = avatar
}, },
SET_ROLES: (state, roles) => { SET_ROLES: (state, roles) => {
state.roles = roles state.roles = roles
}, },
SET_PERMISSIONS: (state, permissions) => { SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions state.permissions = permissions
} }
}, },
actions: { actions: {
// 登录 // 登录
Login({ commit }, userInfo) { Login({ commit }, userInfo) {
const username = userInfo.username.trim() const username = userInfo.username.trim()
const password = userInfo.password const password = userInfo.password
const code = userInfo.code const code = userInfo.code
const uuid = userInfo.uuid const uuid = userInfo.uuid
return new Promise((resolve, reject) => { const publicKey = userInfo.publicKey
login(username, password, code, uuid).then(res => { return new Promise((resolve, reject) => {
setToken(res.token) login(username, password, code, uuid, publicKey).then(res => {
commit('SET_TOKEN', res.token) setToken(res.token)
resolve() commit('SET_TOKEN', res.token)
}).catch(error => { resolve()
reject(error) }).catch(error => {
}) reject(error)
}) })
}, })
},
// 获取用户信息
GetInfo({ commit, state }) { // 获取用户信息
return new Promise((resolve, reject) => { GetInfo({ commit, state }) {
getInfo().then(res => { return new Promise((resolve, reject) => {
const user = res.user getInfo().then(res => {
const avatar = user.avatar == "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar; const user = res.user
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 const avatar = user.avatar == "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
commit('SET_ROLES', res.roles) if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_PERMISSIONS', res.permissions) commit('SET_ROLES', res.roles)
} else { commit('SET_PERMISSIONS', res.permissions)
commit('SET_ROLES', ['ROLE_DEFAULT']) } else {
} commit('SET_ROLES', ['ROLE_DEFAULT'])
commit('SET_NAME', user.userName) }
commit('SET_AVATAR', avatar) commit('SET_NAME', user.userName)
resolve(res) commit('SET_AVATAR', avatar)
}).catch(error => { resolve(res)
reject(error) }).catch(error => {
}) reject(error)
}) })
}, })
},
// 退出系统
LogOut({ commit, state }) { // 退出系统
return new Promise((resolve, reject) => { LogOut({ commit, state }) {
logout(state.token).then(() => { return new Promise((resolve, reject) => {
commit('SET_TOKEN', '') logout(state.token).then(() => {
commit('SET_ROLES', []) commit('SET_TOKEN', '')
commit('SET_PERMISSIONS', []) commit('SET_ROLES', [])
removeToken() commit('SET_PERMISSIONS', [])
resolve() removeToken()
}).catch(error => { resolve()
reject(error) }).catch(error => {
}) reject(error)
}) })
}, })
},
// 前端 登出
FedLogOut({ commit }) { // 前端 登出
return new Promise(resolve => { FedLogOut({ commit }) {
commit('SET_TOKEN', '') return new Promise(resolve => {
removeToken() commit('SET_TOKEN', '')
resolve() removeToken()
}) resolve()
} })
} }
} }
}
export default user
export default user

View File

@ -28,3 +28,15 @@ export function decrypt(txt) {
return encryptor.decrypt(txt) // 对数据进行解密 return encryptor.decrypt(txt) // 对数据进行解密
} }
/**
* 对登录的用户名密码加密
* @param loginPublicKey
* @param txt 待加密文本
* @returns {Promise<ArrayBuffer>}
*/
export function encryptLogin(loginPublicKey, txt) {
const encrypt = new JSEncrypt();
encrypt.setPublicKey(loginPublicKey);
return encrypt.encrypt(txt);
}

View File

@ -1,219 +1,229 @@
<template> <template>
<div class="login"> <div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">若依后台管理系统</h3> <h3 class="title">若依后台管理系统</h3>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="loginForm.username" v-model="loginForm.username"
type="text" type="text"
auto-complete="off" auto-complete="off"
placeholder="账号" placeholder="账号"
> >
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input
v-model="loginForm.password" v-model="loginForm.password"
type="password" type="password"
auto-complete="off" auto-complete="off"
placeholder="密码" placeholder="密码"
@keyup.enter.native="handleLogin" @keyup.enter.native="handleLogin"
> >
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="code" v-if="captchaOnOff"> <el-form-item prop="code" v-if="captchaOnOff">
<el-input <el-input
v-model="loginForm.code" v-model="loginForm.code"
auto-complete="off" auto-complete="off"
placeholder="验证码" placeholder="验证码"
style="width: 63%" style="width: 63%"
@keyup.enter.native="handleLogin" @keyup.enter.native="handleLogin"
> >
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input> </el-input>
<div class="login-code"> <div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/> <img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div> </div>
</el-form-item> </el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;"> <el-form-item style="width:100%;">
<el-button <el-button
:loading="loading" :loading="loading"
size="medium" size="medium"
type="primary" type="primary"
style="width:100%;" style="width:100%;"
@click.native.prevent="handleLogin" @click.native.prevent="handleLogin"
> >
<span v-if="!loading"> </span> <span v-if="!loading"> </span>
<span v-else> 中...</span> <span v-else> 中...</span>
</el-button> </el-button>
<div style="float: right;" v-if="register"> <div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link> <router-link class="link-type" :to="'/register'">立即注册</router-link>
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 底部 --> <!-- 底部 -->
<div class="el-login-footer"> <div class="el-login-footer">
<span>Copyright © 2018-2022 ruoyi.vip All Rights Reserved.</span> <span>Copyright © 2018-2022 ruoyi.vip All Rights Reserved.</span>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { getCodeImg } from "@/api/login"; import { preLogin } from "@/api/login";
import Cookies from "js-cookie"; import { getCodeImg } from "@/api/login";
import { encrypt, decrypt } from '@/utils/jsencrypt' import Cookies from "js-cookie";
import { encrypt, decrypt, encryptLogin } from '@/utils/jsencrypt'
export default {
name: "Login", export default {
data() { name: "Login",
return { data() {
codeUrl: "", return {
loginForm: { codeUrl: "",
username: "admin", loginForm: {
password: "admin123", username: "admin",
rememberMe: false, password: "admin123",
code: "", rememberMe: false,
uuid: "" code: "",
}, uuid: ""
loginRules: { },
username: [ loginRules: {
{ required: true, trigger: "blur", message: "请输入您的账号" } username: [
], { required: true, trigger: "blur", message: "请输入您的账号" }
password: [ ],
{ required: true, trigger: "blur", message: "请输入您的密码" } password: [
], { required: true, trigger: "blur", message: "请输入您的密码" }
code: [{ required: true, trigger: "change", message: "请输入验证码" }] ],
}, code: [{ required: true, trigger: "change", message: "请输入验证码" }]
loading: false, },
// loading: false,
captchaOnOff: true, //
// captchaOnOff: true,
register: false, //
redirect: undefined register: false,
}; redirect: undefined
}, };
watch: { },
$route: { watch: {
handler: function(route) { $route: {
this.redirect = route.query && route.query.redirect; handler: function(route) {
}, this.redirect = route.query && route.query.redirect;
immediate: true },
} immediate: true
}, }
created() { },
this.getCode(); created() {
this.getCookie(); this.getCode();
}, this.getCookie();
methods: { },
getCode() { methods: {
getCodeImg().then(res => { getCode() {
this.captchaOnOff = res.captchaOnOff === undefined ? true : res.captchaOnOff; getCodeImg().then(res => {
if (this.captchaOnOff) { this.captchaOnOff = res.captchaOnOff === undefined ? true : res.captchaOnOff;
this.codeUrl = "data:image/gif;base64," + res.img; if (this.captchaOnOff) {
this.loginForm.uuid = res.uuid; this.codeUrl = "data:image/gif;base64," + res.img;
} this.loginForm.uuid = res.uuid;
}); }
}, });
getCookie() { },
const username = Cookies.get("username"); getCookie() {
const password = Cookies.get("password"); const username = Cookies.get("username");
const rememberMe = Cookies.get('rememberMe') const password = Cookies.get("password");
this.loginForm = { const rememberMe = Cookies.get('rememberMe')
username: username === undefined ? this.loginForm.username : username, this.loginForm = {
password: password === undefined ? this.loginForm.password : decrypt(password), username: username === undefined ? this.loginForm.username : username,
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe) password: password === undefined ? this.loginForm.password : decrypt(password),
}; rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
}, };
handleLogin() { },
this.$refs.loginForm.validate(valid => { async handleLogin() {
if (valid) { const login_public_key = await preLogin();
this.loading = true; this.$refs.loginForm.validate(valid => {
if (this.loginForm.rememberMe) { if (valid) {
Cookies.set("username", this.loginForm.username, { expires: 30 }); this.loading = true;
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 }); if (this.loginForm.rememberMe) {
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 }); Cookies.set("username", this.loginForm.username, { expires: 30 });
} else { Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
Cookies.remove("username"); Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
Cookies.remove("password"); } else {
Cookies.remove('rememberMe'); Cookies.remove("username");
} Cookies.remove("password");
this.$store.dispatch("Login", this.loginForm).then(() => { Cookies.remove('rememberMe');
this.$router.push({ path: this.redirect || "/" }).catch(()=>{}); }
}).catch(() => { let authLoginForm = {};
this.loading = false; // encrypt
if (this.captchaOnOff) { authLoginForm.username = encryptLogin(login_public_key, this.loginForm.username);
this.getCode(); authLoginForm.password = encryptLogin(login_public_key, this.loginForm.password);
} authLoginForm.code = this.loginForm.code;
}); authLoginForm.uuid = this.loginForm.uuid;
} authLoginForm.publicKey = login_public_key;
}); console.log(authLoginForm);
} this.$store.dispatch("Login", authLoginForm).then(() => {
} this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}; }).catch(() => {
</script> this.loading = false;
if (this.captchaOnOff) {
<style rel="stylesheet/scss" lang="scss"> this.getCode();
.login { }
display: flex; });
justify-content: center; }
align-items: center; });
height: 100%; }
background-image: url("../assets/images/login-background.jpg"); }
background-size: cover; };
} </script>
.title {
margin: 0px auto 30px auto; <style rel="stylesheet/scss" lang="scss">
text-align: center; .login {
color: #707070; display: flex;
} justify-content: center;
align-items: center;
.login-form { height: 100%;
border-radius: 6px; background-image: url("../assets/images/login-background.jpg");
background: #ffffff; background-size: cover;
width: 400px; }
padding: 25px 25px 5px 25px; .title {
.el-input { margin: 0px auto 30px auto;
height: 38px; text-align: center;
input { color: #707070;
height: 38px; }
}
} .login-form {
.input-icon { border-radius: 6px;
height: 39px; background: #ffffff;
width: 14px; width: 400px;
margin-left: 2px; padding: 25px 25px 5px 25px;
} .el-input {
} height: 38px;
.login-tip { input {
font-size: 13px; height: 38px;
text-align: center; }
color: #bfbfbf; }
} .input-icon {
.login-code { height: 39px;
width: 33%; width: 14px;
height: 38px; margin-left: 2px;
float: right; }
img { }
cursor: pointer; .login-tip {
vertical-align: middle; font-size: 13px;
} text-align: center;
} color: #bfbfbf;
.el-login-footer { }
height: 40px; .login-code {
line-height: 40px; width: 33%;
position: fixed; height: 38px;
bottom: 0; float: right;
width: 100%; img {
text-align: center; cursor: pointer;
color: #fff; vertical-align: middle;
font-family: Arial; }
font-size: 12px; }
letter-spacing: 1px; .el-login-footer {
} height: 40px;
.login-code-img { line-height: 40px;
height: 38px; position: fixed;
} bottom: 0;
</style> width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
.login-code-img {
height: 38px;
}
</style>