Redis单点登录

117 阅读6分钟

单点登录

Redis连接池构建以及调试

声明:

public class RedisPool {
    private static JedisPool pool;//Jedis连接池,加static是因为要在tomcat启动时就加载
​
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total", "20"));//设置和Redis Server的最大连接数
​
    private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle", "20"));//JedisPool中最大的空闲实例数量
​
    private static Integer minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle", "20"));//JedisPool中最小的空闲实例数量
​
    private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow", "true"));//在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值true。则得到的jedis实例肯定是可以用的。
​
    private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return", "true"));//在return一个jedis实例的时候,是否要进行验证操作,如果赋值true。则放回jedispool的jedis实例肯定是可以用的。
​
    private static String redis1Ip = PropertiesUtil.getProperty("redis1.ip");
​
    private static Integer redis1Port = Integer.parseInt(PropertiesUtil.getProperty("redis1.port"));
​
    private static String redis2Ip = PropertiesUtil.getProperty("redis2.ip");
​
    private static Integer redis2Port = Integer.parseInt(PropertiesUtil.getProperty("redis2.port"));
​
    private static void initPool() {//后续后用一个内部静态块来调用这个方法,为了避免外部有人调用,设置成private
        JedisPoolConfig config = new JedisPoolConfig();//只用config一个对象就可以初始化这个连接池
​
        config.setMaxTotal(maxTotal);//不赋值源码中的默认值是8
        config.setMaxIdle(maxIdle);//不赋值源码中的默认值是8
        config.setMinIdle(minIdle);//不赋值源码中的默认值是0
​
        config.setTestOnBorrow(testOnBorrow);//不赋值默认的是false
        config.setTestOnReturn(testOnReturn);//不赋值默认的是false
​
        config.setBlockWhenExhausted(true);//连接耗尽的时候,是否阻塞,false会抛出异常,true阻塞直到超时。默认为true。
​
        pool = new JedisPool(config, redis1Ip, redis1Port, 1000 * 2);
    }
​
    static {
        initPool();
    }
​
    //对外部的类,要把jedis中的内部的实例开放出去
    public static Jedis getJedis() {
        return pool.getResource();//从连接池获得实例
    }
​
    //把jedis放回连接池
    public static void returnBrokenResource(Jedis jedis) {
        pool.returnBrokenResource(jedis);
    }
​
    public static void returnResource(Jedis jedis) {
        pool.returnResource(jedis);
    }
​
​
    public static void main(String[] args) {
        Jedis jedis = pool.getResource();
​
        for (int i = 0; i < 10; i++) {
            jedis.set("key" + i, "value" + i);
        }
        returnResource(jedis);
​
//        pool.destroy();//临时调用,销毁连接池中的所有连接
        System.out.println("program is end");
    }
}
​

JedisPoolConfig

JedisPool

Jedis API封装及调试

public static Long expire(String key,int exTime){
    Jedis jedis = null;
    Long result = null;
    try {
        jedis = RedisPool.getJedis();
        result = jedis.expire(key,exTime);
    } catch (Exception e) {
        log.error("expire key:{} error",key,e);
        RedisPool.returnBrokenResource(jedis);
        return result;
    }
    RedisPool.returnResource(jedis);
    return result;
}
​
//exTime的单位是秒
public static String setEx(String key,String value,int exTime){
    Jedis jedis = null;
    String result = null;
    try {
        jedis = RedisPool.getJedis();
        result = jedis.setex(key,exTime,value);
    } catch (Exception e) {
        log.error("setex key:{} value:{} error",key,value,e);
        RedisPool.returnBrokenResource(jedis);
        return result;
    }
    RedisPool.returnResource(jedis);
    return result;
}
​
public static String set(String key,String value){
    Jedis jedis = null;
    String result = null;
​
    try {
        jedis = RedisPool.getJedis();
        result = jedis.set(key,value);
    } catch (Exception e) {
        log.error("set key:{} value:{} error",key,value,e);
        RedisPool.returnBrokenResource(jedis);
        return result;
    }
    RedisPool.returnResource(jedis);
    return result;
}
​
public static String get(String key){
    Jedis jedis = null;
    String result = null;
    try {
        jedis = RedisPool.getJedis();
        result = jedis.get(key);
    } catch (Exception e) {
        log.error("get key:{} error",key,e);
        RedisPool.returnBrokenResource(jedis);
        return result;
    }
    RedisPool.returnResource(jedis);
    return result;
}
​
public static Long del(String key){
    Jedis jedis = null;
    Long result = null;
    try {
        jedis = RedisPool.getJedis();
        result = jedis.del(key);
    } catch (Exception e) {
        log.error("del key:{} error",key,e);
        RedisPool.returnBrokenResource(jedis);
        return result;
    }
    RedisPool.returnResource(jedis);
    return result;
}

