且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

基于redis+springboot从零开始设计一个类阿里系的单点登录(2)

更新时间:2022-05-18 18:32:29

设计思路与环境

环境

JDK 11

maven3.6.2

redis 6

springboot 2.x


设计与编写思路

单点登录验证中心:

islogin:验证是否有token,并且设置对应user的限时key

checklogin:检查有没有token,没有的话就带着请求地址转发到login登录,如果有token就返回token给客户端做二次请求校验

verify: 校验token是否正确,返回布尔值

注销

客户端思路:


请求工具封装

拦截器:http请求验证是否有登陆过,如果没有转发到检查登录

服务器登录完整之后会转发会客户端,拦截器判断是否有token参数,获取token请求验证中心二次验证,通过的话放行

功能流程图

基于redis+springboot从零开始设计一个类阿里系的单点登录(2)



SS0_server主要实现

客户端拦截器请求处理:


拦截器:请求islogin判断有没有登录


server:返回登录判断结果,如果检查到redis有token就生成对应的限时userkey


拦截器:如果没有登陆过,并且token为null那么就请求checklofin


server:检查没有token,代表第一次请求转发到login登录并且返回携带token返回


拦截器:检测到token请求二次校验,


server:返回与redis和tokenmap的校验结果


拦截器:根据结果判断是否重新请求checklogin或者通过


下面代码实现上述流程的server处理


服务端核心功能

logincontroller

islogin:验证是否有token,并且设置对应user的限时key

checklogin:检查有没有token,没有的话就带着请求地址转发到login登录,如果有token就返回token给客户端做二次请求校验

verify: 校验token是否正确,返回布尔值

注销

/**
 * @projectName: SSO_self
 * @package: com.hyc.sso_server.controller
 * @className: loginController
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/1/9 12:44
 * @version: 1.0
 */
@Controller
public class loginController {
    @Autowired
    StringRedisTemplate redisTemplate;

    @RequestMapping("/index")
    public String login() {
        return "login";
    }

    UserAndPassword userAndPassword = new UserAndPassword();

    @RequestMapping("/login")
    public String login(String username, String password, String redirectUrl, RedirectAttributes model) {
        System.out.println("loginuser" + "\t" + username + "\t" + "password" + "\t" + password);
        //模拟登录数据库设计
        if ("admin".equals(username) && "123456".equals(password)) {
            //校验成功
            //    1. 创建token
            String token = UUID.randomUUID().toString();
            System.out.println("token创建成功==>" + token);
            //    2. 需要把令牌信息收入到数据库中
            //if (redisTemplate.hasKey("token")) {
            //    redisTemplate.delete("token");
            //}
            redisTemplate.opsForSet().add("token", token);
            userAndPassword.setUsername(username);
            userAndPassword.setPassword(password);
            userTokenMap.tokenmap.put(username, token);
            //    3.重定向到redirecturl
            model.addAttribute("token", token);
            model.addAttribute("username", username);
            System.out.println(redirectUrl);
            return "redirect:" + redirectUrl;
        }
        //如果密码不正确,重新的返回登录页面
        System.out.println("用户密码不正确,重复回到登录页面");
        model.addAttribute("redirectUrl", redirectUrl);
        return "login";
    }

    //检查登录
    @RequestMapping("/checkLogin")
    public String checklogin(String redirectUrl, Model model) {
        String getgenretetoken = userTokenMap.tokenmap.get(userAndPassword.getUsername());
        Set<String> token = redisTemplate.opsForSet().members("token");
        if (!token.contains(getgenretetoken)) {
            //    没有全局会话的话 跳转带统一认证中心的登陆界面
            // 携带转发来的地址 一起转发回去
            model.addAttribute("redirectUrl", redirectUrl);
            return "login";
        } else {
            //   全局会话 取出令牌信息 重定向到 redirecturl
            model.addAttribute("token", getgenretetoken);
            model.addAttribute("username", userAndPassword.getUsername());
            return "redirect:" + redirectUrl;
        }
    }

