Spring Authorization Server优化篇:添加redis缓存支持和统一响应类

2023年 7月 12日 48.4k 0

前言

今天为大家展示一下如何使用Spring data redis来缓存项目中数据,在项目使用人数少的情况下使用HttpSession问题不大,但是当并发多了就顶不住了,基本都会选择一些NoSQL来做缓存,本人就选择了比较常用的redis来做缓存;关于统一响应类这个东西就是为了规范项目的响应值,方便前端对接接口,其它人对接接口时更轻松。

添加统一响应类

在model包下添加Result.java类

package com.example.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;

import java.io.Serializable;

/**
 * 公共响应类
 *
 * @author vains
 * @date 2021/3/10 14:10
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result implements Serializable {

    /**
     * 响应状态码
     */
    private Integer code;

    /**
     * 响应信息
     */
    private String message;

    /**
     * 接口是否处理成功
     */
    private Boolean success;

    /**
     * 接口响应时携带的数据
     */
    private T data;

    /**
     * 操作成功携带数据
     * @param data 数据
     * @param  类型
     * @return 返回统一响应
     */
    public static  Result success(T data) {
        return new Result(HttpStatus.OK.value(), ("操作成功."),Boolean.TRUE, data);
    }

    /**
     * 操作成功不带数据
     * @return 返回统一响应
     */
    public static Result success() {
        return new Result(HttpStatus.OK.value(), ("操作成功."), Boolean.TRUE, (null));
    }

    /**
     * 操作成功携带数据
     * @param message 成功提示消息
     * @param data 成功携带数据
     * @param  类型
     * @return 返回统一响应
     */
    public static  Result success(String message, T data) {
        return new Result(HttpStatus.OK.value(), message, Boolean.TRUE, data);
    }

    /**
     * 操作失败返回
     * @param message 成功提示消息
     * @param  类型
     * @return 返回统一响应
     */
    public static  Result error(String message) {
        return new Result(HttpStatus.INTERNAL_SERVER_ERROR.value(), message, Boolean.FALSE, (null));
    }

    /**
     * 操作失败返回
     * @param code 错误码
     * @param message 成功提示消息
     * @param  类型
     * @return 返回统一响应
     */
    public static  Result error(Integer code, String message) {
        return new Result(code, message, Boolean.FALSE, (null));
    }

    /**
     * oauth2 问题
     * @param message 失败提示消息
     * @param data 具体的错误信息
     * @param  类型
     * @return 返回统一响应
     */
    public static  Result oauth2Error(Integer code, String message, T data) {
        return new Result(code, message, Boolean.FALSE, data);
    }

    /**
     * oauth2 问题
     * @param message 失败提示消息
     * @param data 具体的错误信息
     * @param  类型
     * @return 返回统一响应
     */
    public static  Result oauth2Error(String message, T data) {
        return new Result(HttpStatus.UNAUTHORIZED.value(), message, Boolean.FALSE, data);
    }

}

优化项目

在controller包下添加LoginController

该类中的接口是原AuthorizationController接口中的,现在挪到该类中,并使用Redis来替换HttpSession存储验证码信息,编写一个CaptchaResult来将redis中的key返回给前端,前端登录时携带这个key来获取缓存数据,CaptchaResult类在后边

package com.example.controller;

import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.ShearCaptcha;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.example.model.Result;
import com.example.model.response.CaptchaResult;
import com.example.support.RedisOperator;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import static com.example.constant.RedisConstants.*;

/**
 * 登录接口,登录使用的接口
 *
 * @author vains
 */
@RestController
@RequiredArgsConstructor
public class LoginController {

    private final RedisOperator redisOperator;

    @GetMapping("/getSmsCaptcha")
    public Result getSmsCaptcha(String phone) {
        // 示例项目,固定1234
        String smsCaptcha = "1234";
        // 存入缓存中,5分钟后过期
        redisOperator.set((SMS_CAPTCHA_PREFIX_KEY + phone), smsCaptcha, CAPTCHA_TIMEOUT_SECONDS);
        return Result.success("获取短信验证码成功.", smsCaptcha);
    }

