欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于Redis實(shí)現(xiàn)共享Session登錄的實(shí)現(xiàn)

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

背景

session 共享問題:如果后端服務(wù)是集群模式,由于多臺機(jī)器之間并不共享 session 存儲空間,當(dāng)請求切換到不同服務(wù)時會導(dǎo)致數(shù)據(jù)丟失的問題

session 的替代方案應(yīng)該滿足:

1.數(shù)據(jù)共享

2.內(nèi)存存儲

3.key、value 結(jié)構(gòu)

Redis 能夠滿足以上的要求,因此可以采用 Redis 來實(shí)現(xiàn)共享登錄

實(shí)現(xiàn)流程

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

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

2.短信驗(yàn)證碼登錄、注冊接口

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

流程圖如下所示:

image-20241024152619663

image-20241024152633775

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

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

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

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

代碼實(shí)現(xiàn)

實(shí)體類

@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;

    /**
     * 手機(jī)號碼
     */
    private String phone;

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

    /**
     * 昵稱,默認(rèn)是隨機(jī)字符
     */
    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;
}

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

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

結(jié)果返回類

@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 {

    // 將對象轉(zhuǎn)為 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); // 設(shè)置為可訪問私有字段
            Object fieldValue = field.get(obj);
            if (fieldValue != null) {
                result.put(field.getName(), field.get(obj).toString());
            }
        }
        return result;
    }

    // 將 Map 轉(zhuǎn)為對象
    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); // 設(shè)置為可訪問私有字段
            String fieldValueStr = fieldValue.toString();
            // 根據(jù)字段類型進(jìn)行轉(zhuǎn)換
            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ā)送手機(jī)驗(yàn)證碼
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone) {
        return userService.sendCode(phone);
    }
    
    /**
     * 登錄功能
     * @param loginForm 登錄參數(shù),包含手機(jī)號、驗(yàn)證碼;或者手機(jī)號、密碼
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm){
        return userService.login(loginForm);
    }
}

服務(wù)層

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

    @Autowired
    private StringRedisTemplate redisTemplate;

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

    @Override
    public Result login(LoginFormDTO loginForm/*, HttpSession session*/) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        /*// 從 session 取出手機(jī)號和驗(yàn)證碼
        String[] phoneAndCode = session.getAttribute("code").toString().split("-");
        // 校驗(yàn)手機(jī)號和驗(yàn)證碼
        if(!phoneAndCode[0].equals(phone) || !phoneAndCode[1].equals(code)) {
            return Result.fail("手機(jī)號或驗(yàn)證碼錯誤");
        }*/
        // 從 redis 中取出驗(yàn)證碼
        String realCode = redisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        if(StringUtils.isBlank(realCode) || !realCode.equals(code)) {
            return Result.fail("驗(yàn)證碼錯誤");
        }
        // 根據(jù)手機(jī)號查詢用戶
        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 轉(zhuǎn)為 UserDTO 再轉(zhuǎn)為 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);
    }

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

}

攔截器及其配置類

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

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

@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( // 排除的攔截路徑
            		   // 以下根據(jù)業(yè)務(wù)需求來寫
                        "/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 轉(zhuǎn)為 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;
    }

}

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

相關(guān)文章

  • 詳解Redis使用認(rèn)證密碼登錄

    詳解Redis使用認(rèn)證密碼登錄

    本篇文章主要介紹了詳解Redis使用認(rèn)證密碼登錄 。啟用Redis的認(rèn)證密碼可以增加Redis服務(wù)器的安全性。有興趣的可以了解下
    2017-06-06
  • Redis優(yōu)化token校驗(yàn)主動失效的實(shí)現(xiàn)方案

    Redis優(yōu)化token校驗(yàn)主動失效的實(shí)現(xiàn)方案

    在普通的token頒發(fā)和校驗(yàn)中 當(dāng)用戶發(fā)現(xiàn)自己賬號和密碼被暴露了時修改了登錄密碼后舊的token仍然可以通過系統(tǒng)校驗(yàn)直至token到達(dá)失效時間,所以系統(tǒng)需要token主動失效的一種能力,所以本文給大家介紹了Redis優(yōu)化token校驗(yàn)主動失效的實(shí)現(xiàn)方案,需要的朋友可以參考下
    2024-03-03
  • 淺談Redis?中的過期刪除策略和內(nèi)存淘汰機(jī)制

    淺談Redis?中的過期刪除策略和內(nèi)存淘汰機(jī)制

    本文主要介紹了Redis?中的過期刪除策略和內(nèi)存淘汰機(jī)制,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04
  • Redis連接池監(jiān)控(連接池是否已滿)與優(yōu)化方法

    Redis連接池監(jiān)控(連接池是否已滿)與優(yōu)化方法

    本文詳細(xì)講解了如何在Linux系統(tǒng)中監(jiān)控Redis連接池的使用情況,以及如何通過連接池參數(shù)配置、系統(tǒng)資源使用情況、Redis命令監(jiān)控、外部監(jiān)控工具等多種方法進(jìn)行檢測和優(yōu)化,以確保系統(tǒng)在高并發(fā)場景下的性能和穩(wěn)定性,討論了連接池的概念、工作原理、參數(shù)配置,以及優(yōu)化策略等內(nèi)容
    2024-09-09
  • Redis數(shù)據(jù)結(jié)構(gòu)之intset整數(shù)集合使用學(xué)習(xí)

    Redis數(shù)據(jù)結(jié)構(gòu)之intset整數(shù)集合使用學(xué)習(xí)

    這篇文章主要為大家介紹了Redis數(shù)據(jù)結(jié)構(gòu)之整數(shù)集合使用學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-07-07
  • 如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性

    如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性

    這篇文章主要介紹了如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性,文中舉了兩個場景例子介紹的非常詳細(xì),需要的朋友可以參考下
    2023-05-05
  • springboot整合使用云服務(wù)器上的Redis方法

    springboot整合使用云服務(wù)器上的Redis方法

    這篇文章主要介紹了springboot整合使用云服務(wù)器上的Redis,整合步驟通過導(dǎo)入依賴,配置yml文件,注入redisTemplate結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),文中給大家分享了可能遇到的坑,感興趣的朋友跟隨小編一起看看吧
    2022-09-09
  • Redis集群(cluster模式)搭建過程

    Redis集群(cluster模式)搭建過程

    文章介紹了Redis集群的概念、使用原因和搭建方法,Redis集群通過分區(qū)實(shí)現(xiàn)數(shù)據(jù)水平擴(kuò)容,提供了一定的可用性,文章詳細(xì)闡述了集群的連接方式,解釋了如何分配節(jié)點(diǎn),并提供了詳細(xì)的集群搭建步驟,包括創(chuàng)建節(jié)點(diǎn)、清空數(shù)據(jù)、修改配置、啟動節(jié)點(diǎn)、配置集群等
    2024-10-10
  • Redis類型type與編碼encoding原理及使用示例

    Redis類型type與編碼encoding原理及使用示例

    這篇文章主要為大家介紹了Redis類型type與編碼encoding原理及使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • Redis面試必會的題目

    Redis面試必會的題目

    這篇文章主要介紹了Redis面試必會的題目,幫助大家更好的理解和學(xué)習(xí)redis數(shù)據(jù)庫,感興趣的朋友可以了解下
    2020-08-08

最新評論