    @RequestMapping("/islogin")
    @ResponseBody
    public String islogin() {
        String getgenretetoken = userTokenMap.tokenmap.get(userAndPassword.getUsername());
        Set<String> token = redisTemplate.opsForSet().members("token");
        if (!token.contains(getgenretetoken)) {
            return "false";
        } else {
            String islogin = null;
            if (redisTemplate.hasKey(userAndPassword.getUsername())) {
                islogin = redisTemplate.opsForValue().get(userAndPassword.getUsername());
            } else {
                islogin = String.valueOf(RedisUtils.cacheValue(userAndPassword.getUsername(), "true", 30));
            }
            return islogin;
        }
    }

    //校验token 是否是验证中心产生
    @RequestMapping("/verify")
    @ResponseBody
    public String verifytoken(String token) {
        if (redisTemplate.opsForSet().isMember("token", token)) {
            System.out.println("服务端token验证ok===>" + token);
            return "true";
        }
        return "false";
    }

    @RequestMapping("/loginOut")
    @ResponseBody
    public String loginout() {

        RedisUtils.redisTemplate.delete("token");
        RedisUtils.redisTemplate.delete("islogin");
        System.out.println("删除成功");
        return "注销成功";
    }
}

redis监听器

检查userkey是否过期,redis触发过期时间就删除对应的token

配置:


@Configuration
public class RedisConfiguration {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    //配置 将spring的 redis链接工厂放入监听容器
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer() {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
        return redisMessageListenerContainer;
    }

    //将自定义的监听器放入容器中
    @Bean
    public keyexpireListener keyExpiredListener() {
        return new keyexpireListener(this.redisMessageListenerContainer());
    }
}


自定义监听器

/**
 * @projectName: SSO_self
 * @package: com.hyc.sso_server.Listener
 * @className: keyexpireListener
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/1/11 17:30
 * @version: 1.0
 */
public class keyexpireListener extends KeyExpirationEventMessageListener {

    //日志
    private static final Logger LOGGER = LoggerFactory.getLogger(keyexpireListener.class);

    /**
     * @param listenerContainer must not be {@literal null}.
     */
    public keyexpireListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    //实现信息方法,打印监听日志,后续可能对过期key处理
    /*redis key 过期:pattern=__keyevent@*__:expired,channel=__keyevent@0__:expired,key=name
     * 测试完毕
     * */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
        //过期的key
        String key = new String(message.getBody(), StandardCharsets.UTF_8);
        //  如果登录令牌过期,则删除token 要求用户重新登录
        for (String s : userTokenMap.tokenmap.keySet()) {
            if (key.equals(s)) {
                RedisUtils.redisTemplate.opsForSet().remove("token", userTokenMap.tokenmap.get(key));
                System.out.println("删除成功");
            }
        }
        LOGGER.info("redis key 过期:pattern={},channel={},key={}", new String(pattern), channel, key);
    }
}

客户端核心

SSO请求工具

public class SSOClientUtil {

    private static Properties ssoProperties = new Properties();
    public static String SERVER_URL_PREFIX; //统一认证中心地址
    public static String CLIENT_HOST_URL; //当前客户端地址