    @GetMapping("/getCaptcha")
    public Result getCaptcha() {
        // 使用huTool-captcha生成图形验证码
        // 定义图形验证码的长、宽、验证码字符数、干扰线宽度
        ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(150, 40, 4, 2);
        // 生成一个唯一id
        long id = IdWorker.getId();
        // 存入缓存中,5分钟后过期
        redisOperator.set((IMAGE_CAPTCHA_PREFIX_KEY + id), captcha.getCode(), CAPTCHA_TIMEOUT_SECONDS);
        return Result.success("获取验证码成功.", new CaptchaResult(String.valueOf(id), captcha.getCode(), captcha.getImageBase64Data()));
    }

}

CaptchaResult

生成的key是long,类使用String是因为前端对于long类型的大数据会有精度丢失问题,所以使用String

package com.example.model.response;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 获取验证码返回
 *
 * @author vains
 */
@Data
@AllArgsConstructor
public class CaptchaResult {

    /**
     * 验证码id
     */
    private String captchaId;

    /**
     * 验证码的值
     */
    private String code;

    /**
     * 图片验证码的base64值
     */
    private String imageData;

}

优化登录页面

页面修改不大,这里只放关键代码,如果有需要的可以去gitee查看完整代码:代码地址
页面登录表单添加隐藏域,存储redis存储验证码的key


获取验证码并设置值的地方修改

function getVerifyCode() {
    let requestOptions = {
        method: 'GET',
        redirect: 'follow'
    };

    fetch(`${window.location.origin}/getCaptcha`, requestOptions)
        .then(response => response.text())
        .then(r => {
            if (r) {
                let result = JSON.parse(r);
                document.getElementById('captchaId').value = result.data.captchaId
                document.getElementById('code-image').src = result.data.imageData
            }
        })
        .catch(error => console.log('error', error));
}

修改CaptchaAuthenticationProvider从redis中获取验证码

package com.example.authorization.captcha;

import com.example.constant.SecurityConstants;
import com.example.exception.InvalidCaptchaException;
import com.example.support.RedisOperator;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Objects;

import static com.example.constant.RedisConstants.IMAGE_CAPTCHA_PREFIX_KEY;

/**
 * 验证码校验
 * 注入ioc中替换原先的DaoAuthenticationProvider
 * 在authenticate方法中添加校验验证码的逻辑
 * 最后调用父类的authenticate方法并返回
 *
 * @author vains
 */
@Slf4j
public class CaptchaAuthenticationProvider extends DaoAuthenticationProvider {

    private final RedisOperator redisOperator;

    /**
     * 利用构造方法在通过{@link Component}注解初始化时
     * 注入UserDetailsService和passwordEncoder,然后
     * 设置调用父类关于这两个属性的set方法设置进去
     *
     * @param userDetailsService 用户服务,给框架提供用户信息
     * @param passwordEncoder    密码解析器,用于加密和校验密码
     */
    public CaptchaAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, RedisOperator redisOperator) {
        this.redisOperator = redisOperator;
        super.setPasswordEncoder(passwordEncoder);
        super.setUserDetailsService(userDetailsService);
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        log.info("Authenticate captcha...");

        // 获取当前request
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            throw new InvalidCaptchaException("Failed to get the current request.");
        }
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();

        // 获取当前登录方式
        String loginType = request.getParameter(SecurityConstants.LOGIN_TYPE_NAME);
        if (!Objects.equals(loginType, SecurityConstants.PASSWORD_LOGIN_TYPE)) {
            // 只要不是密码登录都不需要校验图形验证码
            log.info("It isn't necessary captcha authenticate.");
            return super.authenticate(authentication);
        }

        // 获取参数中的验证码
        String code = request.getParameter(SecurityConstants.CAPTCHA_CODE_NAME);
        if (ObjectUtils.isEmpty(code)) {
            throw new InvalidCaptchaException("The captcha cannot be empty.");
        }

        String captchaId = request.getParameter(SecurityConstants.CAPTCHA_ID_NAME);
        // 获取缓存中存储的验证码
        String captchaCode = redisOperator.getAndDelete((IMAGE_CAPTCHA_PREFIX_KEY + captchaId));
        if (!ObjectUtils.isEmpty(captchaCode)) {
            if (!captchaCode.equalsIgnoreCase(code)) {
                throw new InvalidCaptchaException("The captcha is incorrect.");
            }
        } else {
            throw new InvalidCaptchaException("The captcha is abnormal. Obtain it again.");
        }

        log.info("Captcha authenticated.");
        return super.authenticate(authentication);
    }
}

