sprringboot3整合springsecurity6.0(只做token权限验证)+redis+自定义权限验证+自定义操作日志

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);
            }
        };
    }
}

好的,以下是完整的Spring Boot 2.7.2整合Spring Security + Redis RBAC权限控制的Java代码示例: 首先,您需要在您的Spring Boot项目中添加Spring SecurityRedis的依赖。 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 接下来,创建一个User实体类,该类表示系统中的用户。 ```java @Data @AllArgsConstructor @NoArgsConstructor public class User implements UserDetails { private static final long serialVersionUID = -8091879091924046844L; private String username; private String password; private List<String> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } ``` 然后,创建一个UserDetailsServiceImpl类,该类实现了UserDetailsService接口,用于从Redis中获取用户信息。 ```java @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = (User) redisTemplate.opsForHash().get("users", username); if (user == null) { throw new UsernameNotFoundException("User not found with username: " + username); } return user; } } ``` 接下来,创建一个RBACAuthorityService类,该类用于从Redis中获取角色和权限信息。 ```java @Service public class RBACAuthorityService { @Autowired private RedisTemplate<String, Object> redisTemplate; public List<String> getRolesByUserName(String username) { return (List<String>) redisTemplate.opsForHash().get("roles", username); } public List<String> getPermissionsByRole(String role) { return (List<String>) redisTemplate.opsForHash().get("permissions", role); } } ``` 然后,创建一个RBACPermissionEvaluator类,该类实现了PermissionEvaluator接口,用于判断用户是否有权限访问某个URL。 ```java @Component public class RBACPermissionEvaluator implements PermissionEvaluator { @Autowired private RBACAuthorityService rbacAuthorityService; @Override public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { if (auth == null || targetDomainObject == null || !(permission instanceof String)) { return false; } String username = auth.getName(); List<String> roles = rbacAuthorityService.getRolesByUserName(username); for (String role : roles) { List<String> permissions = rbacAuthorityService.getPermissionsByRole(role); if (permissions.contains(permission)) { return true; } } return false; } @Override public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) { return false; } } ``` 然后,您需要创建一个RedisTokenRepositoryImpl类,该类实现了PersistentTokenRepository接口,用于将Remember-Me令牌存储到Redis中。 ```java @Repository public class RedisTokenRepositoryImpl implements PersistentTokenRepository { private RedisTemplate<String, Object> redisTemplate; public RedisTokenRepositoryImpl(RedisConnectionFactory redisConnectionFactory) { this.redisTemplate = new RedisTemplate<>(); this.redisTemplate.setConnectionFactory(redisConnectionFactory); this.redisTemplate.setKeySerializer(new StringRedisSerializer()); this.redisTemplate.setHashKeySerializer(new StringRedisSerializer()); this.redisTemplate.setHashValueSerializer(new GenericToStringSerializer<>(Object.class)); this.redisTemplate.afterPropertiesSet(); } @Override public void createNewToken(PersistentRememberMeToken token) { redisTemplate.opsForHash().put("rememberMeTokens", token.getSeries(), token); } @Override public void updateToken(String series, String tokenValue, Date lastUsed) { PersistentRememberMeToken token = getTokenForSeries(series); if (token != null) { token.setTokenValue(tokenValue); token.setDate(lastUsed); redisTemplate.opsForHash().put("rememberMeTokens", series, token); } } @Override public PersistentRememberMeToken getTokenForSeries(String seriesId) { return (PersistentRememberMeToken) redisTemplate.opsForHash().get("rememberMeTokens", seriesId); } @Override public void removeUserTokens(String username) { HashOperations<String, String, PersistentRememberMeToken> ops = redisTemplate.opsForHash(); Map<String, PersistentRememberMeToken> tokens = ops.entries("rememberMeTokens"); for (PersistentRememberMeToken token : tokens.values()) { if (username.equals(token.getUsername())) { ops.delete("rememberMeTokens", token.getSeries()); } } } } ``` 接下来,创建一个SecurityConfig类,该类用于配置Spring Security。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private RBACPermissionEvaluator permissionEvaluator; @Autowired private RedisConnectionFactory redisConnectionFactory; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasAnyRole("ADMIN", "USER") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login").permitAll() .and() .logout().permitAll() .and() .rememberMe() .tokenRepository(redisTokenRepository()) .tokenValiditySeconds(86400) .userDetailsService(userDetailsService); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/images/**"); } @Bean public RedisTokenRepositoryImpl redisTokenRepository() { return new RedisTokenRepositoryImpl(redisConnectionFactory); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new RBACInterceptor(permissionEvaluator)); } } ``` 在上述代码中,我们使用了RBACPermissionEvaluator来判断用户是否有权限访问某个URL。我们还使用了RedisTokenRepositoryImpl来将Remember-Me令牌存储到Redis中。最后,我们使用了RBACInterceptor来拦截请求并进行权限验证。在addInterceptors方法中,我们将RBACInterceptor注册到Spring MVC拦截器链中。 最后,创建一个RBACInterceptor类,该类实现了HandlerInterceptor接口,用于拦截请求并进行权限验证。 ```java public class RBACInterceptor implements HandlerInterceptor { private RBACPermissionEvaluator permissionEvaluator; public RBACInterceptor(RBACPermissionEvaluator permissionEvaluator) { this.permissionEvaluator = permissionEvaluator; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String url = request.getRequestURI(); String method = request.getMethod(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { response.sendRedirect("/login"); return false; } boolean hasPermission = permissionEvaluator.hasPermission(authentication, url, method); if (!hasPermission) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } return true; } } ``` 上述代码中,我们使用RBACPermissionEvaluator来判断用户是否有权限访问某个URL。如果用户没有权限访问某个URL,我们将返回HTTP 403 Forbidden错误。 希望这些信息能对您有所帮助!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值