    static {
        try {
            ssoProperties.load(SSOClientUtil.class.getClassLoader().getResourceAsStream("sso.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        SERVER_URL_PREFIX = ssoProperties.getProperty("server-url-prefix");
        CLIENT_HOST_URL = ssoProperties.getProperty("client-host-url");
    }

    /**
     * 当客户端请求被拦截,跳往统一认证中心,需要带redirectUrl的参数,统一认证中心登录后回调的地址
     */
    public static String getRedirectUrl(HttpServletRequest request) {
        //获取请求URL
        return CLIENT_HOST_URL + request.getServletPath();
    }

    /**
     * 根据request获取跳转到统一认证中心的地址,通过Response跳转到指定的地址
     */
    public static void redirectToSSOURL(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String redirectUrl = getRedirectUrl(request);
        StringBuilder url = new StringBuilder(50)
                .append(SERVER_URL_PREFIX)
                .append("/checkLogin?redirectUrl=")
                .append(redirectUrl);
        response.sendRedirect(url.toString());
    }

    public static void redirectToSSOURL(HttpServletRequest request, HttpServletResponse response, String token) throws IOException {
        String redirectUrl = getRedirectUrl(request);
        StringBuilder url = new StringBuilder(50)
                .append(SERVER_URL_PREFIX)
                .append("/checkLogin?redirectUrl=")
                .append(redirectUrl)
                .append("&token=")
                .append(token);
        response.sendRedirect(url.toString());
    }

    /**
     * 获取客户端的完整登出地址
     */
    public static String getClientLogOutUrl() {
        return CLIENT_HOST_URL + "/loginOut";
    }

    /**
     * 获取认证中心的登出地址
     */
    public static String getServerLogOutUrl() {
        return SERVER_URL_PREFIX + "/loginOut";
    }
}

对应配置文件

server-url-prefix=http://www.sso.com:8001
client-host-url=http://www.tm.com:8003

http请求工具

/**
 * @projectName: SSO_self
 * @package: com.hyc.http
 * @className: HttpUitl
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/1/9 0:52
 * @version: 1.0
 */
public class HttpUitl {
    public static String sendHttpRequest(String httpurl, Map<String, String> parms) throws Exception {
        //定义需要访问的地址
        URL url = new URL(httpurl);
        //连接 URL
        HttpURLConnection connect = (HttpURLConnection) url.openConnection();
        //请求方式
        connect.setRequestMethod("POST");
        //携带参数
        connect.setDoOutput(true);
        if (parms != null && parms.size() > 0) {
            StringBuffer sb = new StringBuffer();
            for (Map.Entry<String, String> par : parms.entrySet()) {
                sb.append("&").append(par.getKey()).append("=").append(par.getValue());
            }
            connect.getOutputStream().write(sb.substring(1).toString().getBytes("UTF-8"));
        }
        // 发起请求
        connect.connect();
        //接受返回值
        String response = StreamUtils.copyToString(connect.getInputStream(), Charset.forName("UTF-8"));
        return response;
    }
}

拦截器

请求校验

二次校验

拦截判断

/**
 * @projectName: SSO_self
 * @package: com.hyc.ssoclienttb.interceptor
 * @className: ssoInterceptor
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/1/9 20:20
 * @version: 1.0
 */
public class ssoInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String httpuri = SSOClientUtil.SERVER_URL_PREFIX + "/islogin";
        HashMap<String, String> isloginparms = new HashMap<>();
        String islogin = HttpUitl.sendHttpRequest(httpuri, isloginparms);
        if (islogin.equals("true")) {
            return true;
        }
        //判断地址栏是否有携带token参数
        String token = request.getParameter("token");
        System.out.println(token);
        if (!StringUtils.isEmpty(token)) {
            System.out.println("检测到token信息,需要去sso服务器验证!");
            //    token 信息不为空 说明拥有令牌,我们需要去认证中心检查这个令牌,以防伪造
            String httpURL = SSOClientUtil.SERVER_URL_PREFIX + "/verify";
            HashMap<String, String> parms = new HashMap<>();
            //放入登出的位置和token
            parms.put("token", token);
            try {
                String isverify = HttpUitl.sendHttpRequest(httpURL, parms);
                if ("true".equals(isverify)) {
                    //    如果返回的字符串是true 说明这个token是由统一认证中心产生的
                    System.out.println("检测到token信息,验证通过");
                    return true;
                } else {
                    SSOClientUtil.redirectToSSOURL(request, response);
                    return false;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //不存在用通过我们的认证工具类 转发到对应的认证界面 ssoclientuitl
        SSOClientUtil.redirectToSSOURL(request, response);
        return false;
    }


实现效果

此时redis的keys的状态

基于redis+springboot从零开始设计一个类阿里系的单点登录(2)


这里我们启动三个项目

基于redis+springboot从零开始设计一个类阿里系的单点登录(2)



访问两个客户端


http://www.sso.com:8001/checkLogin?redirectUrl=http://www.tm.com:8003/tmall

http://www.sso.com:8001/checkLogin?redirectUrl=http://www.tb.com:8002/taobao

基于redis+springboot从零开始设计一个类阿里系的单点登录(2)基于redis+springboot从零开始设计一个类阿里系的单点登录(2)




可以看出我们这两个都是没有登陆过的,接下来我们登录其中一个客户端

基于redis+springboot从零开始设计一个类阿里系的单点登录(2)



此时查看redis状态

基于redis+springboot从零开始设计一个类阿里系的单点登录(2)



之后刷新另一个

基于redis+springboot从零开始设计一个类阿里系的单点登录(2)

会发现淘宝客户端登录之后,天猫不需要登录,也可以直接进入首页。