1
0
mirror of https://github.com/FatttSnake/Pinnacle-OA.git synced 2026-04-04 22:41:24 +08:00

Added login expiration reminder. Add logout reminder.

This commit is contained in:
2023-05-08 09:37:13 +08:00
parent 219f8cca3d
commit 881be1b0f9
15 changed files with 138 additions and 70 deletions

View File

@@ -92,6 +92,10 @@
<artifactId>java-jwt</artifactId> <artifactId>java-jwt</artifactId>
<version>4.3.0</version> <version>4.3.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -5,6 +5,7 @@ import com.cfive.pinnacle.entity.common.ResponseCode;
import com.cfive.pinnacle.entity.common.ResponseResult; import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.service.permission.ILoginService; import com.cfive.pinnacle.service.permission.ILoginService;
import com.cfive.pinnacle.utils.WebUtil; import com.cfive.pinnacle.utils.WebUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -28,8 +29,8 @@ public class LoginController {
} }
@RequestMapping("/logout") @RequestMapping("/logout")
public ResponseResult logout() { public ResponseResult logout(HttpServletRequest request) {
boolean result = loginService.logout(); boolean result = loginService.logout(request.getHeader("token"));
if (result) { if (result) {
return ResponseResult.build(ResponseCode.LOGOUT_SUCCESS, "Logout Success", null); return ResponseResult.build(ResponseCode.LOGOUT_SUCCESS, "Logout Success", null);
} else { } else {

View File

@@ -10,6 +10,8 @@ public class ResponseCode {
public static final int LOGIN_USERNAME_PASSWORD_ERROR = 20011; public static final int LOGIN_USERNAME_PASSWORD_ERROR = 20011;
public static final int LOGOUT_SUCCESS = 20015; public static final int LOGOUT_SUCCESS = 20015;
public static final int LOGOUT_FAILED = 20016; public static final int LOGOUT_FAILED = 20016;
public static final int TOKEN_IS_ILLEGAL = 20017;
public static final int TOKEN_HAS_EXPIRED = 20018;
public static final int DATABASE_SELECT_OK = 20021; public static final int DATABASE_SELECT_OK = 20021;
public static final int DATABASE_SAVE_OK = 20022; public static final int DATABASE_SAVE_OK = 20022;
public static final int DATABASE_UPDATE_OK = 20023; public static final int DATABASE_UPDATE_OK = 20023;

View File

@@ -1,10 +1,12 @@
package com.cfive.pinnacle.filter; package com.cfive.pinnacle.filter;
import com.auth0.jwt.interfaces.DecodedJWT; import com.cfive.pinnacle.entity.common.ResponseCode;
import com.cfive.pinnacle.entity.permission.LoginUser; import com.cfive.pinnacle.entity.permission.LoginUser;
import com.cfive.pinnacle.utils.JwtUtil; import com.cfive.pinnacle.utils.JwtUtil;
import com.cfive.pinnacle.utils.RedisCache; import com.cfive.pinnacle.utils.RedisCache;
import com.cfive.pinnacle.utils.WebUtil;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Nonnull;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -29,26 +31,29 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
} }
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token"); String token = request.getHeader("token");
if (!StringUtils.hasText(token)) { if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
String userId;
try { try {
DecodedJWT decodedJWT = JwtUtil.parseJWT(token); JwtUtil.parseJWT(token);
userId = decodedJWT.getSubject();
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Token is illegal"); String objectResponse = WebUtil.objectResponse(ResponseCode.TOKEN_IS_ILLEGAL, "Token is illegal", null);
WebUtil.renderString(response, objectResponse);
return;
} }
String redisKey = "login:" + userId; String redisKey = "login:" + token;
LoginUser loginUser = new ObjectMapper().convertValue(redisCache.getCacheObject(redisKey), LoginUser.class); LoginUser loginUser = new ObjectMapper().convertValue(redisCache.getCacheObject(redisKey), LoginUser.class);
if (Objects.isNull(loginUser)) { if (Objects.isNull(loginUser)) {
throw new RuntimeException("Not logged in"); String objectResponse = WebUtil.objectResponse(ResponseCode.TOKEN_HAS_EXPIRED, "Token has expired", null);
WebUtil.renderString(response, objectResponse);
return;
} }
// Todo 权限 // Todo 权限
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authenticationToken);

View File

@@ -18,7 +18,7 @@ public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
String objectResponse; String objectResponse;
if (authException instanceof BadCredentialsException) { if (authException instanceof BadCredentialsException) {
objectResponse = WebUtil.objectResponse(ResponseCode.LOGOUT_FAILED, authException.getMessage(), null); objectResponse = WebUtil.objectResponse(ResponseCode.LOGIN_USERNAME_PASSWORD_ERROR, authException.getMessage(), null);
} else if (authException instanceof InsufficientAuthenticationException) { } else if (authException instanceof InsufficientAuthenticationException) {
objectResponse = WebUtil.objectResponse(ResponseCode.UNAUTHORIZED, authException.getMessage(), null); objectResponse = WebUtil.objectResponse(ResponseCode.UNAUTHORIZED, authException.getMessage(), null);
} else { } else {

View File

@@ -7,5 +7,5 @@ import java.util.HashMap;
public interface ILoginService { public interface ILoginService {
HashMap<String, String> login(User user); HashMap<String, String> login(User user);
boolean logout(); boolean logout(String token);
} }

View File

@@ -9,11 +9,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashMap; import java.util.HashMap;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service @Service
public class LoginServiceImpl implements ILoginService { public class LoginServiceImpl implements ILoginService {
@@ -46,17 +46,13 @@ public class LoginServiceImpl implements ILoginService {
HashMap<String, String> hashMap = new HashMap<>(); HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("token", jwt); hashMap.put("token", jwt);
redisCache.setCacheObject("login:" + userId, loginUser); redisCache.setCacheObject("login:" + jwt, loginUser, 10, TimeUnit.MINUTES);
return hashMap; return hashMap;
} }
@Override @Override
public boolean logout() { public boolean logout(String token) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return redisCache.deleteObject("login:" + token);
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
return redisCache.deleteObject("login:" + userId);
} }
} }

View File

@@ -1,6 +1,5 @@
package com.cfive.pinnacle.utils; package com.cfive.pinnacle.utils;
import com.cfive.pinnacle.entity.User;
import com.cfive.pinnacle.entity.common.ResponseResult; import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.entity.permission.LoginUser; import com.cfive.pinnacle.entity.permission.LoginUser;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;

View File

@@ -10,6 +10,31 @@ const SIZE_ICON_MD = '24px'
const SIZE_ICON_LG = '32px' const SIZE_ICON_LG = '32px'
const SIZE_ICON_XL = '64px' const SIZE_ICON_XL = '64px'
// Response Code
const SYSTEM_OK = 20000
const LOGIN_SUCCESS = 20010
const LOGIN_USERNAME_PASSWORD_ERROR = 20011
const LOGOUT_SUCCESS = 20015
const LOGOUT_FAILED = 20016
const TOKEN_IS_ILLEGAL = 20017
const TOKEN_HAS_EXPIRED = 20018
const DATABASE_SELECT_OK = 20021
const DATABASE_SAVE_OK = 20022
const DATABASE_UPDATE_OK = 20023
const DATABASE_DELETE_OK = 20024
const DATABASE_SELECT_ERROR = 20031
const DATABASE_SAVE_ERROR = 20032
const DATABASE_UPDATE_ERROR = 20033
const DATABASE_DELETE_ERROR = 20034
const DATABASE_TIMEOUT_ERROR = 20035
const DATABASE_CONNECT_ERROR = 20036
const UNAUTHORIZED = 30010
const ACCESS_DENIED = 30030
const SYSTEM_ERROR = 50001
const SYSTEM_TIMEOUT = 50002
export { export {
PRODUCTION_NAME, PRODUCTION_NAME,
TOKEN_NAME, TOKEN_NAME,
@@ -21,5 +46,26 @@ export {
SIZE_ICON_SM, SIZE_ICON_SM,
SIZE_ICON_MD, SIZE_ICON_MD,
SIZE_ICON_LG, SIZE_ICON_LG,
SIZE_ICON_XL SIZE_ICON_XL,
SYSTEM_OK,
LOGIN_SUCCESS,
LOGIN_USERNAME_PASSWORD_ERROR,
LOGOUT_SUCCESS,
LOGOUT_FAILED,
TOKEN_IS_ILLEGAL,
TOKEN_HAS_EXPIRED,
DATABASE_SELECT_OK,
DATABASE_SAVE_OK,
DATABASE_UPDATE_OK,
DATABASE_DELETE_OK,
DATABASE_SELECT_ERROR,
DATABASE_SAVE_ERROR,
DATABASE_UPDATE_ERROR,
DATABASE_DELETE_ERROR,
DATABASE_TIMEOUT_ERROR,
DATABASE_CONNECT_ERROR,
UNAUTHORIZED,
ACCESS_DENIED,
SYSTEM_ERROR,
SYSTEM_TIMEOUT
} }

View File

@@ -5,6 +5,8 @@ import router from '@/router'
import '@/assets/css/base.css' import '@/assets/css/base.css'
import '@/assets/css/common.css' import '@/assets/css/common.css'
import 'element-plus/theme-chalk/el-message.css'
const app = createApp(App) const app = createApp(App)
app.use(router).mount('#app') app.use(router).mount('#app')

View File

@@ -80,8 +80,8 @@
import { getCaptchaSrc, login, verifyCaptcha } from '@/utils/auth' import { getCaptchaSrc, login, verifyCaptcha } from '@/utils/auth'
import backShape from '@/assets/svg/back-shape.svg' import backShape from '@/assets/svg/back-shape.svg'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css' import { LOGIN_SUCCESS, PRODUCTION_NAME } from '@/constants/Common.constants'
import { PRODUCTION_NAME } from '@/constants/Common.constants' import { setToken } from '@/utils/common'
export default { export default {
name: 'LoginPage', name: 'LoginPage',
@@ -102,7 +102,8 @@ export default {
getNewCaptcha() { getNewCaptcha() {
this.captchaSrc = getCaptchaSrc() this.captchaSrc = getCaptchaSrc()
}, },
async login() { login() {
const _this = this
if (!this.userName) { if (!this.userName) {
ElMessage.error({ ElMessage.error({
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
@@ -131,22 +132,25 @@ export default {
}) })
return return
} }
if (await login(this.userName, this.password)) { login(this.userName, this.password).then((res) => {
ElMessage.success({ const data = res.data
dangerouslyUseHTMLString: true, if (data.code === LOGIN_SUCCESS) {
message: '<strong>登录成功</strong>' setToken(data.data.token)
}) ElMessage.success({
this.loggingIn = true dangerouslyUseHTMLString: true,
const _this = this message: '<strong>登录成功</strong>'
setTimeout(function () { })
_this.$router.push('/') this.loggingIn = true
}, 1500) setTimeout(function () {
} else { _this.$router.push('/')
ElMessage.error({ }, 1500)
dangerouslyUseHTMLString: true, } else {
message: '<strong>用户名</strong> 或 <strong>密码</strong> 错误' ElMessage.error({
}) dangerouslyUseHTMLString: true,
} message: '<strong>用户名</strong> 或 <strong>密码</strong> 错误'
})
}
})
} }
} }
} }

View File

@@ -156,6 +156,7 @@ import {
} from '@/constants/Common.constants.js' } from '@/constants/Common.constants.js'
import _ from 'lodash' import _ from 'lodash'
import { getUsername, logout } from '@/utils/auth' import { getUsername, logout } from '@/utils/auth'
import { ElMessage } from 'element-plus'
export default { export default {
name: 'MainFrame', name: 'MainFrame',
@@ -187,7 +188,13 @@ export default {
}, },
logout() { logout() {
logout() logout()
this.$router.push({ name: 'Login' }) ElMessage.success({
dangerouslyUseHTMLString: true,
message: '<strong>退出登录</strong>'
})
setTimeout(() => {
this.$router.push({ name: 'Login' })
}, 1500)
} }
}, },
mounted() { mounted() {

View File

@@ -1,6 +1,8 @@
import axios, { type AxiosError } from 'axios' import axios, { type AxiosError } from 'axios'
import { getToken, removeToken } from '@/utils/common' import { clearLocalStorage, getToken } from '@/utils/common'
import router from '@/router' import router from '@/router'
import { TOKEN_HAS_EXPIRED, TOKEN_IS_ILLEGAL, UNAUTHORIZED } from '@/constants/Common.constants'
import { ElMessage } from 'element-plus'
const service = axios.create({ const service = axios.create({
baseURL: 'http://localhost:8621', baseURL: 'http://localhost:8621',
@@ -23,17 +25,26 @@ service.interceptors.request.use(
service.interceptors.response.use( service.interceptors.response.use(
(response) => { (response) => {
switch (response.data.code) {
case UNAUTHORIZED:
case TOKEN_IS_ILLEGAL:
case TOKEN_HAS_EXPIRED:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.log(`request error: ${response.data.code} - ${response.data.msg}`)
clearLocalStorage()
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>登录已过期</strong>'
})
setTimeout(function () {
void router.push({ name: 'Login' })
}, 1500)
}
return response return response
}, },
async (error) => { async (error) => {
if (error.response != null) { if (error.response != null) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions /* empty */
console.log(`request error: ${error.response.code} - ${error.response.msg}`)
switch (error.response.code) {
case 30010:
removeToken()
await router.push({ name: 'Login' })
}
} }
return await Promise.reject(error?.response?.data) return await Promise.reject(error?.response?.data)
} }

View File

@@ -1,33 +1,19 @@
import type { Captcha } from './common' import type { Captcha } from './common'
import { import { clearLocalStorage, getCaptcha, getLocalStorage, setLocalStorage } from './common'
getCaptcha,
getLocalStorage,
getToken,
removeLocalStorage,
setLocalStorage,
setToken
} from './common'
import { TOKEN_NAME } from '@/constants/Common.constants' import { TOKEN_NAME } from '@/constants/Common.constants'
import _ from 'lodash' import _ from 'lodash'
import request from '@/services' import request from '@/services'
let captcha: Captcha let captcha: Captcha
async function login(username: string, passwd: string): Promise<boolean> { async function login<T = any>(username: string, passwd: string): Promise<T> {
removeLocalStorage('username') return await request.post('/login', { username, passwd })
await request.post('/login', { username, passwd }).then((res: any) => {
const response = res.data
if (response.code === 20010) {
setToken(response.data.token)
}
})
return !_.isEmpty(getToken())
} }
function logout(): void { function logout(): void {
removeLocalStorage(TOKEN_NAME) void request.get('/logout').finally(() => {
removeLocalStorage('username') clearLocalStorage()
})
} }
function getLoginStatus(): boolean { function getLoginStatus(): boolean {

View File

@@ -77,6 +77,10 @@ function removeToken(): void {
removeLocalStorage(TOKEN_NAME) removeLocalStorage(TOKEN_NAME)
} }
function clearLocalStorage(): void {
localStorage.clear()
}
function randomInt(start: number, end: number): number { function randomInt(start: number, end: number): number {
if (start > end) { if (start > end) {
const t = start const t = start
@@ -149,5 +153,6 @@ export {
removeCookie, removeCookie,
removeLocalStorage, removeLocalStorage,
removeToken, removeToken,
clearLocalStorage,
getCaptcha getCaptcha
} }