Jackson 封装JsonUtil

@Slf4j
/**
 * @Auther: wanglin
 * @Date:9/4/2022 - 04 -09 -下午9:55
 * @Description:com.mmall.util
 * @version:1.0
 */
public class JsonUtil {//序列化与反序列化,用到Jackson工具包
​
    private static ObjectMapper objectMapper = new ObjectMapper();
    static{
        //对象的所有字段全部列入
        objectMapper.setSerializationInclusion(Inclusion.ALWAYS);
​
        //取消默认转换timestamps形式
        objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS,false);
​
        //忽略空Bean转json的错误
        objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS,false);
​
        //所有的日期格式都统一为以下的样式,即yyyy-MM-dd HH:mm:ss
        objectMapper.setDateFormat(new SimpleDateFormat(DateTimeUtil.STANDARD_FORMAT));
​
        //反序列化
        //忽略 在json字符串中存在,但是在java对象中不存在对应属性的情况。防止错误
        objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES,false);
    }
​
    //把对象转换为json,也可以把json转化成对象
    public static <T> String obj2String(T obj){
        if(obj == null){
            return null;
        }
        try {
            return obj instanceof String ? (String)obj :  objectMapper.writeValueAsString(obj);//obj是范型<T>类型,所以要强转
        } catch (Exception e) {
            log.warn("Parse Object to String error",e);
            return null;
        }
    }
​
    //用来封装可以返回格式化好的字符串
    public static <T> String obj2StringPretty(T obj){
        if(obj == null){
            return null;
        }
        try {
            return obj instanceof String ? (String)obj :  objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("Parse Object to String error",e);
            return null;
        }
    }
​
​
    //把字符串转化成对象
    public static <T> T string2Obj(String str,Class<T> clazz){// public static <T> 声明方法只有一个类型T,也可以看作这是一个泛型方法
        if(StringUtils.isEmpty(str) || clazz == null){
            return null;
        }
​
        try {
            return clazz.equals(String.class)? (T)str : objectMapper.readValue(str,clazz);
        } catch (Exception e) {
            log.warn("Parse String to Object error",e);
            return null;
        }
    }
​
    //封装两个通用性非常强的反序列方法。
    //应对比如将字符串反序列化为对象时,对象的类型是List<User> 需要传两个类型,不能传单独一个
    public static <T> T string2Obj(String str, TypeReference<T> typeReference){
        if(StringUtils.isEmpty(str) || typeReference == null){
            return null;
        }
        try {
            return (T)(typeReference.getType().equals(String.class)? str : objectMapper.readValue(str,typeReference));
        } catch (Exception e) {
            log.warn("Parse String to Object error",e);
            return null;
        }
    }
​
    //为社么要用问号:因为集合是一个类型,集合中的元素又是一个类型
    public static <T> T string2Obj(String str,Class<?> collectionClass,Class<?>... elementClasses){//Class<?>代表集合类型,Class<?>...表示可变长参数,可以传一个参数也可以传多个
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass,elementClasses);
        try {
            return objectMapper.readValue(str,javaType);
        } catch (Exception e) {
            log.warn("Parse String to Object error",e);
            return null;
        }
    }
​
    public static void main(String[] args) {
        User u1 = new User();
        u1.setUsername("111");
        u1.setEmail("asghdsafh");
        String user1json = JsonUtil.obj2String(u1);
        String user1jsonpretty = JsonUtil.obj2StringPretty(u1);
        log.info("user1json:{}" , user1json);
        log.info("user1jsonpretty {}", user1jsonpretty);
​
        User user = JsonUtil.string2Obj(user1json, User.class);
        System.out.println("**********************************");
    }
​
}
​

Cookie 封装及使用

写、删、读

domain