修改SmsCaptchaLoginAuthenticationProvider从redis中获取短信验证码

package com.example.authorization.sms;

import com.example.authorization.captcha.CaptchaAuthenticationProvider;
import com.example.constant.SecurityConstants;
import com.example.exception.InvalidCaptchaException;
import com.example.support.RedisOperator;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Objects;

import static com.example.constant.RedisConstants.SMS_CAPTCHA_PREFIX_KEY;

/**
 * 短信验证码校验实现
 *
 * @author vains
 */
@Slf4j
@Component
public class SmsCaptchaLoginAuthenticationProvider extends CaptchaAuthenticationProvider {

    private final RedisOperator redisOperator;

    /**
     * 利用构造方法在通过{@link Component}注解初始化时
     * 注入UserDetailsService和passwordEncoder,然后
     * 设置调用父类关于这两个属性的set方法设置进去
     *
     * @param userDetailsService 用户服务,给框架提供用户信息
     * @param passwordEncoder    密码解析器,用于加密和校验密码
     */
    public SmsCaptchaLoginAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, RedisOperator redisOperator) {
        super(userDetailsService, passwordEncoder, redisOperator);
        this.redisOperator = redisOperator;
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        log.info("Authenticate sms captcha...");

        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException("The sms captcha cannot be empty.");
        }

        // 获取当前request
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            throw new InvalidCaptchaException("Failed to get the current request.");
        }
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();

        // 获取当前登录方式
        String loginType = request.getParameter(SecurityConstants.LOGIN_TYPE_NAME);
        // 获取grant_type
        String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
        // 短信登录和自定义短信认证grant type会走下方认证
        // 如果是自定义密码模式则下方的认证判断只要判断下loginType即可
        // if (Objects.equals(loginType, SecurityConstants.SMS_LOGIN_TYPE)) {}
        if (Objects.equals(loginType, SecurityConstants.SMS_LOGIN_TYPE)
                || Objects.equals(grantType, SecurityConstants.GRANT_TYPE_SMS_CODE)) {
            // 获取存入缓存中的验证码(UsernamePasswordAuthenticationToken的principal中现在存入的是手机号)
            String smsCaptcha = redisOperator.getAndDelete((SMS_CAPTCHA_PREFIX_KEY + authentication.getPrincipal()));
            // 校验输入的验证码是否正确(UsernamePasswordAuthenticationToken的credentials中现在存入的是输入的验证码)
            if (!Objects.equals(smsCaptcha, authentication.getCredentials())) {
                throw new BadCredentialsException("The sms captcha is incorrect.");
            }
            // 在这里也可以拓展其它登录方式,比如邮箱登录什么的
        } else {
            log.info("Not sms captcha loginType, exit.");
            // 其它调用父类默认实现的密码方式登录
            super.additionalAuthenticationChecks(userDetails, authentication);
        }

        log.info("Authenticated sms captcha.");
    }
}

修改Oauth2BasicUserauthorities属性,以防序列化失败

上一篇文章中最开始没有添加@JsonSerialize@JsonIgnoreProperties(ignoreUnknown = true)注解,导致授权确认时框架获取用户信息反序列化时失败,会导致JsonMixin异常,@JsonSerialize注解就是解决这个问题,另一个是忽略未知属性的。

package com.example.entity;

import com.baomidou.mybatisplus.annotation.*;

import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Collection;

import com.example.model.security.CustomGrantedAuthority;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * 

* 基础用户信息表 *

