最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Redis實現共享Session登錄的實現

 更新時間:2025年03月06日 08:24:21   作者:小璐亂撞xllz  
本文主要介紹了基于Redis實現共享Session登錄的實現,包括發(fā)送短信驗證碼、短信驗證碼登錄和注冊、以及登錄狀態(tài)校驗的流程,具有一定的參考價值,感興趣的可以了解一下

背景

session 共享問題:如果后端服務是集群模式,由于多臺機器之間并不共享 session 存儲空間,當請求切換到不同服務時會導致數據丟失的問題

session 的替代方案應該滿足:

1.數據共享

2.內存存儲

3.key、value 結構

Redis 能夠滿足以上的要求,因此可以采用 Redis 來實現共享登錄

實現流程

這里以短信登錄的業(yè)務作為示例,主要包括三個功能:

1.發(fā)送短信驗證碼的接口

2.短信驗證碼登錄、注冊接口

3.校驗登錄狀態(tài)攔截器

流程圖如下所示:

image-20241024152619663

image-20241024152633775

這里采用的策略是,發(fā)送驗證碼時,將對應的手機號作為 key,驗證碼作為 value

登錄、注冊時,需要使用手機號將驗證碼取出,并且以隨機 token 作為 key,用戶信息作為 value 保存用戶數據,這里的用戶數據用 hash 類型保存。最后還需要將這個 token 返回給前端

之后在校驗登錄狀態(tài)時,前端的每次請求都需要攜帶這個 token 值,以便服務端能取出相應的用戶信息

這里使用隨機 token 而不使用手機號作為 key 的目的在于,瀏覽器是需要存儲這個 key 的,以便校驗登錄狀態(tài),如果使用手機號會不安全

代碼實現

實體類

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主鍵
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 手機號碼
     */
    private String phone;

    /**
     * 密碼,加密存儲
     */
    private String password;

    /**
     * 昵稱,默認是隨機字符
     */
    private String nickName;

    /**
     * 用戶頭像
     */
    private String icon = "";

    /**
     * 創(chuàng)建時間
     */
    private LocalDateTime createTime;

    /**
     * 更新時間
     */
    private LocalDateTime updateTime;


}

dto 類

@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

這里單獨抽取 dto 的原因在于,我們不希望將密碼等敏感字段返回給前端

@Data
public class LoginFormDTO {
    private String phone;
    private String code;
    private String password;
}

結果返回類

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Boolean success;
    private String errorMsg;
    private Object data;
    private Long total;

    public static Result ok(){
        return new Result(true, null, null, null);
    }
    public static Result ok(Object data){
        return new Result(true, null, data, null);
    }
    public static Result ok(List<?> data, Long total){
        return new Result(true, null, data, total);
    }
    public static Result fail(String errorMsg){
        return new Result(false, errorMsg, null, null);
    }
}

常量類

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;
}

工具類

public class ObjectMapUtils {

    // 將對象轉為 Map
    public static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {
        Map<String, String> result = new HashMap<>();
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 如果為 static 且 final 則跳過
            if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
                continue;
            }
            field.setAccessible(true); // 設置為可訪問私有字段
            Object fieldValue = field.get(obj);
            if (fieldValue != null) {
                result.put(field.getName(), field.get(obj).toString());
            }
        }
        return result;
    }

    // 將 Map 轉為對象
    public static Object map2Obj(Map<Object, Object> map, Class<?> clazz) throws Exception {
        Object obj = clazz.getDeclaredConstructor().newInstance();
        for (Map.Entry<Object, Object> entry : map.entrySet()) {
            Object fieldName = entry.getKey();
            Object fieldValue = entry.getValue();
            Field field = clazz.getDeclaredField(fieldName.toString());
            field.setAccessible(true); // 設置為可訪問私有字段
            String fieldValueStr = fieldValue.toString();
            // 根據字段類型進行轉換
            if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
                field.set(obj, Integer.parseInt(fieldValueStr));
            } else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
                field.set(obj, Boolean.parseBoolean(fieldValueStr));
            } else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
                field.set(obj, Double.parseDouble(fieldValueStr));
            } else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
                field.set(obj, Long.parseLong(fieldValueStr));
            } else if (field.getType().equals(String.class)) {
                field.set(obj, fieldValueStr);
            } else if(field.getType().equals(LocalDateTime.class)) {
                field.set(obj, LocalDateTime.parse(fieldValueStr));
            }

        }
        return obj;
    }

}

