1.引入pom.xml
<!--security安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--springboot_redis缓存框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.RedisConfig
@Configuration
public class RedisConfig {
/**
* *redis序列化的工具定置类,下面这个请一定开启配置
* *127.0.0.1:6379> keys *
* *1) “ord:102” 序列化过
* *2)“\xaclxedlxeelx05tixeelaord:102” 野生,没有序列化过
* *this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
* *this.redisTemplate.opsForList();// 提供了操作List类型的所有方法
* *this.redisTemplate.opsForset(); //提供了操作set类型的所有方法
* *this.redisTemplate.opsForHash(); //提供了操作hash类型的所有方认
* *this.redisTemplate.opsForZSet(); //提供了操作zset类型的所有方法
* param LettuceConnectionFactory
* return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
// 设置key序列化方式string
redisTemplate.setKeySerializer(RedisSerializer.string()); // RedisSerializer.string() 等价于 new StringRedisSerializer()
// 设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
redisTemplate.setValueSerializer(RedisSerializer.json()); // RedisSerializer.json() 等价于 new GenericJackson2JsonRedisSerializer()
// 设置hash的key的序列化方式
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化方式
redisTemplate.setHashValueSerializer(RedisSerializer.json());
// 使配置生效
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
3.SecurityConfiguration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfiguration {
/**
* 接口文档放行
*/
public static final List<String> DOC_WHITE_LIST = List.of("/doc.html", "/webjars/**", "/v3/api-docs/**");
@Autowired
private JWTAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用basic明文验证
.httpBasic(AbstractHttpConfigurer::disable)
// 前后端分离架构不需要csrf保护
.csrf(AbstractHttpConfigurer::disable)
// 禁用默认登录页
.formLogin(AbstractHttpConfigurer::disable)
// 禁用默认登出页
.logout(AbstractHttpConfigurer::disable)
// 前后端分离是无状态的,不需要session了,直接禁用。
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((authorize) -> authorize
///静态资源,可匿名访问
.requestMatchers(HttpMethod.GET, DOC_WHITE_LIST.toArray(new String[0])).permitAll()
.anyRequest().authenticated())
// 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler));
return http.build();
}
}
4.JWTAuthenticationFilter
@Component
@Log4j2
public class JWTAuthenticationFilter extends OncePerRequestFilter {
private static final String TOKEN_ILLEGAL = "40001";
private static final String TOKEN_KICK = "40002";
private static final String TOKEN_INVALID = "40003";
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取当前请求的uri
String uri = request.getRequestURI();
log.info("请求路径:" + uri);
// 判断是否是认证请求路径
// 是:直接放行
if (uri.contains("/doc.html") || uri.contains("/webjars") || uri.contains("/v3/api-docs")) {
filterChain.doFilter(request, response);
return;
}
// 否:获取请求头中携带的token
String authorization = request.getHeader("Authorization");
log.info("携带authorization:" + authorization);
// 判断是否携带token
// 否:抛出异常
if (StringUtils.isBlank(authorization)) {
request.setAttribute(CacheConstants.TOKEN_EXCEPTION, "请登录后再操作");
request.setAttribute(CacheConstants.TOKEN_EXCEPTION_CODE, TOKEN_ILLEGAL);
throw new RuntimeException("无效token");
}
String token = authorization.replace("Bearer ", "");
if (StringUtils.isBlank(token)) {
request.setAttribute(CacheConstants.TOKEN_EXCEPTION, "请登录后再操作");
request.setAttribute(CacheConstants.TOKEN_EXCEPTION_CODE, TOKEN_ILLEGAL);
throw new RuntimeException("无效token");
}
try {
// 查看token是否有效
String userIdByToken = tokenService.getUserIdByToken(token);
if (StringUtils.isBlank(userIdByToken)) {
request.setAttribute(CacheConstants.TOKEN_EXCEPTION, "无效token");
request.setAttribute(CacheConstants.TOKEN_EXCEPTION_CODE, TOKEN_ILLEGAL);
throw new RuntimeException("无效token");
}
// 根据userId获取用户信息
LoginUser loginUser = tokenService.getUserByUserId(userIdByToken);
if (null == loginUser) {
request.setAttribute(CacheConstants.TOKEN_EXCEPTION, "登录已过期");
request.setAttribute(CacheConstants.TOKEN_EXCEPTION_CODE, TOKEN_INVALID);
throw new RuntimeException("登录已过期");
}
// 放行
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (RuntimeException e) {
log.error("用户信息获取失败token{}======== ", token);
log.error("用户信息获取失败exception{}============", e);
throw e;
} catch (Exception e) {
log.error("用户信息获取失败token{}========", token);
log.error("用户信息获取失败exception{}============", e);
request.setAttribute(CacheConstants.TOKEN_EXCEPTION, "系统错误,请联系管理员");
request.setAttribute(CacheConstants.TOKEN_EXCEPTION_CODE, HttpStatus.INTERNAL_SERVER_ERROR);
throw new RuntimeException("系统错误,请联系管理员");
}
}
}
5.AccessDeniedHandlerImpl 自定义认证异常信息
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseUtils.buildResponse(response, ErrorCode.ACCESS_ERROR.getMessage(), ErrorCode.ACCESS_ERROR.getCode());
}
}
6.AccessDeniedHandlerImpl 自定义授权异常信息(因AuthenticationException获取不了RuntimeException错误信息,所以只能用request传递,本人技术有限,大家有什么方案可以推荐)
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 获取错误信息
String exception = (String) request.getAttribute(CacheConstants.TOKEN_EXCEPTION);
// 获取错误状态码
String exceptionCode = request.getAttribute(CacheConstants.TOKEN_EXCEPTION_CODE).toString();
ResponseUtils.buildResponse(response, exception, exceptionCode);
}
}
ResponseUtils
public class ResponseUtils {
private static final String CONTENT_TYPE = "application/json;charset=utf-8";
public static void buildResponse(HttpServletResponse response, String msg, String httpStatus) throws IOException {
response.setContentType(CONTENT_TYPE); // 返回JSON
response.setStatus(HttpStatus.OK); // 状态码
ErrorVO errorVO = new ErrorVO(httpStatus, msg);
response.getWriter().write(JSONObject.toJSONString(errorVO));
}
}
RestControllerAdviceHandler(因捕获不了AccessDeniedException异常 只能定义一个全局捕获异常,大家有什么好的方法求推荐)
@ControllerAdvice
@Log4j2
public class RestControllerAdviceHandler {
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public ResponseDto handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String errorCode = ErrorCode.ACCESS_ERROR.getCode();
String message = ErrorCode.ACCESS_ERROR.getMessage();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return ResponseDto.failure(errorCode, message);
}
}
7.自定义权限验证@PreAuthorize(“@ss.hasPermi(‘works:enter’)”)
@Service("ss")
public class PermissionService {
/**
* 所有权限标识
*/
private static final String ALL_PERMISSION = "*:*:*";
/**
* 管理员角色权限标识
*/
private static final String SUPER_ADMIN = "admin";
private static final String ROLE_DELIMETER = ",";
private static final String PERMISSION_DELIMETER = ",";
@Autowired
private TokenService tokenService;
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
}
LoginUser loginUser = tokenService.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
PermissionContextHolder.setContext(permission);
return hasPermissions(loginUser.getPermissions(), permission);
}
/**
* 验证用户是否不具备某权限,与 hasPermi逻辑相反
*
* @param permission 权限字符串
* @return 用户是否不具备某权限
*/
public boolean lacksPermi(String permission) {
return hasPermi(permission) != true;
}
/**
* 验证用户是否具有以下任意一个权限
*
* @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
* @return 用户是否具有以下任意一个权限
*/
public boolean hasAnyPermi(String permissions) {
if (StringUtils.isEmpty(permissions)) {
return false;
}
LoginUser loginUser = tokenService.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
PermissionContextHolder.setContext(permissions);
Set<String> authorities = loginUser.getPermissions();
for (String permission : permissions.split(PERMISSION_DELIMETER)) {
if (permission != null && hasPermissions(authorities, permission)) {
return true;
}
}
return false;
}
/**
* 判断用户是否拥有某个角色
*
* @param roleStr 角色字符串
* @return 用户是否具备某角色
*/
public boolean hasRole(String roleStr) {
if (StringUtils.isEmpty(roleStr)) {
return false;
}
LoginUser loginUser = tokenService.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {
return false;
}
for (Role role : loginUser.getUser().getRoles()) {
String roleKey = role.getRoleKey();
if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(roleStr))) {
return true;
}
}
return false;
}
/**
* 验证用户是否不具备某角色,与 isRole逻辑相反。
*
* @param role 角色名称
* @return 用户是否不具备某角色
*/
public boolean lacksRole(String role) {
return hasRole(role) != true;
}
/**
* 验证用户是否具有以下任意一个角色
*
* @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
* @return 用户是否具有以下任意一个角色
*/
public boolean hasAnyRoles(String roles) {
if (StringUtils.isEmpty(roles)) {
return false;
}
LoginUser loginUser = tokenService.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {
return false;
}
for (String role : roles.split(ROLE_DELIMETER)) {
if (hasRole(role)) {
return true;
}
}
return false;
}
/**
* 判断是否包含权限
*
* @param permissions 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
private boolean hasPermissions(Set<String> permissions, String permission) {
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
}
LoginUser
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.liquidnet.service.user.domain.model;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.liquidnet.service.user.entity.User;
import java.util.Collection;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
private Long userId;
private Long deptId;
private String token;
private Long loginTime;
private Long expireTime;
private String ipaddr;
private String loginLocation;
private String browser;
private String os;
private Set<String> permissions;
private Set<String> roles;
private User user;
public Long getUserId() {
return this.userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getDeptId() {
return this.deptId;
}
public void setDeptId(Long deptId) {
this.deptId = deptId;
}
public String getToken() {
return this.token;
}
public void setToken(String token) {
this.token = token;
}
public LoginUser() {
}
public LoginUser(User user, Set<String> permissions) {
this.user = user;
this.permissions = permissions;
}
public LoginUser(Long userId, Long deptId, User user, Set<String> permissions, Set<String> roles) {
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
this.roles = roles;
}
@JSONField(
serialize = false
)
@JsonIgnore
public String getPassword() {
return this.user.getPassword();
}
@JsonIgnore
public String getUsername() {
return this.user.getUserName();
}
@JSONField(
serialize = false
)
@JsonIgnore
public boolean isAccountNonExpired() {
return true;
}
@JSONField(
serialize = false
)
@JsonIgnore
public boolean isAccountNonLocked() {
return true;
}
@JSONField(
serialize = false
)
@JsonIgnore
public boolean isCredentialsNonExpired() {
return true;
}
@JSONField(
serialize = false
)
@JsonIgnore
public boolean isEnabled() {
return true;
}
public Long getLoginTime() {
return this.loginTime;
}
public void setLoginTime(Long loginTime) {
this.loginTime = loginTime;
}
public String getIpaddr() {
return this.ipaddr;
}
public void setIpaddr(String ipaddr) {
this.ipaddr = ipaddr;
}
public String getLoginLocation() {
return this.loginLocation;
}
public void setLoginLocation(String loginLocation) {
this.loginLocation = loginLocation;
}
public String getBrowser() {
return this.browser;
}
public void setBrowser(String browser) {
this.browser = browser;
}
public String getOs() {
return this.os;
}
public void setOs(String os) {
this.os = os;
}
public Long getExpireTime() {
return this.expireTime;
}
public void setExpireTime(Long expireTime) {
this.expireTime = expireTime;
}
public Set<String> getPermissions() {
return this.permissions;
}
public Set<String> getRoles() {
return this.roles;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
public User getUser() {
return this.user;
}
public void setUser(User user) {
this.user = user;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
TokenService
package com.chunjuan.base.service.impl;
import com.chunjuan.base.common.constant.CacheConstants;
import com.chunjuan.base.service.interfaces.ITokenService;
import com.chunjuan.base.utils.RedisCache;
import com.chunjuan.base.utils.ServletUtils;
import com.chunjuan.base.utils.StringUtils;
import com.liquidnet.service.user.domain.model.LoginUser;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class TokenService implements ITokenService {
@Autowired
protected HttpServletRequest request;
@Autowired
private RedisCache redisService;
@Override
public String getUserIdByToken(String token) {
Object obj = redisService.getCacheObject(getTokenKey(token));
if (obj != null) {
return (String) obj;
}
return null;
}
@Override
public LoginUser getUserByUserId(String userId) {
Object obj = redisService.getCacheObject(getUserKey(userId));
if (obj != null) {
return (LoginUser) obj;
}
return null;
}
@Override
public LoginUser getLoginUser() {
LoginUser loginUser = null;
String authorization = ServletUtils.getRequest().getHeader("Authorization");
if (StringUtils.isEmpty(authorization)) {
return null;
}
// 获取请求携带的令牌
String token = authorization.replace("Bearer ", "");
if (StringUtils.isEmpty(token)) {
return null;
}
try {
String userId = null;
String tokenKey = getTokenKey(token);
if (StringUtils.isNotEmpty(tokenKey)) {
userId = (String) redisService.getCacheObject(tokenKey);
// 解析对应的权限以及用户信息
String userKey = getUserKey(userId);
loginUser = (LoginUser) redisService.getCacheObject(userKey);
}
return loginUser;
} catch (Exception e) {
log.error("从缓存获取用户信息异常:{}", e.getMessage());
throw new RuntimeException("用户获取失败,请重新登录!");
}
}
private String getTokenKey(String token) {
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
private String getUserKey(String userId) {
return CacheConstants.LOGIN_USER_KEY + userId;
}
}
RedisCache
package com.chunjuan.base.utils;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author zjp
* @create 2023-06-20 15:22
*/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
@RequiredArgsConstructor
public class RedisCache {
private final RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection) {
return redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 删除Hash中的数据
*
* @param key
* @param hkey
*/
public void delCacheMapValue(final String key, final String hkey) {
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
}
8.自定义操作日志注解 @Log(title = “测试相关接口”, businessType = BusinessType.UPDATE)
LogAspect 日志切面
package com.chunjuan.base.aspect;
import com.alibaba.fastjson.JSON;
import com.chunjuan.base.common.AsyncFactory;
import com.chunjuan.base.common.AsyncManager;
import com.chunjuan.base.common.annotation.Log;
import com.chunjuan.base.common.filter.PropertyPreExcludeFilter;
import com.chunjuan.base.entity.UserOperaLog;
import com.chunjuan.base.enums.BusinessStatus;
import com.chunjuan.base.service.impl.TokenService;
import com.chunjuan.base.utils.ServletUtils;
import com.chunjuan.base.utils.StringUtils;
import com.chunjuan.base.utils.ip.IpUtils;
import com.liquidnet.service.user.domain.model.LoginUser;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collection;
import java.util.Map;
import java.util.TimerTask;
/**
* 操作日志记录处理
*
* @author anjiabin
*/
@Aspect
@Component
public class LogAspect {
/**
* 排除敏感属性字段
*/
public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
@Autowired
private TokenService tokenService;
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try {
// 获取当前的用户
LoginUser loginUser = tokenService.getLoginUser();
// *========数据库日志=========*//
UserOperaLog operLog = new UserOperaLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
operLog.setOperaIp(ip);
operLog.setOperaUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
if (loginUser != null) {
operLog.setOperaName(loginUser.getUsername());
}
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
TimerTask timerTask = AsyncFactory.recordOper(operLog);
// 保存数据库
AsyncManager.me().execute(timerTask);
} catch (Exception exp) {
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, UserOperaLog operLog, Object jsonResult) throws Exception {
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog);
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, UserOperaLog operLog) throws Exception {
String requestMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs());
operLog.setOperaParam(StringUtils.substring(params, 0, 2000));
} else {
Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
operLog.setOperaParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter()), 0, 2000));
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (StringUtils.isNotNull(o) && !isFilterObject(o)) {
try {
String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter());
params += jsonObj.toString() + " ";
} catch (Exception e) {
}
}
}
}
return params.trim();
}
/**
* 忽略敏感属性
*/
public PropertyPreExcludeFilter excludePropertyPreFilter() {
return new PropertyPreExcludeFilter().addExcludes(EXCLUDE_PROPERTIES);
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
Log
package com.chunjuan.base.common.annotation;
import com.chunjuan.base.enums.BusinessType;
import com.chunjuan.base.enums.OperatorType;
import java.lang.annotation.*;
/**
* 自定义操作日志记录注解
*
* @author anjiabin
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
*/
public String title() default "";
/**
* 功能
*/
public BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
public OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
public boolean isSaveResponseData() default true;
}
AsyncFactory
public class AsyncFactory {
/**
* 操作日志记录
*
* @param operLog 操作日志信息
* @return 任务task
*/
public static TimerTask recordOper(final UserOperaLog operLog) {
return new TimerTask() {
@Override
public void run() {
// 远程查询操作地点
operLog.setOperaLocation(AddressUtils.getRealAddressByIP(operLog.getOperaIp()));
SpringUtils.getBean(IUserOperaLogService.class).save(operLog);
}
};
}
}
AsyncManager
package com.chunjuan.base.common;
import com.chunjuan.base.utils.Threads;
import com.chunjuan.base.utils.spring.SpringUtils;
import java.util.TimerTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 异步任务管理器
*
* @author anjiabin
*/
public class AsyncManager {
private static AsyncManager me = new AsyncManager();
/**
* 操作延迟10毫秒
*/
private final int OPERATE_DELAY_TIME = 10;
/**
* 异步操作任务调度线程池
*/
private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
/**
* 单例模式
*/
private AsyncManager() {
}
public static AsyncManager me() {
return me;
}
/**
* 执行任务
*
* @param task 任务
*/
public void execute(TimerTask task) {
executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
}
/**
* 停止任务线程池
*/
public void shutdown() {
Threads.shutdownAndAwaitTermination(executor);
}
}
ShutdownManager
package com.chunjuan.base.common;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 确保应用退出时能关闭后台线程
*
* @author anjiabin
*/
@Component
public class ShutdownManager {
private static final Logger logger = LoggerFactory.getLogger("sys-user");
@PreDestroy
public void destroy() {
shutdownAsyncManager();
}
/**
* 停止异步执行任务
*/
private void shutdownAsyncManager() {
try {
logger.info("====关闭后台任务任务线程池====");
AsyncManager.me().shutdown();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
ThreadPoolConfig
package com.chunjuan.base.config;
import com.chunjuan.base.utils.Threads;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置
*
* @author anjiabin
**/
@Configuration
public class ThreadPoolConfig {
// 核心线程池大小
private int corePoolSize = 50;
// 最大可创建的线程数
private int maxPoolSize = 200;
// 队列最大长度
private int queueCapacity = 1000;
// 线程池维护线程所允许的空闲时间
private int keepAliveSeconds = 300;
/**
* 执行周期性或定时任务
*/
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() {
return new ScheduledThreadPoolExecutor(corePoolSize,
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
new ThreadPoolExecutor.CallerRunsPolicy()) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
Threads.printException(r, t);
}
};
}
}