* * @author vains */ @Data @JsonSerialize @TableName("oauth2_basic_user") @JsonIgnoreProperties(ignoreUnknown = true) public class Oauth2BasicUser implements UserDetails, Serializable { @Serial private static final long serialVersionUID = 1L; /** * 自增id */ @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 用户名、昵称 */ private String name; /** * 账号 */ private String account; /** * 密码 */ private String password; /** * 手机号 */ private String mobile; /** * 邮箱 */ private String email; /** * 头像地址 */ private String avatarUrl; /** * 是否已删除 */ private Boolean deleted; /** * 用户来源 */ private String sourceFrom; /** * 创建时间 */ @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; /** * 修改时间 */ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; /** * 权限信息 * 非数据库字段 */ @TableField(exist = false) private Collection authorities; @Override public Collection getAuthorities() { return this.authorities; } @Override public String getUsername() { return this.account; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return !this.deleted; } }

CustomGrantedAuthority

该类中添加@JsonSerialize也是为了解决JsonMixin问题

package com.example.model.security;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;

/**
 * 自定义权限类
 *
 * @author vains
 */
@Data
@JsonSerialize
@NoArgsConstructor
@AllArgsConstructor
public class CustomGrantedAuthority implements GrantedAuthority {

    private String authority;

    @Override
    public String getAuthority() {
        return this.authority;
    }
}

Redis常量类RedisConstants

package com.example.constant;

/**
 * Redis相关常量
 *
 * @author vains
 */
public class RedisConstants {

    /**
     * 短信验证码前缀
     */
    public static final String SMS_CAPTCHA_PREFIX_KEY = "mobile_phone:";

    /**
     * 图形验证码前缀
     */
    public static final String IMAGE_CAPTCHA_PREFIX_KEY = "image_captcha:";

    /**
     * 验证码过期时间,默认五分钟
     */
    public static final long CAPTCHA_TIMEOUT_SECONDS = 60L * 5;

}

SecurityConstants添加常量

/**
 * 登录方式入参名
 */
public static final String LOGIN_TYPE_NAME = "loginType";

/**
 * 验证码id入参名
 */
public static final String CAPTCHA_ID_NAME = "captchaId";

/**
 * 验证码值入参名
 */
public static final String CAPTCHA_CODE_NAME = "code";

