cookies共享 sso_SSO单点登录(test)

5287de63915e366863be0bbcbe42cf9f.png

单点登录

单点登录全称:Single Sign On(SSO),是指在多系统应用群中登录其中一个系统,便可在其他所有系统中得到授权而无需再次登录。

业务介绍

  • 早期单一服务器的用户认证,单点性能压力,无法扩展

5bbd17cc27779c7cd2f87f67f0077070.png
单一服务器
  • Web应用集群:session共享模式,解决了单点性能瓶颈,问题是,跨顶级域名无法访问,cookie中使用JSESSIONID容易被篡改,盗取

af09c5cd4b00e55e7ba7dc2a29680f37.png
session共享模式
  • 分布式SSO模式:解决跨域,用户身份信息独立管理,更好的分布式管理,可以自己扩展安全策略,缺点就是,认证服务器压力较大

45db6dbfb352b6c73c747d6553fc79cd.png
SSO

SSO简单流程图

2cbaa8a2df15a037db7f78c8bac28513.png
SSO

SSO登录流程:

  • 用户访问具体受保护的业务(就是会被拦截的业务)之前,会被拦截器所拦截,这里采用注解式拦截方法(后面说明)。
  • 拦截到认证中心,认证中心只负责两件事(负责token的颁发和对token真伪的验证),如果发现用户没有登录,直接返回到登录页面引导用户进行认证
  • 用户进行登录验证,认证中心调用后台数据库进行验证,验证成功,生成认证token(下面说明生成token),将token存入redis缓存中,实现用户信息的共享。带着token跳转最初的具体业务的地址,如果是带token的跳转,则将token写入Cookie中,并继续打开具体业务功能
  • 用户在获取到认证后的token继续访问其他受保护的业务时,先检查Cookie中是否存在token,如果存在,则继续提交到认证中心验证,认证成功,跳转到具体业务页面

27e1c3153ba63404bc92151125ab286c.png
完整流程

认证中心采取这种架构的原因(认证中心独立出来的好处):实现单点登录

用户的登录认证、用户服务 都独立出来,这么做的好处是,在将来扩展其他业务时,只需要在认证中心完成认证,即可完成具体业务功能,还有一个好处是,在分布式系统中会有多个模块,会出现 跨顶级域名(一级域名)认证,为了突破Cookie中JSESSIONID跨域不能共享的这些限制(redis 也可以实现session共享,太过繁琐),需要将认证中心完全独立出来,降低耦合,放弃 session共享的形式,而是直接采用token的形式,用户第一次认证完成后,在浏览器中定义一个token(包含用户信息),访问具体业务模块时,携带这个token,业务模块将浏览器中的token 和Redis中的token进行对比,数据一致代表认证成功,然后就可以访问具体业务,用token 代替JSESSIONID,把session舍弃,token+redis 实现

生成token

  • 生成token 前先进行用户名密码的核对(数据库校验)
  • 将用户信息加载到redis中 自定义格式存储 如“user:”+(账号,手机,唯一)+“info”
  • 自定义token的生成,采用JWT工具生成
  • 将token 存入redis中一份,实现用户信息共享
  • 由前端重定向用户之前的业务地址,同时把token作为参数附上

JWT工具的介绍及使用:

什么是 JWT -- JSON WEB TOKEN​www.jianshu.com
c095313201e05c952d694c045dc5ff4c.png
/***
 * jwt 制作token
 * @param umsMember 数据库校验后的用户信息
 * @param request 
 * @return
 */
public String makingToken(UmsMember umsMember,HttpServletRequest request){

    //jwt 制作token
    String id = umsMember.getId();
    String nickname = umsMember.getNickname();

    HashMap<String,Object> userMap = new HashMap<>();

    userMap.put("memberId",id);
    userMap.put("nickname",nickname);
    //用户基本信息可以扩展

    String ipAddr = request.getHeader("x-forwarded-for");//通过nginx转发的客户端ip

    if (StringUtils.isBlank(ipAddr)) {

        ipAddr = request.getRemoteAddr();//从request中获取ip

        if (StringUtils.isBlank(ipAddr)) {
            ipAddr = UmsConst.LOCALHOST;
        }
    }


    //按照设计的算法对参数进行加密后生成 token  //ip 作为盐值
    return JwtUtil.encode(UmsConst.TOKEN_KEY, userMap, ipAddr);

}