控制層

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private IUserService userService;
    
    /**
     * 發(fā)送手機驗證碼
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone) {
        return userService.sendCode(phone);
    }
    
    /**
     * 登錄功能
     * @param loginForm 登錄參數,包含手機號、驗證碼;或者手機號、密碼
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm){
        return userService.login(loginForm);
    }
}

服務層

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Result sendCode(String phone/*, HttpSession session*/) {
        // 校驗手機號
        if(RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手機號格式錯誤");
        }
        // 生成驗證碼
        String code = RandomUtil.randomNumbers(6);
        /*// 保存驗證碼到 session
        session.setAttribute("code", phone + "-" + code);*/
        // 保存驗證碼到 redis
        redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code,
                RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // 發(fā)送驗證碼
        log.debug("發(fā)送驗證碼:" + code + ",手機號:" + phone);
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm/*, HttpSession session*/) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        /*// 從 session 取出手機號和驗證碼
        String[] phoneAndCode = session.getAttribute("code").toString().split("-");
        // 校驗手機號和驗證碼
        if(!phoneAndCode[0].equals(phone) || !phoneAndCode[1].equals(code)) {
            return Result.fail("手機號或驗證碼錯誤");
        }*/
        // 從 redis 中取出驗證碼
        String realCode = redisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        if(StringUtils.isBlank(realCode) || !realCode.equals(code)) {
            return Result.fail("驗證碼錯誤");
        }
        // 根據手機號查詢用戶
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone, phone);
        User user = this.getOne(queryWrapper);
        // 用戶如果不存在,則創(chuàng)建新用戶
        if(user == null) {
            user = createUserWithPhone(phone);
        }
        /*// session 保存用戶信息
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));*/
        // redis 保存用戶信息
        String token = UUID.randomUUID().toString(true);
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        try {
            // 將 User 轉為 UserDTO 再轉為 Map
            Map<String, String> userMap = ObjectMapUtils.obj2Map(BeanUtil.copyProperties(user, UserDTO.class));
            redisTemplate.opsForHash().putAll(tokenKey, userMap);
            redisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        // 將 token 返回
        return Result.ok(token);
    }

    // 根據手機號創(chuàng)建新用戶
    public User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        // 保存至數據庫
        this.save(user);
        return user;
    }

}

攔截器及其配置類

這里會使用兩個攔截器,一個是攔截一切路徑的刷新攔截器,主要用途就是如果用戶在 token 有效期內訪問了系統(tǒng),那么就會刷新超時時間;另一個是攔截部分路徑的登錄校驗攔截器,主要就是檢驗用戶是否登錄

添加刷新攔截器的原因在于,如果用登錄校驗攔截器進行刷新工作,由于排除了部分路徑,因此如果用戶一直訪問這些被排除的部分路徑,會導致用戶 token 的有效期不會被刷新。所以需要單獨添加一個攔截所有路徑的攔截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 刷新攔截器
        registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate)).order(10);
        // 登錄攔截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns( // 排除的攔截路徑
            		   // 以下根據業(yè)務需求來寫
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(20);
    }
}
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate redisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 獲取用戶
        String token = request.getHeader("authorization");
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        // 用戶不存在,直接放行
        if(entries.isEmpty()) {
            return true;
        }
        // Map 轉為 UserDTO
        UserDTO user = (UserDTO) ObjectMapUtils.map2Obj(entries, UserDTO.class);
        // 用戶存在,放入 ThreadLocal
        UserHolder.saveUser(user);
        // 刷新 token 有效期
        redisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 銷毀 ThreadLocal
        UserHolder.removeUser();
    }

}
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 用戶未登錄,攔截
        if(UserHolder.getUser() == null) {
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }
        return true;
    }

}

