diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java index dab2bf879..594b68645 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -1,7 +1,15 @@ package com.ruoyi.web.controller.system; +import java.security.*; import java.util.List; 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.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -34,12 +42,33 @@ public class SysLoginController @Autowired 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 登录信息 * @return 结果 */ + @DecryptLogin @PostMapping("/login") public AjaxResult login(@RequestBody LoginBody loginBody) { diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DecryptLogin.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DecryptLogin.java new file mode 100644 index 000000000..6b4eed0b1 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DecryptLogin.java @@ -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 { +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java index 6deff23bb..247617c29 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java @@ -39,6 +39,11 @@ public class Constants */ public static final String FAIL = "1"; + /** + * 预登录 redis key + */ + public static final String PRE_LOGIN_KEY = "pre_login_key:"; + /** * 登录成功 */ diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java index 583b7a61e..75482a90e 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java @@ -27,6 +27,11 @@ public class LoginBody */ private String uuid; + /** + * RSA公钥 + */ + private String publicKey; + public String getUsername() { return username; @@ -66,4 +71,8 @@ public class LoginBody { this.uuid = uuid; } + + public String getPublicKey() { + return publicKey; + } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DecryptParameters.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DecryptParameters.java new file mode 100644 index 000000000..4e56c8189 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DecryptParameters.java @@ -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)); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index 58dd02ad0..86e3a9271 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -97,7 +97,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter // 过滤请求 .authorizeRequests() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - .antMatchers("/login", "/register", "/captchaImage").anonymous() + .antMatchers("/preLogin", "/login", "/register", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/", diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java index 6f8b9aa5c..72ef4864b 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -1,6 +1,8 @@ package com.ruoyi.framework.web.service; import javax.annotation.Resource; + +import org.apache.commons.codec.binary.Base64; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; @@ -25,6 +27,9 @@ import com.ruoyi.framework.manager.factory.AsyncFactory; import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysUserService; +import java.security.*; +import java.util.concurrent.TimeUnit; + /** * 登录校验方法 * @@ -131,4 +136,25 @@ public class SysLoginService sysUser.setLoginDate(DateUtils.getNowDate()); 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); + } } diff --git a/ruoyi-ui/src/api/login.js b/ruoyi-ui/src/api/login.js index 26742e79c..7e3feff84 100644 --- a/ruoyi-ui/src/api/login.js +++ b/ruoyi-ui/src/api/login.js @@ -1,59 +1,75 @@ -import request from '@/utils/request' - -// 登录方法 -export function login(username, password, code, uuid) { - const data = { - username, - password, - code, - uuid - } - return request({ - url: '/login', - headers: { - isToken: false - }, - method: 'post', - data: data - }) -} - -// 注册方法 -export function register(data) { - return request({ - url: '/register', - headers: { - isToken: false - }, - method: 'post', - data: data - }) -} - -// 获取用户详细信息 -export function getInfo() { - return request({ - url: '/getInfo', - method: 'get' - }) -} - -// 退出方法 -export function logout() { - return request({ - url: '/logout', - method: 'post' - }) -} - -// 获取验证码 -export function getCodeImg() { - return request({ - url: '/captchaImage', - headers: { - isToken: false - }, - method: 'get', - timeout: 20000 - }) -} \ No newline at end of file +import request from '@/utils/request' + +// 预登陆,获取RSA密钥 +export function preLogin() { + return new Promise(resolve => { + request({ + url: '/preLogin', + headers: { + isToken: false + }, + method: 'get' + }).then(res =>{ + resolve(res.data) + }) + }) +} + +// 登录方法 +export function login(username, password, code, uuid, publicKey) { + const data = { + username, + password, + code, + uuid, + publicKey + } + return request({ + url: '/login', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 注册方法 +export function register(data) { + return request({ + url: '/register', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 获取用户详细信息 +export function getInfo() { + return request({ + url: '/getInfo', + method: 'get' + }) +} + +// 退出方法 +export function logout() { + return request({ + url: '/logout', + method: 'post' + }) +} + +// 获取验证码 +export function getCodeImg() { + return request({ + url: '/captchaImage', + headers: { + isToken: false + }, + method: 'get', + timeout: 20000 + }) +} diff --git a/ruoyi-ui/src/store/modules/user.js b/ruoyi-ui/src/store/modules/user.js index fce1a86b8..2beaecf47 100644 --- a/ruoyi-ui/src/store/modules/user.js +++ b/ruoyi-ui/src/store/modules/user.js @@ -1,96 +1,97 @@ -import { login, logout, getInfo } from '@/api/login' -import { getToken, setToken, removeToken } from '@/utils/auth' - -const user = { - state: { - token: getToken(), - name: '', - avatar: '', - roles: [], - permissions: [] - }, - - mutations: { - SET_TOKEN: (state, token) => { - state.token = token - }, - SET_NAME: (state, name) => { - state.name = name - }, - SET_AVATAR: (state, avatar) => { - state.avatar = avatar - }, - SET_ROLES: (state, roles) => { - state.roles = roles - }, - SET_PERMISSIONS: (state, permissions) => { - state.permissions = permissions - } - }, - - actions: { - // 登录 - Login({ commit }, userInfo) { - const username = userInfo.username.trim() - const password = userInfo.password - const code = userInfo.code - const uuid = userInfo.uuid - return new Promise((resolve, reject) => { - login(username, password, code, uuid).then(res => { - setToken(res.token) - commit('SET_TOKEN', res.token) - resolve() - }).catch(error => { - reject(error) - }) - }) - }, - - // 获取用户信息 - GetInfo({ commit, state }) { - return new Promise((resolve, reject) => { - getInfo().then(res => { - const user = res.user - const avatar = user.avatar == "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar; - if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 - commit('SET_ROLES', res.roles) - commit('SET_PERMISSIONS', res.permissions) - } else { - commit('SET_ROLES', ['ROLE_DEFAULT']) - } - commit('SET_NAME', user.userName) - commit('SET_AVATAR', avatar) - resolve(res) - }).catch(error => { - reject(error) - }) - }) - }, - - // 退出系统 - LogOut({ commit, state }) { - return new Promise((resolve, reject) => { - logout(state.token).then(() => { - commit('SET_TOKEN', '') - commit('SET_ROLES', []) - commit('SET_PERMISSIONS', []) - removeToken() - resolve() - }).catch(error => { - reject(error) - }) - }) - }, - - // 前端 登出 - FedLogOut({ commit }) { - return new Promise(resolve => { - commit('SET_TOKEN', '') - removeToken() - resolve() - }) - } - } -} - -export default user +import { login, logout, getInfo } from '@/api/login' +import { getToken, setToken, removeToken } from '@/utils/auth' + +const user = { + state: { + token: getToken(), + name: '', + avatar: '', + roles: [], + permissions: [] + }, + + mutations: { + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar + }, + SET_ROLES: (state, roles) => { + state.roles = roles + }, + SET_PERMISSIONS: (state, permissions) => { + state.permissions = permissions + } + }, + + actions: { + // 登录 + Login({ commit }, userInfo) { + const username = userInfo.username.trim() + const password = userInfo.password + const code = userInfo.code + const uuid = userInfo.uuid + const publicKey = userInfo.publicKey + return new Promise((resolve, reject) => { + login(username, password, code, uuid, publicKey).then(res => { + setToken(res.token) + commit('SET_TOKEN', res.token) + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 获取用户信息 + GetInfo({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo().then(res => { + const user = res.user + const avatar = user.avatar == "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar; + if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 + commit('SET_ROLES', res.roles) + commit('SET_PERMISSIONS', res.permissions) + } else { + commit('SET_ROLES', ['ROLE_DEFAULT']) + } + commit('SET_NAME', user.userName) + commit('SET_AVATAR', avatar) + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + + // 退出系统 + LogOut({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token).then(() => { + commit('SET_TOKEN', '') + commit('SET_ROLES', []) + commit('SET_PERMISSIONS', []) + removeToken() + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 前端 登出 + FedLogOut({ commit }) { + return new Promise(resolve => { + commit('SET_TOKEN', '') + removeToken() + resolve() + }) + } + } +} + +export default user diff --git a/ruoyi-ui/src/utils/jsencrypt.js b/ruoyi-ui/src/utils/jsencrypt.js index 78d95234a..3a81013a2 100644 --- a/ruoyi-ui/src/utils/jsencrypt.js +++ b/ruoyi-ui/src/utils/jsencrypt.js @@ -28,3 +28,15 @@ export function decrypt(txt) { return encryptor.decrypt(txt) // 对数据进行解密 } +/** + * 对登录的用户名密码加密 + * @param loginPublicKey + * @param txt 待加密文本 + * @returns {Promise} + */ +export function encryptLogin(loginPublicKey, txt) { + const encrypt = new JSEncrypt(); + encrypt.setPublicKey(loginPublicKey); + return encrypt.encrypt(txt); +} + diff --git a/ruoyi-ui/src/views/login.vue b/ruoyi-ui/src/views/login.vue index 6e240dd24..8ab43d74c 100644 --- a/ruoyi-ui/src/views/login.vue +++ b/ruoyi-ui/src/views/login.vue @@ -1,219 +1,229 @@ - - - - - + + + + +