/**
 * @author panxs
 * @version 1.0
 * @date 2020/2/4 0:16
 */

/**
 * 用jwt实现用户登录的校验(Token 的加密、解密算法)
 *
 * key:   服务器上的公共key
 * param: 用户的基本信息
 * salt:  盐值:可以是 ip 或者 time
 *
 * 三个参数 生成的 token
 */
public class JwtUtil {
    //加密算法
    public static String encode(String key, Map<String,Object> param,String salt){

        if (StringUtils.isNotBlank(salt)) {
            key+=salt;
        }

        JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS256, key);
        jwtBuilder.setClaims(param);
        String token = jwtBuilder.compact();
        return token;

    }
    //解密算法
    public  static Map<String,Object>  decode(String token ,String key,String salt){
        Claims claims=null;
        if (salt!=null){
            key+=salt;
        }
        try {
            claims= Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
        } catch ( JwtException e) {
            return null;
        }
        return  claims;
    }

}

redis中添加token实现用户信息的共享,并设置过期时间

/**
* 生成token 之后往redis中保留一份,实现用户信息的共享
* redis 这里不做说明
*/
public void addUserToken(String token, String id) {
    Jedis jedis = null;
    try {
        jedis = redisUtil.getJedis();
        jedis.setex("user:"+id+":token",60*60*24,token);

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            jedis.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Cookie中添加token

/**
 * @author panxs
 * @date  2020/2/2 16:08
 * @version 1.0
 */
public class CookieUtil {
    /***
     * 获得cookie中的值,
     * @param request
     * @param cookieName
     * @param isDecoder
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null || cookieName == null){
            return null;
        }
        String retValue = null;
        try {
            for (int i = 0; i < cookies.length; i++) {
                if (cookies[i].getName().equals(cookieName)) {
                    if (isDecoder) {//如果涉及中文
                        retValue = URLDecoder.decode(cookies[i].getValue(), "UTF-8");
                    } else {
                        retValue = cookies[i].getValue();
                    }
                    break;
                }
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return retValue;
    }
    /***
     * 设置cookie的值
     * @param request
     * @param response
     * @param cookieName
     * @param cookieValue
     * @param cookieMaxage 失效时间
     * @param isEncode
     */
    public static   void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
        try {
            if (cookieValue == null) {
                cookieValue = "";
            } else if (isEncode) {
                cookieValue = URLEncoder.encode(cookieValue, "utf-8");
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if (cookieMaxage >= 0)
                cookie.setMaxAge(cookieMaxage);
            if (null != request)// 设置域名的cookie
                cookie.setDomain(getDomainName(request));
            // 在域名的根路径下保存
            cookie.setPath("/");
            response.addCookie(cookie);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /***
     * 获得cookie的主域名,本系统为gmall.com,保存时使用
     * @param request
     * @return
     */
    private static final String getDomainName(HttpServletRequest request) {
        String domainName = null;
        String serverName = request.getRequestURL().toString();//获取浏览器地址栏的url
        if (serverName == null || serverName.equals("")) {
            domainName = "";
        } else {
            serverName = serverName.toLowerCase();
            serverName = serverName.substring(7);
            final int end = serverName.indexOf("/");
            serverName = serverName.substring(0, end);
            final String[] domains = serverName.split(".");
            int len = domains.length;
            if (len > 3) {
                // www.xxx.com.cn
                domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
            } else if (len <= 3 && len > 1) {
                // xxx.com or xxx.cn
                domainName = domains[len - 2] + "." + domains[len - 1];
            } else {
                domainName = serverName;
            }
        }
        if (domainName != null && domainName.indexOf(":") > 0) {
            String[] ary = domainName.split(":");
            domainName = ary[0];
        }
        System.out.println("domainName = " + domainName);
        return domainName;
    }
    /***
     * 将cookie中的内容按照key删除
     * @param request
     * @param response
     * @param cookieName
     */
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
        setCookie(request, response, cookieName, null, 0, false);
    }
}

基于注解式的拦截器(自定义拦截)

/**
 * @author panxs
 * @version 1.0
 * @date 2020/2/3 20:23
 */
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {


    /*
    *拦截器的配置
    *
    * 第一类方法:不需要进行拦截(没有拦截器注解@LoginRequired),直接放行
    *
    *
    * 第二类方法:需要拦截但是拦截校验失败(用户没有登陆或者登陆过期了)也可以继续访问的方法,比如说购物车中的所有方法
    *
    *
    * 第三类方法:需要拦截,并且拦截校验一定要通过(用户登录成功了) 才能访问的方法
    *
    *
    * 访问没有认证的方法 会被拦截到认证中心 如果没有登录 会被转到认证中心 , 然后再认证中心 登录 之后会重新访问原来访问的方法(做了一次重定向)
    *
    */

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //拦截代码

        //判断被拦截的请求的访问的方法的注解(是否是需要拦截的)
        //利用反射获取该方法的所有信息
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        LoginRequired methodAnnotation = handlerMethod.getMethodAnnotation(LoginRequired.class);


        //是否拦截拦截
        if (methodAnnotation==null) {

            return  true; //不需要拦截
        }

        /**
         * token 的 四种情况
         *                    oldToken NULL       oldToken NOTNUll
         *
         * newToken NULL        从未登录过             之前登录过
         *
         * newToken NOTNULL      刚刚登录               过期
         *
         *
         */

        String token ="";


        String oldToken = CookieUtil.getCookieValue(request, "oldToken", true);

        if (StringUtils.isNotBlank(oldToken)) {

            token = oldToken;

        }

        String newToken = request.getParameter("token");

        if (StringUtils.isNotBlank(newToken)) {

            token = newToken;

        }

        //获取该请求是否必须要登录
        boolean b = methodAnnotation.loginSuccess();

        //先验证
        String status=UmsConst.STATUS_FAIL;
        Map<String, String> statusMap = new HashMap<>();
        if (StringUtils.isNotBlank(token)) {
            //调用验证中心进行验证
            String addr = request.getHeader("x-forwarded-for");//通过nginx转发的客户端ip
            if (StringUtils.isBlank(addr)) {

                addr = request.getRemoteAddr();//从request中获取ip
                if (StringUtils.isBlank(addr)) {

                    addr = UmsConst.LOCALHOST;
                }

            }

            /**
             * 注意:
             *
             * 这里会产生一个新的request
             */
            String statusJson = HttpClientUtil.doGet("http://auth.qmall.com:8882/auth/verification?token=" + token+"&currentIp="+addr);

            statusMap = JSON.parseObject(statusJson, Map.class);

            status = statusMap.get("status");

        }


        if (b) {
            //必须登录才能使用
            //String oldToken = CookieUtil.getCookieValue(request, "oldToken", true);
            //request.getParameter("token");

            if (!UmsConst.STATUS_SUCCESS.equals(status)) {

                //重定向到认证中心登录
                response.sendRedirect("http://auth.qmall.com:8882/auth/index?ReturnUrl="+request.getRequestURL());
                return false;

            }



            //需要将 token携带的用户信息 写入
            request.setAttribute("memberId",statusMap.get("memberId"));
            request.setAttribute("nickname",statusMap.get("nickname"));
            //验证成功 覆盖/设置cookie中的token
            if (StringUtils.isNotBlank(token)){

                CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true);
            }


        }else{
            //不需要登录也能使用的模块 但是必须要验证
            if (UmsConst.STATUS_SUCCESS.equals(status)) {

                //需要将 token携带的用户信息 写入
                request.setAttribute("memberId",statusMap.get("memberId"));
                request.setAttribute("nickname",statusMap.get("nickname"));
            }

            //验证成功 覆盖cookie中的token
            if (StringUtils.isNotBlank(token)){

                CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true);
            }
        }



        return true;  //要拦截

    }
}

自定义注解

/**
 * @author panxs
 * @version 1.0
 * @date 2020/2/3 20:46
 */

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 拦截器的标识
 */
@Target(ElementType.METHOD)  //只在方法上生效
@Retention(RetentionPolicy.RUNTIME)  //生效范围 , 在虚拟机运行时也生效
public @interface LoginRequired {

    boolean loginSuccess() default true;

}

2020年3月4日14:22:37 panxs

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值