@Slf4j
public class CookieUtil {
​
    private final static String COOKIE_DOMAIN = ".happymmall.com";//xxx.happymmall.com的域名可以读到cookie
​
    private final static String COOKIE_NAME = "mmall_login_token";//服务端种到客户端浏览器中的
​
    //读cookie从request中读
    public static String readLoginToken(HttpServletRequest request){
        Cookie[] cks = request.getCookies();
        if(cks != null){
            for(Cookie ck : cks){
                log.info("read cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                if(StringUtils.equals(ck.getName(),COOKIE_NAME)){
                    log.info("return cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                    return ck.getValue();
                }
            }
        }
        return null;
    }
    //c和d能共享a的cookie, 也能共享e的cookie
​
    //X:domain=".happymmall.com"
    //a:A.happymmall.com            cookie:domain=A.happymmall.com;path="/"
    //b:B.happymmall.com            cookie:domain=B.happymmall.com;path="/"
    //c:A.happymmall.com/test/cc    cookie:domain=A.happymmall.com;path="/test/cc"
    //d:A.happymmall.com/test/dd    cookie:domain=A.happymmall.com;path="/test/dd"
    //e:A.happymmall.com/test       cookie:domain=A.happymmall.com;path="/test"
​
    //写cookie从response中写
    //对response的回应
    public static void writeLoginToken(HttpServletResponse response,String token){
        Cookie ck = new Cookie(COOKIE_NAME,token);
        ck.setDomain(COOKIE_DOMAIN);
        ck.setPath("/");//代表设置在根目录
        ck.setHttpOnly(true);//防止脚本攻击带来的信息泄露风险,规定不许通过脚本访问cookie
        //单位是秒。
        //如果这个maxage不设置的话,cookie就不会写入硬盘,而是写在内存。只在当前页面有效。
        ck.setMaxAge(60 * 60 * 24 * 365);//如果是-1,代表永久
        log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
        response.addCookie(ck);
    }
​
​
    public static void delLoginToken(HttpServletRequest request,HttpServletResponse response){
        Cookie[] cks = request.getCookies();
        if(cks != null){
            for(Cookie ck : cks){
                if(StringUtils.equals(ck.getName(),COOKIE_NAME)){
                    ck.setDomain(COOKIE_DOMAIN);
                    ck.setPath("/");
                    ck.setMaxAge(0);//设置成0,代表删除此cookie。
                    log.info("del cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                    response.addCookie(ck);
                    return;
                }
            }
        }
    }
​
}

改造成用cookie作为key从Redis中找的模式

@Controller
@RequestMapping("/user/")
public class UserController {
​
    @Autowired
    private IUserService iUserService;//把Service中的接口注入
​
    /**
     * 用户登录
     * @param username
     * @param password
     * @param session
     * @return
     */
    @RequestMapping(value = "login.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse) {
        //service -->mybatis --> dao
        ServerResponse<User> response = iUserService.login(username, password);
        if(response.isSuccess()) {
            //session.setAttribute(Const.CURRENT_USER, response.getData());
​
            CookieUtil.writeLoginToken(httpServletResponse, session.getId());
            //获得sessionId值作为Redis的key,所获得的对象作为value序列化后存入Redis中
            RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            //把登录信息保存到服务端的Redis中,在客户端使用
​
​
            //在服务端写一个cookie
​
        }
        return response;
    }
​
    @RequestMapping(value = "logout.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        CookieUtil.delLoginToken(httpServletRequest, httpServletResponse);//cookie的删除是把coolie的有效时间设置为e,返回给浏览器
​
        //同样的,在Redis中也要把数据删除
        RedisShardedPoolUtil.del(loginToken);
        //session.removeAttribute(Const.CURRENT_USER);
        return ServerResponse.createBySuccess();
    }
​
    @RequestMapping(value = "register.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> register(User user) {
        return iUserService.register(user);
    }
​
    @RequestMapping(value = "check_valid.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> checkValid(String str, String type) {
        return iUserService.checkValid(str, type);
    }
​
    @RequestMapping(value = "get_user_info.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> getUserInfo (HttpServletRequest httpServletRequest) {
        //User user = (User) session.getAttribute(Const.CURRENT_USER);
​
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        if(StringUtils.isEmpty(loginToken)) {
            return ServerResponse.createByErrorMessage("用户未登录,无法获取用户信息");
        }
​
        String userJsonStr = RedisShardedPoolUtil.get(loginToken);
        User user = JsonUtil.string2Obj(userJsonStr, User.class);
​
​
        if(user != null) {
            //改造成为不从session中拿信息,而是从cookie中拿信息,去redis中查找
            return ServerResponse.createBySuccess(user);
        }
        return ServerResponse.createByErrorMessage("用户未登录,无法获取用户信息");
    }
​
    @RequestMapping(value = "foeget_get_question.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> forgetGetQuestion(String username) {
        return iUserService.selectQuestion(username);
    }
​
    @RequestMapping(value = "foeget_check_answer.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> forgetCheckAnswer(String username, String question, String answer) {
        return iUserService.checkAnswer(username, question, answer);
    }
​
    @RequestMapping(value = "foeget_reset_password.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> forgetResetPassword(String username, String passwordNew, String forgetToken) {
        return iUserService.forgetResetPassword(username, passwordNew, forgetToken);
    }
​
    @RequestMapping(value = "reset_reset_password.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> resetPassword(HttpSession session, String passwordOld, String passwordNew) {
        User user = (User)session.getAttribute(Const.CURRENT_USER);
        if(user == null)
            return ServerResponse.createByErrorMessage("用户为登录");
​
        return iUserService.resetPassword(passwordOld, passwordNew, user);
    }
​
    @RequestMapping(value = "update_information.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> update_information(HttpServletRequest httpServletRequest, User user) {
        //User currentUser = (User)session.getAttribute(Const.CURRENT_USER);
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        if(StringUtils.isEmpty(loginToken)) {
            return ServerResponse.createByErrorMessage("用户未登录,无法获取用户信息");
        }
​
        String userJsonStr = RedisShardedPoolUtil.get(loginToken);
        User currentUser = JsonUtil.string2Obj(userJsonStr, User.class);
        if(currentUser == null)
            return ServerResponse.createByErrorMessage("用户未登录");
​
        user.setId(currentUser.getId());
        user.setUsername(user.getUsername());
        ServerResponse<User> response = iUserService.updateInformation(user);
        if(response.isSuccess()) {
            //session.setAttribute(Const.CURRENT_USER, response.getData());
            RedisShardedPoolUtil.setEx(loginToken, JsonUtil.obj2String(response.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
        }
        return response;
    }
​
    @RequestMapping(value = "get_information.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> get_information(HttpServletRequest httpServletRequest) {
       // User currentUser = (User)session.getAttribute(Const.CURRENT_USER);
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        if(StringUtils.isEmpty(loginToken)) {
            return ServerResponse.createByErrorMessage("用户未登录,无法获取用户信息");
        }
​
        String userJsonStr = RedisShardedPoolUtil.get(loginToken);
        User currentUser = JsonUtil.string2Obj(userJsonStr, User.class);
        if(currentUser == null)
            return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),"未登录,需要强制登录status = 10");
​
        return iUserService.getInformation(currentUser.getId());
    }
}

SessionExpireFilter 重置Session有效期

session的默认有效期是30分钟,我们需要在请求时将session的有效期重新设置成30分钟,否则就变成了只能登录30分钟

过滤所有.do结尾的,并在其中进行判断,把session的时间进行重置

public class SessionExpireFilter implements Filter {
​
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
​
    }
​
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
​
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
​
        if(StringUtils.isNotEmpty(loginToken)){
            //判断logintoken是否为空或者"";
            //如果不为空的话,符合条件,继续拿user信息
​
            String userJsonStr = RedisShardedPoolUtil.get(loginToken);
            User user = JsonUtil.string2Obj(userJsonStr,User.class);
            if(user != null){
                //如果user不为空,则重置session的时间,即调用expire命令
                RedisShardedPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }
        filterChain.doFilter(servletRequest,servletResponse);//在web.xml中配置了filter,凡是以do结尾的, 就走拦截器,重置session时间
    }
​
    @Override
    public void destroy() {
​
    }
}

Guava Cache 迁移 Redis缓存

public ServerResponse<String> checkAnswer(String username, String question, String answer) {
    int resultCount = userMapper.checkAnswer(username, question, answer);
    if(resultCount > 0) {
        //说明问题以及问题答案是这个用户的,并且答案是对的
        String forgetToken = UUID.randomUUID().toString();
        //TokenCache.setKey(TokenCache.TOKEN_PREFIX + username, forgetToken);
        RedisShardedPoolUtil.setEx(Const.TOKEN_PREFIX+ username, forgetToken, 60 * 60 * 12);
        return ServerResponse.createBySuccess(forgetToken);
    }
    return ServerResponse.createByErrorMessage("问题的答案错误");
}
​
public ServerResponse<String> forgetResetPassword(String username, String passwordNew, String forgetToken){
    if(StringUtils.isNoneBlank(forgetToken))
        return ServerResponse.createByErrorMessage("参数错误,token需要传递");
​
    //需要校验一下username,因为拼接的时候是"token_"+username,如果username是空的,那么这个缓存就变成了一个没有变量控制的
    //每个用户都可以拿到forgetToken
    ServerResponse validResponse = this.checkValid(username, Const.USERNAME);
    if(!validResponse.isSuccess()) {
        return validResponse;
    }
​
   // String token = TokenCache.getKey(TokenCache.TOKEN_PREFIX+ username);
    String token = RedisShardedPoolUtil.get(Const.TOKEN_PREFIX+ username);
    if(StringUtils.isNoneBlank(token)) {
        return ServerResponse.createByErrorMessage("token无效或者过期");
    }
​
    if(StringUtils.equals(forgetToken, token)) {
        String md5password = MD5Util.MD5EncodeUtf8(passwordNew);
        int rowCount = userMapper.updatePasswordByUsername(username, md5password);
​
        if(rowCount > 0)
            return ServerResponse.createBySuccessMessage("修改密码成功");
    }else {
        return ServerResponse.createByErrorMessage("token错误,请重新获取重置密码的token");
    }
    return ServerResponse.createByErrorMessage("修改密码失败");
}