到此這篇關于基于Redis實現共享Session登錄的實現的文章就介紹到這了,更多相關Redis Session共享登錄內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家! 

相關文章

  • 用Lua腳本實現Redis原子操作的示例

    用Lua腳本實現Redis原子操作的示例

    本文主要介紹了用Lua腳本實現Redis原子操作的示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-03-03
  • Redis的數據淘汰策略使用及注意事項

    Redis的數據淘汰策略使用及注意事項

    Redis的數據淘汰策略是指在內存不足時,如何決定哪些數據需要被淘汰以釋放內存空間,Redis提供了多種數據淘汰策略,如noeviction、allkeys-lru、allkeys-lfu、volatile-lru、volatile-lfu、volatile-random、allkeys-random和volatile-ttl
    2025-12-12
  • Centos7.3安裝Redis4.0.6詳細圖文教程

    Centos7.3安裝Redis4.0.6詳細圖文教程

    這篇文章主要介紹了Centos7.3安裝Redis4.0.6詳細教程圖解,本文圖文并茂給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下
    2018-10-10
  • Redis?集群模式(Cluster)原理詳解

    Redis?集群模式(Cluster)原理詳解

    redis?cluster集群是一個由多個主從節(jié)點集群組成的分布式服務集群,它具有復制、高可用和分片特性,cluster集群不需要sentinel?哨兵也能完成節(jié)點移除和故障轉移的功能,本文就詳細的給大家介紹一下Redis?集群模式原理,感興趣的朋友跟著小編一起來看看吧
    2023-07-07
  • Redis實戰(zhàn)之Jedis使用技巧詳解

    Redis實戰(zhàn)之Jedis使用技巧詳解

    Jedis?是老牌的?Redis?的?Java?客戶端,提供了比較全面的?Redis?命令的操作支持,也是目前使用最廣泛的客戶端。這篇文章主要為大家詳細介紹了Jedis的使用技巧,需要的可以參考一下
    2022-12-12
  • redis配置文件redis.conf中文版(基于2.4)

    redis配置文件redis.conf中文版(基于2.4)

    這篇文章主要介紹了redis配置文件redis.conf中文版(基于2.4),對英文不好的朋友是非常好的參考,需要的朋友可以參考下
    2014-06-06
  • 使用lua+redis解決發(fā)多張券的并發(fā)問題

    使用lua+redis解決發(fā)多張券的并發(fā)問題

    這篇文章主要介紹了使用lua+redis解決發(fā)多張券的并發(fā)問題,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-01-01
  • Redis?生成分布式業(yè)務單號的實現

    Redis?生成分布式業(yè)務單號的實現

    在業(yè)務系統(tǒng)中很多場景下需要生成不重復的ID,本文主要介紹了Redis生成分布式業(yè)務單號的實現,具有一定的參考價值,感興趣的可以了解一下
    2024-04-04
  • Redis集群的實現全過程

    Redis集群的實現全過程

    Redis集群的實現方案主要有客戶端分片、代理模式和Cluster模式,其中,Cluster模式是Redis官方推薦的實現方案,它具有高可用性、高性能和自動分片等優(yōu)點
    2024-12-12
  • Caffeine結合Redis空值緩存實現多級緩存

    Caffeine結合Redis空值緩存實現多級緩存

    本文介紹了SpringBoot整合Caffeine本地緩存+Redis分布式緩存+空值緩存的三級緩存方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2026-01-01

最新評論

朔州市| 浏阳市| 桓台县| 河源市| 泗洪县| 大荔县| 元江| 鄂托克前旗| 江川县| 板桥市| 白城市| 门头沟区| 威远县| 徐汇区| 宁城县| 兰溪市| 嘉荫县| 彝良县| 新龙县| 德清县| 依兰县| 灵宝市| 南川市| 沾化县| 赤峰市| 东莞市| 奎屯市| 乌拉特前旗| 金寨县| 兴安盟| 桂林市| 太原市| 邵阳县| 渭南市| 兴安盟| 洛宁县| 莆田市| 松桃| 鄂伦春自治旗| 陈巴尔虎旗| 鲜城|