单点登录 订阅
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 展开全文
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
信息
外文名
Single Sign On
简    称
SSO
使用场景
运维内控审计系统、4A系统
中文名
单点登录
功    能
企业内部登录系统
解    释
企业业务整合的解决方案
单点登录概述
很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影、豆瓣日记......真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?一次注册。 一次注册不难,想一下是不是只要Server之间同步用户信息就行了?可以,但这样描述不太完整,后续讲用户注册的时候详细说。实际上用户信息的管理才是SSO真正的难点,只是作为初学者,我们的难点在于实现SSO的技术!我们先讨论实现手段。一次登录与一次退出。 回头看看普通商场的故事,什么东西才是保持登录状态关键的东西?记录器(session)?那种叫做cookie的纸张?写在纸张上的ID? 是session里面记录的信息跟那个ID,cookie不只是记录ID的工具而已。客户端持有ID,服务端持有session,两者一起用来保持登录状态。客户端需要用ID来作为凭证,而服务端需要用session来验证ID的有效性(ID可能过期、可能根本就是伪造的找不到对应的信息、ID下对应的客户端还没有进行登录验证等)。但是session这东西一开始是每个server自己独有的,豆瓣FM有自己的session、豆瓣读书有自己的session,而记录ID的cookie又是不能跨域的。所以,我们要实现一次登录一次退出,只需要想办法让各个server的共用一个session的信息,让客户端在各个域名下都能持有这个ID就好了。再进一步讲,只要各个server拿到同一个ID,都能有办法检验出ID的有效性、并且能得到ID对应的用户信息就行了,也就是能检验ID [1]  。
收起全文
精华内容
下载资源
问答
  • 基于springboot几种实现单点登录的方式都有:1.用redis缓存实现2.基于jwt实现sso3.基于spring-security oauth实现单点登录4.基于shiro实现单点登录
  • 利用springMvc 实现的简单的单点登录Demo,内含三个小Mavn项目分别是 1、认证中心SSOServer 2、子系统1SSOClient1 3、子系统2SSOClient2 文章请参考 http://blog.csdn.net/qq_31183297/article/details/79419222
  • springBoot单点登录实例

    2018-04-02 10:44:43
    该项目为springBoot单点登录实例,其中包含了权限设置。前端为HTML写的简单示例。开发工具为IDEA,启动项目根据模块划分,1、mysql-->MysqlTest(创建数据库实例,根据实际情况可自行更改,)。2、web1。2、web2
  • Cas的全称是Centeral Authentication Service,是对单点登录SSO(Single Sign On)的一种实现。其由Cas Server和Cas Client两部分组成,Cas Server是核心,而Cas Client通常就对应于我们的应用。一个Cas Server可以...
  • 单点登录实现源码

    2017-11-30 18:07:23
    单点登录实现源码(内含三个工程,一个认证系统+两个子系统) 参考博客 http://blog.csdn.net/qq_23994787/article/details/78678930
  • 慕课java实现sso单点登录自己实现的代码,基于Struts2,本课程首先以新浪为例介绍了单点登录SSO的实现场景,然后对实现SSO所用的核心技术和原理进行分析,最后通过案例演示了同域SSO和跨域SSO的实现。
  • 公司需要做统一的sso单点登录,由于CAS过于重量级和复杂。所以就自己基于springMVC mybatis redis缓存实现了SSO单点登录。mark一下!
  • 基于JWT实现的单点登录系统,使用idea开发的,可以供学习参考,有什么问题可以咨询
  • 单点登录

    千次阅读 2017-01-12 22:29:41
    1:什么事单点登录: SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的...

    1:什么事单点登录:
    SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。

    2:为什么需要单点登录:
    应为有些公司有好多业务可以使用同一个账户登录,所以需要单点登录

    3:传统的登录流程
    传统登陆流程图

    4:传统流程出现的问题
    部署到集群上会出现问题,主要是session不能共享的问题

    5:解决方法:
    1):可以使用tomcat的共享session,在tomcat里面配置就可以了,每个tomcat服务器都会不断的广播自己的session的,但是集群多的话广播session占用的资源比较多;不能解决分布式工程的session共享问题。例如支付宝和淘宝单点登录的问题
    2):把session保存到redis里面,使用同一个redis服务器来实现session的共享

    6:单点登录流程图
    单点登录流程图

    ps:单点登录需要做成一个webservice来使用

    单点登录一般需要实现如下功能:
    a)数据校验接口
    b)注册接口
    c)登录接口
    d)根据token查询用户接口
    e)安全退出(作业)

    展开全文
  • java 单点登录源代码

    热门讨论 2014-11-10 15:48:44
    java web单点登录源代码,一个验证系统,两个测试系统
  • SSO 单点登录系统 +源码 +部署文档+架构图+权限管,包含项目数据库
  • 通过redis(缓存)实现单点登录

    热门讨论 2015-05-19 16:45:44
    跨应用访问 通过redis实现单点登录。即保证安全又减少多次输入密码的繁琐。
  • 实现OAuth2授权,并且实现单点登录的小例子,请使用vs2015打开
  • 基于Spring Boot、Mybatis、Redis的SSO单点登录系统demo,内含一个母系统,2个子测试系统,也可以当成整合demo来学习
  • Spring Cloud 安全:集成OAuth2实现身份认证和单点登录 示例代码
  • cas单点登录demo

    千次下载 热门讨论 2015-03-02 10:23:50
    cas单点登录的例子程序,运行见说明
  • struts2+spring+cas单点登录

    热门讨论 2013-09-13 15:32:22
    使用struts2+spring+cas实现的单点登录功能,里面包括cas-server3.5.2项目一个,cas-client3.2.1 web项目两个,数据库脚本,请按照里面的说明文档进行部署,希望你们也能配置成功。
  • 单点登录DEMO

    千次下载 热门讨论 2014-03-04 21:37:50
    参见http://blog.csdn.net/ghsau/article/details/20466351
  • NC单点登录测试用例

    2013-04-01 22:47:49
    NC单点登录测试用例: 使用http GET或者POST方式,向NC servlet提交如下信息
  • 单点登录sso的原理与java实现详细讲解与示例(文档与源码)
  • 完全跨域单点登录DEMO

    千次下载 热门讨论 2014-03-05 15:39:36
    详情见http://blog.csdn.net/ghsau/article/details/20545513
  • web-sso单点登录源码

    热门讨论 2014-10-29 15:14:58
    单点登录(SSO)的技术被越来越广泛地运用到各个领域的软件系统当中。本文从业务的角度分析了单点登录的需求和应用领域;从技术本身的角度分析了单点登录技术的内部机制和实现手段,并且给出Web-SSO和桌面SSO的实现...
  • SSO单点登录方案大全

    千次阅读 2019-11-08 09:12:50
    分布式微服务系统主流常用的登录方案 前言: 单点登录其实是一个概念,主要是为了解决一次登录,多系统(本系统或外部系统)之间不需要重复登录的问题,就目前来说,主流的解决方案针对业务场景分为3个方向:1: 同一公司,同...

    分布式微服务系统主流常用的登录方案

    前言: 单点登录其实是一个概念,主要是为了解决一次登录,多系统(本系统或外部系统)之间不需要重复登录的问题,就目前来说,主流的解决方案针对业务场景分为3个方向:

    1: 同一公司,同父域下的单点登录解决方案.

    如[map.baidu.com] [baidu.com] [image.baidu.com]

    基于cookie开源项目代表: JWT(jwt.io/);会详细介绍和实现;

    2: 同一公司,不同域下的单点登录解决方案.

    如[taobao.com] [tmall.com]

    基于中央认证服务器开源项目代表:CAS(github.com/apereo/cas); apereo.org/projects/cas

    3: 不同公司之间,不同域下的 第三方登录功能实现.

    如第三方网站支持qq登录,微信登录,微博登录等;

    基于OAuth2.0协议各大公司自己的支持;



    一: 基于JWT的单点登录解决方案:

    首先我们看一下图形,这个就是基于JWT解决方案的单点登录解决方案;


    应用和原理 : 什么是JWT,什么业务场景使用JWT?

    JWT: <https://jwt.io/>
    JSON Web Tokens are an open, industry standard RFC 7519 method for representing **claims** securely between two parties.
    JSONWeb令牌是一种开放的、行业标准的RFC7519方法,用于在双方之间安全地表示**声明**。

    JWT 应用场景:

    Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用,JWT 省去了服务器存储用户信息的过程。

    Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。


    JWT的令牌结构 看官方文档jwt.io/introduction/ 并解释

    JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:Header,Payload,Signature。

    官网对于这个三部分的定义。一个典型的JWT Token是 tttttttt.yyyyyyyyyyy.eeeeeee 这样组成。




    Header : 由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。然后,用Base64对这个JSON编码就得到JWT的第一部分。


    Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。分为三种类型

    1:registered claims。标准声明。一般常用于校验的有 iat,exp,nbf 校验 token 是否过期。

    /*
    iss: jwt签发者
    sub: jwt所面向的用户
    aud: 接收jwt的一方
    exp: jwt的过期时间,这个过期时间必须要大于签发时间
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
    */

    2:public claims。公开声明。 可以存放任意信息,比如userid等。不是信息安全的。

    3:private claims。私密声明。 双方约定的信息。

    Signature Base64(header).Base64(payload) + head中定义的算法 + 密钥 生成一个字符串 。


    使用方式:

    客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

    此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

    // Authorization: Bearer <token>

    另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。


    通过代码实现JWT之单点登录(基于springboot+dubbo+zookeeper)


    代码实现JWT服务器(基于dubbo实现):

    JWTService:

    package com.qianfeng.user;

    import com.qianfeng.user.dto.UserRequest;
    import com.qianfeng.user.dto.UserResponse;

    /**
    * 用户服务对外接口
    *
    * @author Martin
    *
    */
    public interface IUserService {


    /**
    * 用户登录接口
    * @param request user info
    * @return 查询到的结果
    */
    UserResponse login(UserRequest request);


    }

    JWT ServiceImpl:


    package com.qianfeng.user;

    import com.qianfeng.user.dao.entity.UserEntity;
    import com.qianfeng.user.dao.mapper.UserMapper;
    import com.qianfeng.user.dto.UserRequest;
    import com.qianfeng.user.dto.UserResponse;
    import com.qianfeng.user.util.JWTTokenUtil;
    import org.apache.commons.lang3.StringUtils;
    import org.joda.time.DateTime;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.util.HashMap;
    import java.util.Map;

    /**
    *
    * 用户业务实现类
    * @author Martin
    */
    @Service("userServiceImpl")
    public class UserServiceImpl implements IUserService{

    @Autowired
    private UserMapper userMapper;

    /**
    * 用户登录方法实现
    * @param request 请求参数
    * @return 登录结果
    */
    public UserResponse login(UserRequest request) {

    //非空校验
    UserResponse ur = verification(request);
    if(null != ur){
    return ur;
    }

    UserEntity ue = new UserEntity();
    ue.setUserName(request.getUserName());
    ue.setUserPass(request.getUserPass());

    //执行登录
    UserEntity user = userMapper.login(ue);

    ur = new UserResponse();

    if(null == user){ //登录失败
    ur.setCode(ErrorCode.CODE_LOGIN_FAIL);
    ur.setMsg(ErrorCode.MSG_LOGIN_FAIL);
    return ur;
    }

    //获取token
    Map<String,Object> map=new HashMap<>();
    map.put("uid",user.getUserID());
    map.put("exp", DateTime.now().plusSeconds(30).toDate().getTime()/1000); //设置过期时间 当前时间的基础上加上30s

    String token = JWTTokenUtil.generatorToken(map); //通过封装的算法来获取token
    ur.setToken(token);


    ur.setUid(user.getUserID());//将一些用户信息返回,可以封装一个通用类

    return ur;
    }

    //非空校验
    private UserResponse verification(UserRequest request){
    UserResponse ur = null;

    //请求为空
    if(null == request){
    ur = new UserResponse();
    ur.setCode(ErrorCode.CODE_ERROR_SYSTEM);
    ur.setMsg(ErrorCode.MSG_ERROR_SYSTEM);
    return ur;
    }

    //用户名为空
    if(StringUtils.isEmpty(request.getUserName())){
    ur = new UserResponse();
    ur.setCode(ErrorCode.CODE_USER_EMPTY);
    ur.setMsg(ErrorCode.MSG_USER_EMPTY);
    return ur;
    }

    //密码为空
    if(StringUtils.isEmpty(request.getUserPass())){
    ur = new UserResponse();
    ur.setCode(ErrorCode.CODE_PASS_EMPTY);
    ur.setMsg(ErrorCode.MSG_PASS_EMPTY);
    return ur;
    }

    return ur;
    }
    }

    JWT Util:

    package com.qianfeng.user.util;

    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jws;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;

    import javax.crypto.spec.SecretKeySpec;
    import javax.xml.bind.DatatypeConverter;
    import java.security.Key;
    import java.util.Map;

    /**
    * JWT 生成token和提取token信息工具类
    * @author Martin
    */
    public class JWTTokenUtil {

    static SignatureAlgorithm sa = SignatureAlgorithm.HS256;//使用hs256算法

    //获取key
    private static Key generatorKey(){
    byte[] bin=DatatypeConverter.parseBase64Binary("2521afcf18c749c1a8a7615c03d15e43");
    Key key=new SecretKeySpec(bin,sa.getJcaName());
    return key;
    }

    /**
    * 将传过来的信息按照 Header,Payload,Signature 的方式组装一个字符串
    * @param payLoad
    * @return
    */
    public static String generatorToken(Map<String,Object> payLoad){
    ObjectMapper objectMapper=new ObjectMapper();

    try {

    return Jwts.builder().setPayload(objectMapper.writeValueAsString(payLoad))
    .signWith(sa,generatorKey()).compact();

    } catch (JsonProcessingException e) {

    e.printStackTrace();
    }
    return null;
    }


    /**
    * 将token解析,得到Payload内容
    * @param token
    * @return
    */
    public static Claims phaseToken(String token){

    //将token解析成claims
    Jws<Claims> jws = Jwts.parser().setSigningKey(generatorKey()).parseClaimsJws(token);

    // jws.getHeader(); Header
    // jws.getBody(); Payload
    // jws.getSignature(); Signature


    return jws.getBody();
    }

    }


    代码实现JWT客户端请求:

    tocken校验拦截器:

    package com.qianfeng.conf.interceptor;

    import com.qianfeng.user.anno.Evade;
    import com.qianfeng.user.util.CookieUtil;
    import com.qianfeng.user.util.JWTTokenUtil;
    import com.qianfeng.user.web.BaseController;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.SignatureException;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.lang.reflect.Method;

    /**
    * 拦截器,如果没有token,直接返回结果。
    * @author Martin
    */
    public class TokenInterceptor implements HandlerInterceptor {


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

    if(handler instanceof ResourceHttpRequestHandler || handler instanceof AbstractErrorController){
    return true;
    }

    System.out.println("start");

    HandlerMethod handlerMethod = (HandlerMethod)handler;

    //如果需要规避
    if(hasEvade(handlerMethod)){
    System.out.println("evade : " +handlerMethod.toString());
    return true;
    }

    //从cookie中获取token
    String access_token = CookieUtil.getCookieValue(request, "access_token");

    //如果没有token,跳转到登录页面,这里需要判断一下是否是ajax请求
    if(StringUtils.isEmpty(access_token)){
    System.out.println("token is null");
    return vafail(request,response);
    }

    Claims claims = null;
    try{
    claims = JWTTokenUtil.phaseToken(access_token); //解析token
    }catch (ExpiredJwtException e){ //token已经过期
    System.out.println("token out time");
    return vafail(request,response);
    }catch (SignatureException e1){ //签名校验失败
    System.out.println("token signature fail");
    return vafail(request,response);
    }

    if(null != claims && !StringUtils.isEmpty(claims.get("uid").toString())){

    System.out.println("token success");

    //获取id,并将id设置到Controller

    if(handlerMethod.getBean() instanceof BaseController){
    BaseController bean = (BaseController)handlerMethod.getBean();
    bean.setUserID(claims.get("uid").toString());
    }
    return true;
    }

    System.out.println("end");
    return false;
    }

    private boolean vafail(HttpServletRequest request,HttpServletResponse response) throws Exception{
    if(CookieUtil.isAjax(request)){
    response.getWriter().println("{'result':'6666','msg':'no token'}");
    return false;
    }
    response.sendRedirect("login");
    return false;
    }


    /**
    * 验证是否可以规避,不需要拦截
    * @param handlerMethod
    * @return
    */
    private boolean hasEvade(HandlerMethod handlerMethod){

    Method method = handlerMethod.getMethod();

    return method.getAnnotation(Evade.class) != null; //判断方法上面是否有自定义的规避注解。

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    // TODO
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    // TODO
    }
    }

    LoginController登录实现代码:

    package com.qianfeng.user.web;

    import com.alibaba.dubbo.config.annotation.Reference;
    import com.qianfeng.user.anno.Evade;
    import com.qianfeng.user.dto.UserRequest;
    import com.qianfeng.user.dto.UserResponse;
    import com.qianfeng.user.IUserService;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    /**
    * 登录
    * @author Martin
    */
    @Controller
    public class LoginController extends BaseController{

    @Reference //通过dubbo注入实现类
    IUserService userService;


    @GetMapping("/login")
    @Evade //自定义注解,配置该注解不拦截请求;
    public String jumpLogin(){
    return "login";
    }


    @RequestMapping(value = "/dologin")
    @Evade
    public String doLogin(UserRequest ur, HttpServletRequest request, HttpServletResponse response){

    UserRequest res = new UserRequest(ur.getUserName(),ur.getUserPass());

    if(null == userService){ //dubbo服务不可用
    return "error";
    }

    UserResponse rp = userService.login(res); //通过dubbo调用用户服务进行登录

    //登录失败跳转到登录页面
    if(null == response || StringUtils.isEmpty(rp.getUid())){
    return "login";
    }

    //按照以前,如果登录成功,是需要将用户信息保存到session中。
    request.getSession().setAttribute("user",rp);
    request.setAttribute("uid",rp.getUid());

    //使用token的方式,将信息保存到客户端 access_token : 注意,这里在获取的时候也需要这个值
    //response.addHeader("Set-Cookie","access_token="+rp.getToken()+";Path=/;HttpOnly"); //根目录的 cookie 设置token

    return "index";
    }

    //用来测试,是否可以直接访问资源
    @RequestMapping(value = "/buy")
    public String buy(HttpServletRequest request){

    //从Session中获取用户信息
    UserResponse ur = (UserResponse)request.getSession().getAttribute("user");
    System.out.println("Session中: /buy : id " + ur.getUid());

    // System.out.println("Token中:/buy : id " + getUserID());
    // request.setAttribute("uid",getUserID());

    return "info";
    }

    }

    Evade 自定义注解配置不需要验证的请求:

    package com.qianfeng.user.anno;

    import java.lang.annotation.*;

    /**
    * 自定义注解,该注解可以设置不拦截处理
    * @author Martin
    */
    @Documented
    @Inherited
    @Target({ElementType.METHOD,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Evade {

    }


    以上为主要实现代码




    二: 基于CAS同公司不同域的单点登录解决方案

    Yelu大学研发的CAS单点登录原理:





    主要步骤分为:

    1:用户请求CAS Client的某一个网站,或者直接请求CAS Server登录页面;

    2:用户到CAS Server的登录页面进行验证,验证成功,CAS Server返回ticket到客户端;

    3:客户端再次请求CAS Client资源, CAS Client携带ticket到CAS Server认证,成功则继续访问;


    实现步骤:

    1: 搭建CAS Server端,可使用CAS的框架实现;可自定义登录页面;实现其内部接口即可;

    2: 搭建CAS Client端,就是公司不同的服务器系统,携带秘钥即可;


    三: 基于Auth2.0之第三方登录

    OAuth 2.0是用于**授权**的行业标准协议。
    OAuth 2.0 is the industry-standard protocol for **authorization**。
    OAuth定义了四个角色:
    资源所有者
    能够授予对受保护资源的访问权限的实体。
    当资源所有者是一个人时,它被称为
    最终用户。

    资源服务器
    托管受保护资源的服务器,能够接受
    并使用访问令牌响应受保护的资源请求。

    客户
    代表受保护的资源请求的应用程序
    资源所有者及其授权。“客户”一词的确如此
    并不意味着任何特定的实施特征(例如,
    应用程序是在服务器,桌面还是其他服务器上执行
    设备)。

    授权服务器
    服务器成功后向客户端发出访问令牌
    验证资源所有者并获得授权。

    #### 授权模式

    1 简化模式(Implicit)

    2 授权码模式(Authorization Code)

    3 密码模式(Resource Owner Password Credentials Grant)

    4 客户端模式(Client Credentials)


    场景: 公司的网站支持第三方登录(qq登录,微信登录,微博登录):

    qq开放平台:connect.qq.com/index.ht

    微博开放平台: open.weibo.com/authenti

    接入方式按照对应的平台操作即可,第三方网站接入文档提示非常全面.



    关于登录,还请题主需要关注一下 分布式下登录成功以后,分布式Session的问题以及解决方案;

    方案是用来解决问题的,这些方案没有好不好的说法,只有合不合适的说法,合适的才是最好的。分布式微服务系统主流常用的登录方案

    前言: 单点登录其实是一个概念,主要是为了解决一次登录,多系统(本系统或外部系统)之间不需要重复登录的问题,就目前来说,主流的解决方案针对业务场景分为3个方向:

    1: 同一公司,同父域下的单点登录解决方案.

    如[map.baidu.com] [baidu.com] [image.baidu.com]

    基于cookie开源项目代表: JWT(jwt.io/);会详细介绍和实现;

    2: 同一公司,不同域下的单点登录解决方案.

    如[taobao.com] [tmall.com]

    基于中央认证服务器开源项目代表:CAS(github.com/apereo/cas); apereo.org/projects/cas

    3: 不同公司之间,不同域下的 第三方登录功能实现.

    如第三方网站支持qq登录,微信登录,微博登录等;

    基于OAuth2.0协议各大公司自己的支持;


    一: 基于JWT的单点登录解决方案:

    首先我们看一下图形,这个就是基于JWT解决方案的单点登录解决方案;



    应用和原理 : 什么是JWT,什么业务场景使用JWT?

    JWT: <jwt.io/>
    JSON Web Tokens are an open, industry standard RFC 7519 method for representing **claims** securely between two parties.
    JSONWeb令牌是一种开放的、行业标准的RFC7519方法,用于在双方之间安全地表示**声明**。

    JWT 应用场景:

    Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用,JWT 省去了服务器存储用户信息的过程。

    Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。


    JWT的令牌结构 看官方文档jwt.io/introduction/ 并解释

    JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:Header,Payload,Signature。

    官网对于这个三部分的定义。一个典型的JWT Token是 tttttttt.yyyyyyyyyyy.eeeeeee 这样组成。




    Header : 由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。然后,用Base64对这个JSON编码就得到JWT的第一部分。


    Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。分为三种类型

    1:registered claims。标准声明。一般常用于校验的有 iat,exp,nbf 校验 token 是否过期。

    /*
    iss: jwt签发者
    sub: jwt所面向的用户
    aud: 接收jwt的一方
    exp: jwt的过期时间,这个过期时间必须要大于签发时间
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
    */

    2:public claims。公开声明。 可以存放任意信息,比如userid等。不是信息安全的。

    3:private claims。私密声明。 双方约定的信息。

    Signature Base64(header).Base64(payload) + head中定义的算法 + 密钥 生成一个字符串 。


    使用方式:

    客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

    此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

    // Authorization: Bearer <token>

    另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。


    通过代码实现JWT之单点登录(基于springboot+dubbo+zookeeper)


    代码实现JWT服务器(基于dubbo实现):

    JWTService:

    package com.qianfeng.user;

    import com.qianfeng.user.dto.UserRequest;
    import com.qianfeng.user.dto.UserResponse;

    /**
    * 用户服务对外接口
    *
    * @author Martin
    *
    */
    public interface IUserService {


    /**
    * 用户登录接口
    * @param request user info
    * @return 查询到的结果
    */
    UserResponse login(UserRequest request);


    }

    JWT ServiceImpl:


    package com.qianfeng.user;

    import com.qianfeng.user.dao.entity.UserEntity;
    import com.qianfeng.user.dao.mapper.UserMapper;
    import com.qianfeng.user.dto.UserRequest;
    import com.qianfeng.user.dto.UserResponse;
    import com.qianfeng.user.util.JWTTokenUtil;
    import org.apache.commons.lang3.StringUtils;
    import org.joda.time.DateTime;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.util.HashMap;
    import java.util.Map;

    /**
    *
    * 用户业务实现类
    * @author Martin
    */
    @Service("userServiceImpl")
    public class UserServiceImpl implements IUserService{

    @Autowired
    private UserMapper userMapper;

    /**
    * 用户登录方法实现
    * @param request 请求参数
    * @return 登录结果
    */
    public UserResponse login(UserRequest request) {

    //非空校验
    UserResponse ur = verification(request);
    if(null != ur){
    return ur;
    }

    UserEntity ue = new UserEntity();
    ue.setUserName(request.getUserName());
    ue.setUserPass(request.getUserPass());

    //执行登录
    UserEntity user = userMapper.login(ue);

    ur = new UserResponse();

    if(null == user){ //登录失败
    ur.setCode(ErrorCode.CODE_LOGIN_FAIL);
    ur.setMsg(ErrorCode.MSG_LOGIN_FAIL);
    return ur;
    }

    //获取token
    Map<String,Object> map=new HashMap<>();
    map.put("uid",user.getUserID());
    map.put("exp", DateTime.now().plusSeconds(30).toDate().getTime()/1000); //设置过期时间 当前时间的基础上加上30s

    String token = JWTTokenUtil.generatorToken(map); //通过封装的算法来获取token
    ur.setToken(token);


    ur.setUid(user.getUserID());//将一些用户信息返回,可以封装一个通用类

    return ur;
    }

    //非空校验
    private UserResponse verification(UserRequest request){
    UserResponse ur = null;

    //请求为空
    if(null == request){
    ur = new UserResponse();
    ur.setCode(ErrorCode.CODE_ERROR_SYSTEM);
    ur.setMsg(ErrorCode.MSG_ERROR_SYSTEM);
    return ur;
    }

    //用户名为空
    if(StringUtils.isEmpty(request.getUserName())){
    ur = new UserResponse();
    ur.setCode(ErrorCode.CODE_USER_EMPTY);
    ur.setMsg(ErrorCode.MSG_USER_EMPTY);
    return ur;
    }

    //密码为空
    if(StringUtils.isEmpty(request.getUserPass())){
    ur = new UserResponse();
    ur.setCode(ErrorCode.CODE_PASS_EMPTY);
    ur.setMsg(ErrorCode.MSG_PASS_EMPTY);
    return ur;
    }

    return ur;
    }
    }

    JWT Util:

    package com.qianfeng.user.util;

    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jws;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;

    import javax.crypto.spec.SecretKeySpec;
    import javax.xml.bind.DatatypeConverter;
    import java.security.Key;
    import java.util.Map;

    /**
    * JWT 生成token和提取token信息工具类
    * @author Martin
    */
    public class JWTTokenUtil {

    static SignatureAlgorithm sa = SignatureAlgorithm.HS256;//使用hs256算法

    //获取key
    private static Key generatorKey(){
    byte[] bin=DatatypeConverter.parseBase64Binary("2521afcf18c749c1a8a7615c03d15e43");
    Key key=new SecretKeySpec(bin,sa.getJcaName());
    return key;
    }

    /**
    * 将传过来的信息按照 Header,Payload,Signature 的方式组装一个字符串
    * @param payLoad
    * @return
    */
    public static String generatorToken(Map<String,Object> payLoad){
    ObjectMapper objectMapper=new ObjectMapper();

    try {

    return Jwts.builder().setPayload(objectMapper.writeValueAsString(payLoad))
    .signWith(sa,generatorKey()).compact();

    } catch (JsonProcessingException e) {

    e.printStackTrace();
    }
    return null;
    }


    /**
    * 将token解析,得到Payload内容
    * @param token
    * @return
    */
    public static Claims phaseToken(String token){

    //将token解析成claims
    Jws<Claims> jws = Jwts.parser().setSigningKey(generatorKey()).parseClaimsJws(token);

    // jws.getHeader(); Header
    // jws.getBody(); Payload
    // jws.getSignature(); Signature


    return jws.getBody();
    }

    }


    代码实现JWT客户端请求:

    tocken校验拦截器:

    package com.qianfeng.conf.interceptor;

    import com.qianfeng.user.anno.Evade;
    import com.qianfeng.user.util.CookieUtil;
    import com.qianfeng.user.util.JWTTokenUtil;
    import com.qianfeng.user.web.BaseController;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.SignatureException;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.lang.reflect.Method;

    /**
    * 拦截器,如果没有token,直接返回结果。
    * @author Martin
    */
    public class TokenInterceptor implements HandlerInterceptor {


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

    if(handler instanceof ResourceHttpRequestHandler || handler instanceof AbstractErrorController){
    return true;
    }

    System.out.println("start");

    HandlerMethod handlerMethod = (HandlerMethod)handler;

    //如果需要规避
    if(hasEvade(handlerMethod)){
    System.out.println("evade : " +handlerMethod.toString());
    return true;
    }

    //从cookie中获取token
    String access_token = CookieUtil.getCookieValue(request, "access_token");

    //如果没有token,跳转到登录页面,这里需要判断一下是否是ajax请求
    if(StringUtils.isEmpty(access_token)){
    System.out.println("token is null");
    return vafail(request,response);
    }

    Claims claims = null;
    try{
    claims = JWTTokenUtil.phaseToken(access_token); //解析token
    }catch (ExpiredJwtException e){ //token已经过期
    System.out.println("token out time");
    return vafail(request,response);
    }catch (SignatureException e1){ //签名校验失败
    System.out.println("token signature fail");
    return vafail(request,response);
    }

    if(null != claims && !StringUtils.isEmpty(claims.get("uid").toString())){

    System.out.println("token success");

    //获取id,并将id设置到Controller

    if(handlerMethod.getBean() instanceof BaseController){
    BaseController bean = (BaseController)handlerMethod.getBean();
    bean.setUserID(claims.get("uid").toString());
    }
    return true;
    }

    System.out.println("end");
    return false;
    }

    private boolean vafail(HttpServletRequest request,HttpServletResponse response) throws Exception{
    if(CookieUtil.isAjax(request)){
    response.getWriter().println("{'result':'6666','msg':'no token'}");
    return false;
    }
    response.sendRedirect("login");
    return false;
    }


    /**
    * 验证是否可以规避,不需要拦截
    * @param handlerMethod
    * @return
    */
    private boolean hasEvade(HandlerMethod handlerMethod){

    Method method = handlerMethod.getMethod();

    return method.getAnnotation(Evade.class) != null; //判断方法上面是否有自定义的规避注解。

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    // TODO
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    // TODO
    }
    }

    LoginController登录实现代码:

    package com.qianfeng.user.web;

    import com.alibaba.dubbo.config.annotation.Reference;
    import com.qianfeng.user.anno.Evade;
    import com.qianfeng.user.dto.UserRequest;
    import com.qianfeng.user.dto.UserResponse;
    import com.qianfeng.user.IUserService;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    /**
    * 登录
    * @author Martin
    */
    @Controller
    public class LoginController extends BaseController{

    @Reference //通过dubbo注入实现类
    IUserService userService;


    @GetMapping("/login")
    @Evade //自定义注解,配置该注解不拦截请求;
    public String jumpLogin(){
    return "login";
    }


    @RequestMapping(value = "/dologin")
    @Evade
    public String doLogin(UserRequest ur, HttpServletRequest request, HttpServletResponse response){

    UserRequest res = new UserRequest(ur.getUserName(),ur.getUserPass());

    if(null == userService){ //dubbo服务不可用
    return "error";
    }

    UserResponse rp = userService.login(res); //通过dubbo调用用户服务进行登录

    //登录失败跳转到登录页面
    if(null == response || StringUtils.isEmpty(rp.getUid())){
    return "login";
    }

    //按照以前,如果登录成功,是需要将用户信息保存到session中。
    request.getSession().setAttribute("user",rp);
    request.setAttribute("uid",rp.getUid());

    //使用token的方式,将信息保存到客户端 access_token : 注意,这里在获取的时候也需要这个值
    //response.addHeader("Set-Cookie","access_token="+rp.getToken()+";Path=/;HttpOnly"); //根目录的 cookie 设置token

    return "index";
    }

    //用来测试,是否可以直接访问资源
    @RequestMapping(value = "/buy")
    public String buy(HttpServletRequest request){

    //从Session中获取用户信息
    UserResponse ur = (UserResponse)request.getSession().getAttribute("user");
    System.out.println("Session中: /buy : id " + ur.getUid());

    // System.out.println("Token中:/buy : id " + getUserID());
    // request.setAttribute("uid",getUserID());

    return "info";
    }

    }

    Evade 自定义注解配置不需要验证的请求:

    package com.qianfeng.user.anno;

    import java.lang.annotation.*;

    /**
    * 自定义注解,该注解可以设置不拦截处理
    * @author Martin
    */
    @Documented
    @Inherited
    @Target({ElementType.METHOD,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Evade {

    }


    以上为主要实现代码,如需全部工程代码请加QQ : 313829864




    二: 基于CAS同公司不同域的单点登录解决方案

    Yelu大学研发的CAS单点登录原理:





    主要步骤分为:

    1:用户请求CAS Client的某一个网站,或者直接请求CAS Server登录页面;

    2:用户到CAS Server的登录页面进行验证,验证成功,CAS Server返回ticket到客户端;

    3:客户端再次请求CAS Client资源, CAS Client携带ticket到CAS Server认证,成功则继续访问;

    实现步骤:

    1: 搭建CAS Server端,可使用CAS的框架实现;可自定义登录页面;实现其内部接口即可;

    2: 搭建CAS Client端,就是公司不同的服务器系统,携带秘钥即可;


    三: 基于Auth2.0之第三方登录

    OAuth 2.0是用于**授权**的行业标准协议。
    OAuth 2.0 is the industry-standard protocol for **authorization**。
    OAuth定义了四个角色:
    资源所有者
    能够授予对受保护资源的访问权限的实体。
    当资源所有者是一个人时,它被称为
    最终用户。

    资源服务器
    托管受保护资源的服务器,能够接受
    并使用访问令牌响应受保护的资源请求。

    客户
    代表受保护的资源请求的应用程序
    资源所有者及其授权。“客户”一词的确如此
    并不意味着任何特定的实施特征(例如,
    应用程序是在服务器,桌面还是其他服务器上执行
    设备)。

    授权服务器
    服务器成功后向客户端发出访问令牌
    验证资源所有者并获得授权。

    #### 授权模式

    1 简化模式(Implicit)

    2 授权码模式(Authorization Code)

    3 密码模式(Resource Owner Password Credentials Grant)

    4 客户端模式(Client Credentials)


    场景: 公司的网站支持第三方登录(qq登录,微信登录,微博登录):

    qq开放平台:connect.qq.com/index.ht

    微博开放平台: open.weibo.com/authenti

    接入方式按照对应的平台操作即可,第三方网站接入文档提示非常全面.



    关于登录,还请题主需要关注一下 分布式下登录成功以后,分布式Session的问题以及解决方案;

    方案是用来解决问题的,这些方案没有好不好的说法,只有合不合适的说法,合适的才是最好的。


    转自https://www.zhihu.com/question/342103776/answer/806983636
    展开全文
  • SSO CAS单点登录配置教程,亲自操作后写的一个配置教程,里面有一个配置好的工程
  • SSO单点登录Java实现实例

    千次阅读 多人点赞 2021-02-05 14:43:44
    本文主要讲解,基于令牌token方式实现,SpringBoot工程下的SSO单点登录整合代码实例Demo,文末附源码地址。 1.环境准备 SSO认证中心服务( www.mysso.com) 客户端1(www.myclient1.com) 客户端2...
    随着公司系统的增加,每次新建一个项目是否还在做登录功能呢,还在做重复的工作?统一登录SSO你值得拥有。本文主要讲解,基于令牌token方式实现,SpringBoot工程下的SSO单点登录整合代码实例Demo,文末附源码地址。
    

    1.环境准备

    • SSO认证中心服务( www.mysso.com)
    • 客户端1(www.myclient1.com)
    • 客户端2(www.myclient2.com)

    由于是Demo实例,这里若有要访问-需要修改一下本机host,添加如下映射

     127.0.0.1 www.mysso.com
     127.0.0.1 www.myclient1.com
     127.0.0.1 www.myclient2.com
    

    2.搭建SSO认证中心服务

    问题来了,搭建一个SSO统一登录需要哪些功能?

    • 统一登录页+接口
    • 校验令牌有效性接口(调用来源:子系统)
    • 校验登录状态接口(调用来源:子系统)
    • 统一退出(调用来源:子系统或退出认证中心服务)

    这里就不贴Maven依赖了,主要讲解功能,详见文末附源码。

    2.1统一登录页

    创建一个统一登录页

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>程序员小强-SSODemo</title>
    </head>
    <style>body.center {
        text-align: center;
    }
    </style>
    <body class="center">
    <div>
        <h1>SSO用户统一登录</h1>
    </div>
    <div>
        <form name="loginForm" action="/sso/login" method="POST" accept-charset="UTF-8">
            <div><input placeholder="用户名" value="admin" name="username" type="text"/></div>
            <div><input placeholder="密码" value="123456" name="password" type="password"/></div>
            <div style="color: red"><span th:text="${msg}"></span></div>
            <div><input name="redirectUrl" type="hidden" th:value="${redirectUrl}"/></div>
            <input type="submit" value="登录"/>
            <input type="reset" value="重置"/>
        </form>
    </div>
    </body>
    </html>
    

    效果图
    在这里插入图片描述

    2.2统一登录接口

    这里为了简化示例,未引入redis等缓存数据库,使用的 session存储登录信息

      /**
         * 认证中心SSO统一登录方法
         */
        @RequestMapping("/login")
        public String login(LoginParam loginParam, RedirectAttributes redirectAttributes,
                            HttpSession session, Model model) {
    
            //Demo 项目此处模拟数据库账密校验
            if (!"admin".equals(loginParam.getUsername()) || !"123456".equals(loginParam.getPassword())) {
                model.addAttribute("msg", "账户或密码错误,请重新登录!");
                model.addAttribute("redirectUrl", loginParam.getRedirectUrl());
                return "login";
            }
    
            //登录成功
            //创建令牌
            String ssoToken = UUID.randomUUID().toString();
            //把令牌放到全局会话中
            session.setAttribute("ssoToken", ssoToken);
            //设置session失效时间-单位秒
            session.setMaxInactiveInterval(3600);
            //将有效的令牌-放到map容器中(存在该容器中的token都是合法的,正式环境建议redis或存库)
            SSOConstantPool.TOKEN_POOL.add(ssoToken);
    
            //未携带重定向跳转地址-默认跳转到认证中心首页
            if (StringUtils.isEmpty(loginParam.getRedirectUrl())) {
                return "index";
            }
    
            // 携带令牌到客户端
            redirectAttributes.addAttribute("ssoToken", ssoToken);
            log.info("[ SSO登录 ] login success ssoToken:{} , sessionId:{}", ssoToken, session.getId());
            // 跳转到客户端
            return "redirect:" + loginParam.getRedirectUrl();
        }
    

    2.3校验令牌接口

    在跳转到子系统后会携带令牌token,这时候需要调用以下接口来校验token的有效性
    以下接口一共做了2件事情

    • 校验token是否有效
    • 若有效-则记录了注册上来的子系统信息,用于统一注销时候使用
    /**
     * 校验令牌是否合法
     *
     * @param ssoToken    令牌
     * @param loginOutUrl 退出登录访问地址
     * @param jsessionid
     * @return 令牌是否有效
     */
    @ResponseBody
    @RequestMapping("/checkToken")
    public String verify(String ssoToken, String loginOutUrl, String jsessionid) {
        // 判断token是否存在map容器中,如果存在则代表合法
        boolean isVerify = SSOConstantPool.TOKEN_POOL.contains(ssoToken);
        if (!isVerify) {
            log.info("[ SSO-令牌校验 ] checkToken 令牌已失效 ssoToken:{}", ssoToken);
            return "false";
        }
    
        //把客户端的登出地址记录起来,后面注销的时候需要根据使用(生产环境建议存redis或库)
        List<ClientRegisterModel> clientInfoList =
            SSOConstantPool.CLIENT_REGISTER_POOL.computeIfAbsent(ssoToken, k -> new ArrayList<>());
        ClientRegisterModel vo = new ClientRegisterModel();
        vo.setLoginOutUrl(loginOutUrl);
        vo.setJsessionid(jsessionid);
        clientInfoList.add(vo);
        log.info("[ SSO-令牌校验 ] checkToken success ssoToken:{} , clientInfoList:{}", ssoToken, clientInfoList);
        return "true";
    }
    

    2.4校验登录状态接口

    当令牌校验返回失败时,子系统需要调用此接口

    /**
     * 校验是否已经登录认证中心(是否有全局会话)
     * 1.若存在则携带令牌ssoToken跳转至目标页面
     * 2.若不存在则跳转到登录页面
     */
    @RequestMapping("/checkLogin")
    public String checkLogin(String redirectUrl, RedirectAttributes redirectAttributes,
                             Model model, HttpServletRequest request) {
        //从认证中心-session中判断是否已经登录过(判断是否有全局会话)
        Object ssoToken = request.getSession().getAttribute("ssoToken");
    
        // ssoToken为空 - 没有全局回话
        if (StringUtils.isEmpty(ssoToken)) {
            log.info("[ SSO-登录校验 ] checkLogin fail 没有全局回话 ssoToken:{}", ssoToken);
            //登录成功需要跳转的地址继续传递
            model.addAttribute("redirectUrl", redirectUrl);
            //跳转到统一登录页面
            return "login";
        }
    
        log.info("[ SSO-登录校验 ] checkLogin success 有全局回话  ssoToken:{}", ssoToken);
        //重定向参数拼接(将会在url中拼接)
        redirectAttributes.addAttribute("ssoToken", ssoToken);
        //重定向到目标系统
        return "redirect:" + redirectUrl;
    }
    
    

    2.5统一退出接口

        /**
         * 统一注销
         * 1.注销全局会话
         * 2.通过监听全局会话session时效性,向已经注册的所有子系统发起注销请求
         */
        @RequestMapping("/logOut")
        public String logOut(HttpServletRequest request) {
            HttpSession session = request.getSession();
            log.info("[ SSO-统一退出 ] ....start.... sessionId:{}", session.getId());
            //注销全局会话, SSOSessionListener 监听器会处理后续操作
            request.getSession().invalidate();
            log.info("[ SSO-统一退出 ] ....end.... sessionId:{}", session.getId());
            return "logout";
        }
    

    退出监听,当统一认证中心session销毁时,同时注销子系统

    /**
     * session监听器
     *
     * @author 程序员小强
     */
    @Slf4j
    @WebListener
    public class SSOSessionListener implements HttpSessionListener {
        
        /**
         * 销毁事件监听
         * <p>
         * 1.session超时的时候会调用
         * 2.手动调用session.invalidate()方法时会调用.
         *
         * @param se
         */
        @Override
        public void sessionDestroyed(HttpSessionEvent se) {
            HttpSession session = se.getSession();
            String token = (String) session.getAttribute("ssoToken");
            log.debug("[ SSOSessionListener ] ...start..... sessionId:{},token:{}", session.getId(), token);
            //注销全局会话,SSOSessionListener监听类删除对应的信息
            session.invalidate();
            if (StringUtils.isEmpty(token)) {
                log.debug("[ SSOSessionListener ] token is null sessionId:{}", session.getId());
                return;
            }
            //清除存储的有效token数据
            SSOConstantPool.TOKEN_POOL.remove(token);
            //清除并返回已经注册的系统信息
            List<ClientRegisterModel> clientRegisterList = SSOConstantPool.CLIENT_REGISTER_POOL.remove(token);
            if (CollectionUtils.isEmpty(clientRegisterList)) {
                return;
            }
            for (ClientRegisterModel client : clientRegisterList) {
                if (null == client) {
                    continue;
                }
                //取出注册的子系统,依次调用子系统的登出方法(通过会话ID退出子系统的局部会话)
                sendHttpRequest(client.getLoginOutUrl(), client.getJsessionid());
                log.info("[ SSOSessionListener ] 注销系统 url:{},Jsessionid:{}", client.getLoginOutUrl(), client.getJsessionid());
            }
            log.debug("[ SSOSessionListener ] ...end..... sessionId:{},token:{}", session.getId(), token);
        }
    
        /**
         * 发送退出登录请求
         * 模拟浏览器访问形式
         *
         * @param reqUrl     发送请求的地址
         * @param jesssionId 会话Id
         */
        private static void sendHttpRequest(String reqUrl, String jesssionId) {
            try {
                //建立URL连接对象
                URL url = new URL(reqUrl);
                //创建连接
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                //设置请求的方式(需要是大写的)
                conn.setRequestMethod("POST");
                //设置需要响应结果
                conn.setDoOutput(true);
                //通过设置JSESSIONID模拟浏览器端操作
                conn.addRequestProperty("Cookie", "JSESSIONID=" + jesssionId);
                //发送请求到服务器
                conn.connect();
                conn.getInputStream();
                conn.disconnect();
            } catch (Exception e) {
                log.error("[ sendHttpRequest ] exception >> reqUrl:{}", reqUrl, e);
            }
        }
    }
    

    3.搭建客户端服务

    问题来了,搭建一个客户端需要哪些功能?

    • 拦截请求
    • 请求认证中心校验令牌有效性
      • 有效则创建局部会话
      • 无效则继续请求认证中心登录
    • 注销系统-请求认证中心统一注销

    3.1核心请求拦截器实现

    @Configuration
    public class WebConfig extends WebMvcConfigurationSupport {
    
        /**
         * 创建拦截器
         */
        @Bean
        WebInterceptor webInterceptor() {
            return new WebInterceptor();
        }
    
        /**
         * 添加拦截器-进行拦截
         * addPathPatterns 添加拦截
         * excludePathPatterns 排除拦截
         **/
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(this.webInterceptor())
                    .addPathPatterns("/**")
                    .excludePathPatterns("/logOut");
            super.addInterceptors(registry);
        }
    
        /**
         * 返回值-编码 UTF-8
         */
        @Bean
        public HttpMessageConverter<String> responseBodyConverter() {
            return new StringHttpMessageConverter(StandardCharsets.UTF_8);
        }
    
        @Override
        public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
            configurer.favorPathExtension(false);
        }
    
        /**
         * 资源处理器-资源路径 映射
         *
         * @param registry
         */
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/webjars/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
    
    }
    
    /**
     * 创建拦截器-拦截需要安全访问的请求
     * 方法说明
     * 1.preHandle():前置处理回调方法,返回true继续执行,返回false中断流程,不会继续调用其它拦截器
     * 2.postHandle():后置处理回调方法,但在渲染视图之前
     * 3.afterCompletion():全部后置处理之后,整个请求处理完毕后回调。
     *
     * @author 程序员小强
     */
    @Slf4j
    public class WebInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
            log.info("[ WebInterceptor ] >> preHandle  requestUrl:{} ", request.getRequestURI());
            //判断是否有局部会话
            HttpSession session = request.getSession();
            Object isLogin = session.getAttribute("isLogin");
            if (isLogin != null && (Boolean) isLogin) {
                log.debug("[ WebInterceptor ] >> 已登录,有局部会话 requestUrl:{}", request.getRequestURI());
                return true;
            }
            //获取令牌ssoToken
            String token = SSOClientHelper.getSsoToken(request);
    
            //无令牌
            if (StringUtils.isEmpty(token)) {
                //认证中心验证是否已经登录(是否存在全局会话)
                SSOClientHelper.checkLogin(request, response);
                return true;
            }
    
            //有令牌-则请求认证中心校验令牌是否有效
            Boolean checkToken = SSOClientHelper.checkToken(token, session.getId());
    
            //令牌无效
            if (!checkToken) {
                log.debug("[ WebInterceptor ] >> 令牌无效,将跳转认证中心进行认证 requestUrl:{}, token:{}", request.getRequestURI(), token);
                //认证中心验证是否已经登录(是否存在全局会话)
                SSOClientHelper.checkLogin(request, response);
                return true;
            }
    
            //token有效,创建局部会话设置登录状态,并放行
            session.setAttribute("isLogin", true);
            //设置session失效时间-单位秒
            session.setMaxInactiveInterval(1800);
            //设置本域cookie
            CookieUtil.setCookie(response, SSOClientHelper.SSOProperty.TOKEN_NAME, token, 1800);
            log.debug("[ WebInterceptor ] >> 令牌有效,创建局部会话成功 requestUrl:{}, token:{}", request.getRequestURI(), token);
            return true;
        }
    
    }
    

    4.源码介绍

    源码地址:传送门
    在这里插入图片描述

    5.源码实例测试

    由于是Demo实例,这里若有要访问-需要修改一下本机host,添加如下映射

     127.0.0.1 www.mysso.com
     127.0.0.1 www.myclient1.com
     127.0.0.1 www.myclient2.com
    

    添加好本机host映射后-分别启动
    sso-server 域名使用内网域名(www.mysso.com:8081)
    sso-client1 域名使用内网域名(www.myclient1.com:8082)
    sso-client2 域名使用内网域名(www.myclient1.com:8083)在这里插入图片描述

    5.1 客户端1登录

    5.1.1在浏览器端访问 www.myclient1.com:8082

    由于没有登录过任何一个系统,也没有全局会话,所以会跳转到统一认证中心进行登录,可以查看到redirectUrl就是我们要访问的www.myclient1.com:8082
    在这里插入图片描述

    点击登录(账密demo项目写死了,可以直接点击登录)
    在这里插入图片描述
    登录成功后会跳转到redirectUrl地址,并且携带ssoToken,这个时候客户端系统就可以请求认证中心校验ssoToken后创建内部的局部会话。

    5.2 客户端2登录

    由于客户端1已经登录,也就是说已经存在全局会话了,那么在访问客户端2的时候,其实无需登录的,只需要认证中心将最新的ssoToken携带过来就可以了。

    浏览器输入 www.myclient2.com:8083
    在这里插入图片描述
    可以查看到统一认证中心直接返回了认证token

    5.3 统一退出登录

    这里退出客户端1,退出完成后查看客户端1 与客户端2的登录状态
    在这里插入图片描述
    在这里插入图片描述

    这里继续访问www.myclient1.com:8082,会提示继续登录
    在这里插入图片描述

    访问www.myclient2.com:8083,会提示继续登录
    在这里插入图片描述

    说明统一退出登录成功了

    查看一下监听的日志,也是注销了每一个局部会话
    在这里插入图片描述

    至此SSO统一登录实战讲解完毕。

    需要完善的点还很多,比如服务端与客户端交互的时候可以通过加密或者加签方式防止数据被篡改。

    展开全文
  • 分布式Session共享和单点登录实现

    千次阅读 2020-11-09 23:11:41
    文章目录 基于Session的身份认证 简介 实现 分布式Session共享 简介 实现 单点登陆 简介 实现过程 总结 基于Session的身份认证 简介 对于一个Web应用,客户端每次请求时,服务器都会打开一个新的会话,而且服务器...


    基于Session的身份认证

    简介

    对于一个Web应用,客户端每次请求时,服务器都会打开一个新的会话,而且服务器不会维护客户端的上下文信息,因此如何管理客户端会话是必须要解决的问题。我们知道HTTP是无状态的协议,所以它提供了一种机制,通过Session来保存上下文信息,为每个用户分配一个sessionId,并且每个用户收到的sessionId都不一样,变量的值保存在服务器端。Session是以cookie或URL重写为基础的,默认使用cookie来实现,系统会创造一个名为JSESSIONID的值输出到cookie中。当用户从客户端向服务端发起HTTP请求时,会携带有sessionId的cookie请求, 这样服务端就能根据sessionId进行区分用户了。
    在这里插入图片描述

    实现

    只需要简单定义一个Filter,进行拦截非登录请求,然后确认当前请求的Session中是否能够拿到用户信息,如果能拿到用户信息,那么就是登录状态,否则,认定当前请求无效,将请求转发到登录页面即可

    //定义登录过滤器
    public class LoginFilter implements Filter {
        @Override
        public void doFilter(ServletRequest servletRequest,
                             ServletResponse servletResponse, FilterChain filterChain)
                throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse)servletResponse;
            Object user = request.getSession().getAttribute(CommonConstants.USER_ATTR);;
    
            String requestUrl = request.getServletPath();
            //非登陆页面并且不是登陆状态
            if (!requestUrl.startsWith("/login")&& null == user) {
                //则拒绝当前请求,请求转发到登陆页面            
                request.getRequestDispatcher("/login").forward(request,response);
                return ;
            }
            filterChain.doFilter(request,servletResponse);
        }
    
        @Override
        public void destroy() {
        }
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    }
    

    之后通过@Bean方式注入一个FilterRegistrationBean实例,并为它设置Filter属性,指定自定义的Filter实例,这样自定义的Filter才能在程序中生效

    @Configuration
    public class WebMvcConfig {
    
        //将过滤器添加到请求中
        @Bean
        public FilterRegistrationBean sessionFilterRegistration() {
            FilterRegistrationBean registration = new FilterRegistrationBean();
            registration.setFilter(new LoginFilter());
            registration.addUrlPatterns("/*");
            registration.addInitParameter("paramName", "paramValue");
            registration.setName("loginFilter");
            registration.setOrder(1);
            return registration;
        }
    }
    

    分布式Session共享

    简介

    随着分布式架构的演进,单个服务器已经不能满足系统的需要了,通常都会把系统部署在多台服务器上,通过Nginx负载均衡把请求分发到其中的一台服务器上,这样很可能同一个用户的请求被分发到不同的服务器上,因为Session是保存在服务器上的,那么很有可能第一次请求访问的服务器A,创建了Session,但是第二次访问到了服务器B,这时就会出现取不到Session的情况。因此要在集群环境下使用,最好的解决办法就是使用Session共享

    实现

    整个实现的核心点就是通过定义一个Request请求对象的Wrapper包装类,负责对当前Request请求的Session获取逻辑进行重写,将Session信息交由Redis进行存储和管理,包括从Redis获取Session信息以及认证成功后将Session信息提交到Redis中。因为实现比较简单,就不分析了,具体实现可以看下面代码的注释:

    //request请求的包装类
    public class RedisRequestWrapper extends HttpServletRequestWrapper {
        private volatile boolean committed = false;
        private String uuid = UUID.randomUUID().toString();
    
        private RedisSession session;
        private RedisTemplate redisTemplate;
    
        public RedisRequestWrapper(HttpServletRequest request,RedisTemplate redisTemplate) {
            super(request);
            this.redisTemplate = redisTemplate;
        }
    
        /**
         * 提交session信息到redis
         */
        public void commitSession() {
            //避免请求重复提交session
            if (committed) {
                return;
            }
            committed = true;
            RedisSession session = this.getSession();
            if (session != null && null != session.getAttrs()) {
                //将session信息存入redis
                redisTemplate.opsForHash().putAll(session.getId(),session.getAttrs());
            }
        }
    
        /**
         * 创建新session
         */
        public RedisSession createSession() {
            //从cookie中获得JSESSIONID
            String sessionId = CookieUtil.getRequestedSessionId(this);
            Map<String,Object> attr ;
            if (null != sessionId){
                //通过sessionid从redis缓存中,获取session信息
                attr = redisTemplate.opsForHash().entries(sessionId);
            } else {
                //随机生成一个sessionid
                sessionId = UUID.randomUUID().toString();
                attr = new HashMap<>();
            }
            //session成员变量持有
            session = new RedisSession();
            session.setId(sessionId);
            session.setAttrs(attr);
            return session;
        }
    
        /**
         * 获取session
         */
        public RedisSession getSession() {
            return this.getSession(true);
        }
        public RedisSession getSession(boolean create) {
            if (null != session){
                return session;
            }
            return this.createSession();
        }
        
        /**
         * 确认是否登陆
         */
        public boolean isLogin(){
            Object user = getSession().getAttribute(SessionFilter.USER_INFO);
            return null != user;
        }
    
    }
    
    public class CookieUtil{
    
        public static final String COOKIE_NAME_SESSION = "jsession";
        
        /**
         *  从请求的cookie中获取sessionid
         */
        public static String getRequestedSessionId(HttpServletRequest request) {
            Cookie[] cookies = request.getCookies();
            if (cookies == null) {
                return null;
            }
            for (Cookie cookie : cookies) {
                if (cookie == null) {
                    continue;
                }
                //确认是否存在sessionid
                if (!COOKIE_NAME_SESSION.equalsIgnoreCase(cookie.getName())) {
                    continue;
                }
    
                return cookie.getValue();
            }
            return null;
        }
        /**
         * 将session信息保存到cookie中
         */
        public static void onNewSession(HttpServletRequest request,
                                 HttpServletResponse response) {
            HttpSession session = request.getSession();
            String sessionId = session.getId();
            Cookie cookie = new Cookie(COOKIE_NAME_SESSION, sessionId);
            cookie.setHttpOnly(true);
            cookie.setPath(request.getContextPath() + "/");
            //指定一级域名
            cookie.setDomain("xxx.com");
            cookie.setMaxAge(Integer.MAX_VALUE);
            response.addCookie(cookie);
        }
    
    }
    
    //自定义session,实现HttpSession接口
    public class RedisSession implements Serializable,HttpSession {
    
        private String id;
        private Map<String,Object> attrs;
        ...
    }
    

    自定义Filter,用于拦截请求,根据Session信息来确认是否为登录状态

    //自定义filter,用来拦截非登录请求
    public class SessionFilter implements Filter {
        public static final String USER_INFO = "user";
    
        private RedisTemplate redisTemplate;
    
        public void setRedisTemplate(RedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
        @Override
        public void doFilter(ServletRequest servletRequest,
                             ServletResponse servletResponse, FilterChain filterChain)
                throws IOException, ServletException {
    
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse)servletResponse;
            RedisRequestWrapper redisRequestWrapper = new RedisRequestWrapper(request,redisTemplate);
            
            String requestUrl = request.getServletPath();
            //非登陆页面并且不是登陆状态
            if (!"/toLogin".equals(requestUrl)
                    && !requestUrl.startsWith("/login")
                    && !redisRequestWrapper.isLogin()) {
                //拒绝请求,跳转登陆页面
                request.getRequestDispatcher("/toLogin").forward(redisRequestWrapper,response);
                return ;
            }
    
            try {
                filterChain.doFilter(redisRequestWrapper,servletResponse);
            } finally {
                //提交session信息到redis中
                redisRequestWrapper.commitSession();
            }
        }
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
        @Override
        public void destroy() {
        }
    }
    

    通过@Bean方法向容器中注入Filter,使自定义Filter生效

    @Configuration
    public class SessionConfig {
    
        @Bean
        public FilterRegistrationBean sessionFilterRegistration(SessionFilter sessionFilter) {
            FilterRegistrationBean registration = new FilterRegistrationBean();
            registration.setFilter(sessionFilter);
            registration.addUrlPatterns("/*");
            registration.addInitParameter("paramName", "paramValue");
            registration.setName("sessionFilter");
            registration.setOrder(1);
            return registration;
        }
    
        //注入自定义过滤器
        @Bean
        public SessionFilter sessionFilter(RedisTemplate redisTemplate){
            SessionFilter sessionFilter = new SessionFilter();
            sessionFilter.setRedisTemplate(redisTemplate);
            return sessionFilter;
        }
    }
    
    @Controller
    public class IndexController {
    
        @GetMapping("/toLogin")
        public String toLogin(Model model,RedisRequestWrapper request) {
            UserForm user = new UserForm();
            user.setUsername("username");
            user.setPassword("password");
            user.setBackurl(request.getParameter("url"));
            model.addAttribute("user", user);
    
            return "login";
        }
    
        @PostMapping("/login")
        public void login(@ModelAttribute UserForm user,RedisRequestWrapper request,HttpServletResponse response) throws IOException, ServletException {
            request.getSession().setAttribute(SessionFilter.USER_INFO,user);
            //将session信息保存到cookie中
            CookieBasedSession.onNewSession(request,response);
            //重定向到index页面
            response.sendRedirect("/index");
        }
    
        @GetMapping("/index")
        public ModelAndView index(RedisRequestWrapper request) {
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.setViewName("index");
            modelAndView.addObject("user", request.getSession().getAttribute(SessionFilter.USER_INFO));
            request.getSession().setAttribute("test","123");
            return modelAndView;
        }
    }
    

    单点登陆

    简介

    较大的企业内部,一般都有很多的业务支持系统为其提供相应的管理和 IT 服务。通常来说,每个单独的系统都会有自己的安全体系和身份认证系统。进入每个系统都需要进行登录,这样的局面不仅给管理上带来了很大的困难,对客户来说也极不友好。那么如何让客户只需登陆一次,就可以进入多个系统,而不需要重新登录呢。“单点登录”就是专为解决此类问题的。其大致思想流程如下:通过一个 ticket 进行串接各系统间的用户信息

    实现过程

    在每一个需要身份认证的服务中,定义一个SSOFilter用于拦截非登录请求。对于每个拦截的请求,会先从当前请求的Session中确认是否能够拿到用户信息,拿不到用户信息又会确认当前请求中是否携带ticket票据这个参数,如果携带就会尝试从Redis中根据该票据拿到用户信息。如果最终都获取不到用户信息就会被重定向到SSO登录服务的登录页面进行登录处理

    public class SSOFilter implements Filter {
        private RedisTemplate redisTemplate;
    
        public static final String USER_INFO = "user";
    
        public SSOFilter(RedisTemplate redisTemplate){
            this.redisTemplate = redisTemplate;
        }
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest,
                             ServletResponse servletResponse, FilterChain filterChain)
                throws IOException, ServletException {
    
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse)servletResponse;
    
            Object userInfo = request.getSession().getAttribute(USER_INFO);;
    
            //如果未登陆,则拒绝请求,转向登陆页面
            String requestUrl = request.getServletPath();
            if (!"/toLogin".equals(requestUrl)
                    && !requestUrl.startsWith("/login")
                    && null == userInfo) {
    
                String ticket = request.getParameter("ticket");
                //有票据,则使用票据去尝试拿取用户信息
                if (null != ticket){
                    userInfo = redisTemplate.opsForValue().get(ticket);
                }
                //无法得到用户信息,则去CAS服务的登陆页面
                if (null == userInfo){
                    response.sendRedirect("http://cas.com:8080/toLogin?url="+request.getRequestURL().toString());
                    return ;
                }
    
                /**
                 * 将用户信息,加载进session中
                 */
                request.getSession().setAttribute(SSOFilter.USER_INFO,userInfo);
                //登录成功需要将ticket从redis中删除
                redisTemplate.delete(ticket);
            }
    
            filterChain.doFilter(request,servletResponse);
        }
    
        @Override
        public void destroy() {
    
        }
    
    }
    

    在SSO登录服务中,只需要简单定义一个Filter,进行拦截非登录请求,然后确认当前请求的Session中是否能够拿到用户信息,如果能拿到用户信息,那么就是登录状态,否则,认定当前请求无效,将请求转发到登录页面即可

    public class LoginFilter implements Filter {
        public static final String USER_INFO = "user";
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest,
                             ServletResponse servletResponse, FilterChain filterChain)
                throws IOException, ServletException {
    
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse)servletResponse;
    
            Object userInfo = request.getSession().getAttribute(USER_INFO);;
    
           //非登陆页面并且不是登陆状态
            String requestUrl = request.getServletPath();
            if (!"/toLogin".equals(requestUrl)
                    && !requestUrl.startsWith("/login")
                    && null == userInfo) {
                //则拒绝当前请求,请求转发到登陆页面 
                request.getRequestDispatcher("/toLogin").forward(request,response);
                return ;
            }
    
            filterChain.doFilter(request,servletResponse);
        }
    
        @Override
        public void destroy() {
    
        }
    
    }
    

    在SSO登录处理过程中,当请求已认证登录成功后,会先生成一个ticket票据,并将ticket票据和用户信息存放到Redis中,然后重定向回原先请求服务的Url,并携带上ticket票据参数

    @Controller
    public class IndexController {
        @Autowired
        private RedisTemplate redisTemplate;
    
        @GetMapping("/toLogin")
        public String toLogin(Model model,HttpServletRequest request) {
            Object userInfo = request.getSession().getAttribute(LoginFilter.USER_INFO);
            //不为空,则是已登陆状态
            if (null != userInfo){
                String ticket = UUID.randomUUID().toString();
                redisTemplate.opsForValue().set(ticket,userInfo,2, TimeUnit.SECONDS);
                return "redirect:"+request.getParameter("url")+"?ticket="+ticket;
            }
            UserForm user = new UserForm();
            user.setUsername("username");
            user.setPassword("password");
            user.setBackurl(request.getParameter("url"));
            model.addAttribute("user", user);
    
            return "login";
        }
    
        @PostMapping("/login")
        public void login(@ModelAttribute UserForm user,HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {
            request.getSession().setAttribute(LoginFilter.USER_INFO,user);
    
            //登陆成功,创建用户信息票据
            String ticket = UUID.randomUUID().toString();
            //将ticket和用户信息写入到redis中
            redisTemplate.opsForValue().set(ticket,user,20, TimeUnit.SECONDS);
            //重定向,回原请求的url,并携带ticket信息
            if (null == user.getBackurl() || user.getBackurl().length()==0){
                response.sendRedirect("/index");
            } else {
                response.sendRedirect(user.getBackurl()+"?ticket="+ticket);
            }
        }
    
        @GetMapping("/index")
        public ModelAndView index(HttpServletRequest request) {
            ModelAndView modelAndView = new ModelAndView();
    
            modelAndView.setViewName("index");
            modelAndView.addObject("user", request.getSession().getAttribute(LoginFilter.USER_INFO));
    
            request.getSession().setAttribute("test","123");
            return modelAndView;
        }
    }
    

    总结

    1. 用户访问服务A某个页面时,服务A发现自己未登录,重定向到CAS单点登录服务,CAS服务也发现未登录,则跳转到相应的登录页面
    2. 用户输入用户名和密码登录成功后,CAS服务进行认证,将登录状态记录CAS服务的session中,并写入当前CAS服务域名下的Cookie中
    3. CAS服务登录完成后会生成一个Ticket票据,并将Ticket和用户信息记录到Redis中,之后再重定向回服务A,同时将Ticket作为参数传递给服务A
    4. 服务A拿到Ticket后,从Redis中进行查找,查询Ticket对应的用户信息,之后服务A再将登录状态写入session并设置到服务A域名下的Cookie中
    5. 至此,单点登录就完成了,之后再访问服务A时,服务A就是登录状态的
    6. 当有一个新的服务B被用户访问时,服务B发现自己也未登录,此时也重定向到CAS单点登录服务,但是此时CAS服务发现已经登录了,此时就不需要进行登录认证
    7. CAS服务会生成一个Ticket票据,并将Ticket和用户信息记录到Redis中,之后再重定向回服务B,同时将Ticket作为参数传递给服务B
    8. 服务B拿到Ticket后,从Redis中进行查找,查询Ticket对应的用户信息,之后服务B再将登录状态写入session并设置到服务B域名下的Cookie中
    9. 因此服务B不需要进行登录过程,就能完成用户登录认证

    由于本人能力有限,分析不恰当的地方和文章有错误的地方的欢迎批评指出,非常感谢!

    展开全文
  • 单点登录原理

    千次阅读 2019-03-09 21:40:26
    一、系统登录机制 1、http无状态协议  web应用采用browser/server架构,http作为通信协议。http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次...
  • 单点登录说明

    千次阅读 2020-03-13 09:20:02
    什么是单点登录单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分 1、登录  相比于单系统登录,sso...
  • 单点登录与权限管理(web api)

    热门讨论 2013-05-18 11:01:29
    使用wep api开发的单点登录,结合.net自带的权限验证,做了一个登陆与权限管理的demo.

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 321,957
精华内容 128,782
关键字:

单点登录