整合Spring data redis的步骤

  • 引入starter
  • 编写redis配置类(可选)
  • 编写redis操作类(可选)
  • 引入starer

    Spring boot项目中直接引入starter即可

    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    

    引入jackson-datatype-jsr310提供对Java8的特性与Java8时间相关序列化支持

    
        com.fasterxml.jackson.datatype
        jackson-datatype-jsr310
    
    

    编写Redis配置文件

    这个是可选的,但是如果不加这些序列化器会使用jdk的序列化器,导致使用redis客户端查看时与元数据有差异

    package com.example.config;
    
    import com.example.util.JsonUtils;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * Redis的key序列化配置类
     *
     * @author vains
     */
    @Configuration
    public class RedisConfig {
    
        /**
         * 默认情况下使用
         *
         * @param connectionFactory redis链接工厂
         * @return RedisTemplate
         */
        @Bean
        public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
            // 字符串序列化器
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            // 存入redis时序列化值的序列化器
            Jackson2JsonRedisSerializer valueSerializer =
                    new Jackson2JsonRedisSerializer(JsonUtils.MAPPER, Object.class);
    
            RedisTemplate redisTemplate = new RedisTemplate();
    
            // 设置值序列化器
            redisTemplate.setValueSerializer(valueSerializer);
            // 设置hash格式数据值的序列化器
            redisTemplate.setHashValueSerializer(valueSerializer);
            // 默认的Key序列化器为:JdkSerializationRedisSerializer
            redisTemplate.setKeySerializer(stringRedisSerializer);
            // 设置字符串序列化器
            redisTemplate.setStringSerializer(stringRedisSerializer);
            // 设置hash结构的key的序列化器
            redisTemplate.setHashKeySerializer(stringRedisSerializer);
    
            // 设置连接工厂
            redisTemplate.setConnectionFactory(connectionFactory);
    
            return redisTemplate;
        }
    
        /**
         * 操作hash的情况下使用
         *
         * @param connectionFactory redis链接工厂
         * @return RedisTemplate
         */
        @Bean
        public RedisTemplate redisHashTemplate(RedisConnectionFactory connectionFactory) {
    
            return redisTemplate(connectionFactory);
        }
    
    }
    

    编写redis操作类

    可选,框架提供了RedisTemplate来操作redis

    package com.example.support;
    
    import com.example.util.JsonUtils;
    import jakarta.annotation.Resource;
    import org.springframework.data.redis.core.HashOperations;
    import org.springframework.data.redis.core.ListOperations;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ValueOperations;
    import org.springframework.stereotype.Component;
    import org.springframework.util.ObjectUtils;
    
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    /**
     * Redis操作类
     *
     * @param  value的类型
     * @author vains
     */
    @Component
    public class RedisOperator {
    
        /**
         * 这里使用 @Resource 注解是因为在配置文件中注入ioc的泛型是,所以类型匹配不上,
         * resource是会先根据名字去匹配的,所以使用Resource注解可以成功注入
         */
        @Resource
        private RedisTemplate redisTemplate;
    
        @Resource
        private RedisTemplate redisHashTemplate;
    
        /**
         * 设置key的过期时间
         *
         * @param key     缓存key
         * @param timeout 存活时间
         * @param unit    时间单位
         */
        public void setExpire(String key, long timeout, TimeUnit unit) {
            redisHashTemplate.expire(key, timeout, unit);
        }
    
        /**
         * 根据key删除缓存
         *
         * @param keys 要删除的key,可变参数列表
         * @return 删除的缓存数量
         */
        public Long delete(String... keys) {
            if (ObjectUtils.isEmpty(keys)) {
                return 0L;
            }
            return redisTemplate.delete(Arrays.asList(keys));
        }
    
        /**
         * 存入值
         *
         * @param key   缓存中的key
         * @param value 存入的value
         */
        public void set(String key, V value) {
            valueOperations().set(key, value);
        }
    
        /**
         * 根据key取值
         *
         * @param key 缓存中的key
         * @return 返回键值对应缓存
         */
        public V get(String key) {
            return valueOperations().get(key);
        }
    
        /**
         * 设置键值并设置过期时间
         *
         * @param key     键
         * @param value   值
         * @param timeout 过期时间
         * @param unit    过期时间的单位
         */
        public void set(String key, V value, long timeout, TimeUnit unit) {
            valueOperations().set(key, value, timeout, unit);
        }
    
        /**
         * 设置键值并设置过期时间(单位秒)
         *
         * @param key     键
         * @param value   值
         * @param timeout 过期时间,单位:秒
         */
        public void set(String key, V value, long timeout) {
            this.set(key, value, timeout, TimeUnit.SECONDS);
        }
    
        /**
         * 根据key获取缓存并删除缓存
         *
         * @param key 要获取缓存的key
         * @return key对应的缓存
         */
        public V getAndDelete(String key) {
            if (ObjectUtils.isEmpty(key)) {
                return null;
            }
            V value = valueOperations().get(key);
            this.delete(key);
            return value;
        }
    
        /**
         * 往hash类型的数据中存值
         *
         * @param key   缓存中的key
         * @param field hash结构的key
         * @param value 存入的value
         */
        public void setHash(String key, String field, V value) {
            hashOperations().put(key, field, value);
        }
    
        /**
         * 根据key取值
         *
         * @param key 缓存中的key
         * @return 缓存key对应的hash数据中field属性的值
         */
        public Object getHash(String key, String field) {
            return hashOperations().hasKey(key, field) ? hashOperations().get(key, field) : null;
        }
    
        /**
         * 以hash格式存入redis
         *
         * @param key   缓存中的key
         * @param value 存入的对象
         */
        public void setHashAll(String key, Object value) {
            Map map = JsonUtils.objectCovertToObject(value, Map.class, String.class, Object.class);
            hashOperations().putAll(key, map);
        }
    
        /**
         * 设置键值并设置过期时间
         *
         * @param key     键
         * @param value   值
         * @param timeout 过期时间
         * @param unit    过期时间的单位
         */
        public void setHashAll(String key, Object value, long timeout, TimeUnit unit) {
            this.setHashAll(key, value);
            this.setExpire(key, timeout, unit);
        }
    
        /**
         * 设置键值并设置过期时间(单位秒)
         *
         * @param key     键
         * @param value   值
         * @param timeout 过期时间,单位:秒
         */
        public void setHashAll(String key, Object value, long timeout) {
            this.setHashAll(key, value, timeout, TimeUnit.SECONDS);
        }
    
        /**
         * 从redis中获取hash类型数据
         *
         * @param key 缓存中的key
         * @return redis 中hash数据
         */
        public Map getMapHashAll(String key) {
            return hashOperations().entries(key);
        }
    
        /**
         * 根据指定clazz类型从redis中获取对应的实例
         *
         * @param key   缓存key
         * @param clazz hash对应java类的class
         * @param    redis中hash对应的java类型
         * @return clazz实例
         */
        public  T getHashAll(String key, Class clazz) {
            Map entries = hashOperations().entries(key);
            if (ObjectUtils.isEmpty(entries)) {
                return null;
            }
            return JsonUtils.objectCovertToObject(entries, clazz);
        }
    
        /**
         * 根据key删除缓存
         *
         * @param key    要删除的key
         * @param fields key对应的hash数据的键值(HashKey),可变参数列表
         * @return hash删除的属性数量
         */
        public Long deleteHashField(String key, String... fields) {
            if (ObjectUtils.isEmpty(key) || ObjectUtils.isEmpty(fields)) {
                return 0L;
            }
            return hashOperations().delete(key, (Object[]) fields);
        }
    
        /**
         * 将value添加至key对应的列表中
         *
         * @param key   缓存key
         * @param value 值
         */
        public void listPush(String key, V value) {
            listOperations().rightPush(key, value);
        }
    
        /**
         * 将value添加至key对应的列表中,并添加过期时间
         *
         * @param key     缓存key
         * @param value   值
         * @param timeout key的存活时间
         * @param unit    时间单位
         */
        public void listPush(String key, V value, long timeout, TimeUnit unit) {
            listOperations().rightPush(key, value);
            this.setExpire(key, timeout, unit);
        }
    
        /**
         * 将value添加至key对应的列表中,并添加过期时间
         * 默认单位是秒(s)
         *
         * @param key     缓存key
         * @param value   值
         * @param timeout key的存活时间
         */
        public void listPush(String key, V value, long timeout) {
            this.listPush(key, value, timeout, TimeUnit.SECONDS);
        }
    
        /**
         * 将传入的参数列表添加至key的列表中
         *
         * @param key    缓存key
         * @param values 值列表
         * @return 存入数据的长度
         */
        public Long listPushAll(String key, Collection values) {
            return listOperations().rightPushAll(key, values);
        }
    
        /**
         * 将传入的参数列表添加至key的列表中,并设置key的存活时间
         *
         * @param key     缓存key
         * @param values  值列表
         * @param timeout key的存活时间
         * @param unit    时间单位
         * @return 存入数据的长度
         */
        public Long listPushAll(String key, Collection values, long timeout, TimeUnit unit) {
            Long count = listOperations().rightPushAll(key, values);
            this.setExpire(key, timeout, unit);
            return count;
        }
    
        /**
         * 将传入的参数列表添加至key的列表中,并设置key的存活时间
         *  默认单位是秒(s)
         *
         * @param key     缓存key
         * @param values  值列表
         * @param timeout key的存活时间
         * @return 存入数据的长度
         */
        public Long listPushAll(String key, Collection values, long timeout) {
            return this.listPushAll(key, values, timeout, TimeUnit.SECONDS);
        }
    
        /**
         * 根据key获取list列表
         *
         * @param key 缓存key
         * @return key对应的list列表
         */
        public Collection getList(String key) {
            Long size = listOperations().size(key);
            if (size == null || size == 0) {
                return null;
            }
            return listOperations().range(key, 0, (size - 1));
        }
    
        /**
         * value操作集
         *
         * @return ValueOperations
         */
        private ValueOperations valueOperations() {
            return redisTemplate.opsForValue();
        }
    
        /**
         * hash操作集
         *
         * @return ValueOperations
         */
        private HashOperations hashOperations() {
            return redisHashTemplate.opsForHash();
        }
    
        /**
         * hash操作集
         *
         * @return ValueOperations
         */
        private ListOperations listOperations() {
            return redisTemplate.opsForList();
        }
    
    }
    

    暂时只提供了string、list、hash的操作,其它的后续在加吧

    编写一个测试类测试这个工具类

    test目录下

    package com.example;
    
    import com.example.entity.Oauth2BasicUser;
    import com.example.support.RedisOperator;
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    /**
     * redis工具类测试
     *
     * @author vains
     */
    @Slf4j
    @SpringBootTest
    public class RedisOperatorTests {
    
        @Autowired
        private RedisOperator redisOperator;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Autowired
        private RedisOperator userRedisOperator;
    
        @Test
        @SneakyThrows
        void contextLoads() {
            // 默认key
            String defaultKey = "testKey";
            // 默认缓存值
            String defaultValue = "123456";
            // key的存活时间
            long timeout = 3;
            // 操作hash的属性声明
            String name = "name";
    
            // 清除key
            redisOperator.delete(defaultKey);
    
            // 获取用户信息
            Oauth2BasicUser userDetails = (Oauth2BasicUser) userDetailsService.loadUserByUsername("admin");
    
            redisOperator.set(defaultKey, defaultValue);
            log.info("根据key:{}存入值{}", defaultKey, defaultValue);
    
            String valueByKey = redisOperator.get(defaultKey);
            log.info("根据key:{}获取到值:{}", defaultKey, valueByKey);
    
            String valueByKeyAndDelete = redisOperator.getAndDelete(defaultKey);
            log.info("根据key:{}获取到值:{},删除key.", defaultKey, valueByKeyAndDelete);
    
            Long delete = redisOperator.delete(defaultKey);
            log.info("删除key:{},删除数量:{}.", defaultKey, delete);
    
            valueByKey = redisOperator.get(defaultKey);
            log.info("根据key:{}获取到值:{}", defaultKey, valueByKey);
    
            redisOperator.set(defaultKey, defaultValue, timeout);
            log.info("根据key:{}存入值{},存活时长为:{}", defaultKey, defaultValue, timeout);
            valueByKey = redisOperator.get(defaultKey);
            log.info("根据key:{}获取到值:{}", defaultKey, valueByKey);
    
            // 睡眠,让key失效
            TimeUnit.SECONDS.sleep((timeout + 1));
    
            // 重复获取
            valueByKey = redisOperator.get(defaultKey);
            log.info("线程睡眠后根据失效的key:{}获取到值:{}", defaultKey, valueByKey);
    
            redisOperator.setHashAll(defaultKey, userDetails, timeout);
            log.info("根据key:{}存入hash类型值{},存活时间:{}", defaultKey, userDetails, timeout);
    
            Oauth2BasicUser basicUser = redisOperator.getHashAll(defaultKey, Oauth2BasicUser.class);
            log.info("根据key:{}获取到hash类型值:{}", defaultKey, basicUser);
    
            // 睡眠,让key失效
            TimeUnit.SECONDS.sleep((timeout + 1));
            // 重复获取
            basicUser = redisOperator.getHashAll(defaultKey, Oauth2BasicUser.class);
            log.info("线程睡眠后根据失效的key:{}获取到hash类型值:{}", defaultKey, basicUser);
    
            redisOperator.setHashAll(defaultKey, userDetails, timeout);
            log.info("根据key:{}存入hash类型值{},存活时间:{}", defaultKey, userDetails, timeout);
    
            Map mapHashAll = redisOperator.getMapHashAll(defaultKey);
            log.info("根据key:{}获取到hash类型值:{}", defaultKey, mapHashAll);
    
            Object field = redisOperator.getHash(defaultKey, name);
            log.info("根据key:{}获取到hash类型属性:{}的值:{}", defaultKey, name, field);
    
            Long deleteHashField = redisOperator.deleteHashField(defaultKey, name);
            log.info("根据key:{}删除hash类型的{}属性,删除数量:{}", defaultKey, name, deleteHashField);
    
            // 重复获取验证删除
            field = redisOperator.getHash(defaultKey, name);
            log.info("根据key:{}获取到hash类型属性:{}的值:{}", defaultKey, name, field);
            basicUser = redisOperator.getHashAll(defaultKey, Oauth2BasicUser.class);
            log.info("根据key:{}获取到hash类型值:{}", defaultKey, basicUser);
    
            redisOperator.setHash(defaultKey, name, userDetails.getName());
            log.info("根据key:{}设置hash类型的{}属性,属性值为:{}", defaultKey, name, userDetails.getName());
    
            // 重复获取验证删除
            field = redisOperator.getHash(defaultKey, name);
            log.info("根据key:{}获取到hash类型属性:{}的值:{}", defaultKey, name, field);
            basicUser = redisOperator.getHashAll(defaultKey, Oauth2BasicUser.class);
            log.info("根据key:{}获取到hash类型值:{}", defaultKey, basicUser);
    
            // 清除key
            redisOperator.delete(defaultKey);
    
            userRedisOperator.listPush(defaultKey, userDetails);
            log.info("根据key:{}往list类型数据中添加数据:{}", defaultKey, userDetails);
    
            Collection users = userRedisOperator.getList(defaultKey);
            log.info("根据key:{}获取list数据:{}", defaultKey, users);
    
            Long listPushAll = userRedisOperator.listPushAll(defaultKey, List.of(userDetails));
            log.info("根据key:{}往list类型数据中添加数据:{},成功添加{}条数据", defaultKey, List.of(userDetails), listPushAll);
    
            users = userRedisOperator.getList(defaultKey);
            log.info("根据key:{}获取list数据:{}", defaultKey, users);
    
            userRedisOperator.listPush(defaultKey, userDetails, timeout);
            log.info("根据key:{}往list类型数据中添加数据:{},key的存活时间为:{}", defaultKey, userDetails, timeout);
            // 睡眠,让key失效
            TimeUnit.SECONDS.sleep((timeout + 1));
            // 重复获取
            users = userRedisOperator.getList(defaultKey);
            log.info("线程睡眠后根据失效的key:{}获取到list类型值:{}", defaultKey, users);
    
            Long aLong = userRedisOperator.listPushAll(defaultKey, List.of(userDetails), timeout);
            log.info("根据key:{}往list类型数据中添加数据:{},成功添加{}条数据,设置过期时间:{}", defaultKey, List.of(userDetails), aLong, timeout);
        }
    
    }
    

    JsonUtils修改Object初始化权限修饰符和静态代码块内容,新内容如下

    /**
     * 设置为public是为了提供给redis的序列化器
     */
    public final static ObjectMapper MAPPER = new ObjectMapper();
    
    static {
        // 对象的所有字段全部列入,还是其他的选项,可以忽略null等
        MAPPER.setSerializationInclusion(JsonInclude.Include.ALWAYS);
        // 取消默认的时间转换为timeStamp格式
        MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        // 设置Date类型的序列化及反序列化格式
        MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // 忽略空Bean转json的错误
        MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 忽略未知属性,防止json字符串中存在,java对象中不存在对应属性的情况出现错误
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        
        // 新内容 添加java8序列化支持和新版时间对象序列化支持
        MAPPER.registerModule(new Jdk8Module());
        MAPPER.registerModule(new JavaTimeModule());
    }
    

    测试

    组装url发起授权请求

    http://127.0.0.1:8080/oauth2/authorize?client_id=messaging-client&response_type=code&scope=message.read&redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2Flogin%2Foauth2%2Fcode%2Fmessaging-client-oidc

    重定向到登录页面

    登录页面

    查看redis

    redis缓存查看

    登录后重定向至授权确认页面

    授权确认

    确认后重定向至回调页面

    回调页面

    如果有什么问题请在评论区指出,如果我看到会尽快处理的,谢谢
    代码已提交至Gitee:仓库地址

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论