sso单点登录实现

Posted 码农小黑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了sso单点登录实现相关的知识,希望对你有一定的参考价值。

一、sso单点登录原理

单点登录全称Single Sign On(简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分

结合本项目案例,画了下SSO单点登录原理图,如下:

二、sso单点登录实现

1.sso-server单点登录服务端

1.sso-server服务端包含三个模块:

  • sso-common通用模块

  • sso-entity实体类模块

  • sso-web核心web模块

其中sso-common和sso-entity作为sso-client的依赖

2.核心代码

/**
 * Created by wly on 2018/11/19.
 */
@Controller 
@RequestMapping("/sso")
public class SsoController extends BaseController{

@Autowired
private UserService userService;

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private SpringSsoAutoConfig ssoAutoConfig;

@RequestMapping("/login")
public String login(@RequestParam(value = "fromUrl",required = false) String fromUrl, Model model) throws IOException {
    String token = getCookie( getRequest(), ParamEnum.PARAM_COKIE_NAME.getValue() );
    if(StringUtils.isNotBlank( token )){
        fromUrl = contact( fromUrl, ParamEnum.PARAM_TOKEN.getValue(),token );
        return "redirect:"+fromUrl;
    }
    if(StringUtils.isNotBlank( ssoAutoConfig.getSsoServerLoginUrl() )){
        String url = contact( ssoAutoConfig.getSsoServerLoginUrl(), ParamEnum.PARAM_FROM_URL.getValue(), fromUrl );
        return "redirect:"+url;
    }else {
        model.addAttribute( "fromUrl",fromUrl );
        return "login";
    }

}

@RequestMapping("/doLogin")
public String doLogin(ModelMap model , @RequestParam(value = "email",required = false) String email,
                                  @RequestParam(value = "password",required = false) String password,
                                  @RequestParam(value = "phone",required = false) String phone,
                                  @RequestParam(value = "fromUrl",required = false) String fromUrl) throws IOException {
    User user = null;
    Boolean bl = false;
    Map<String,Object> map = new HashMap<>(  );
    if(StringUtils.isNotBlank( email )){
        user = userService.findByEmail( email );
        bl = ValidatePassword( user, password );

    }else if(StringUtils.isNotBlank( phone )){
        user = userService.findByPhone( phone );
        bl = ValidatePassword( user, password );
    }

    if(bl){
        //账号密码正确
        String token = UUID.randomUUID().toString();
        //getResponse().setHeader("Access-Control-Allow-Origin", "*"); 允许跨域
        writeCookie( getResponse(),ParamEnum.PARAM_COKIE_NAME.getValue(),token,ssoAutoConfig.getSsoCookieTimeout() );
        redisTemplate.opsForValue().set( token,user,ssoAutoConfig.getSsoCookieTimeout(),TimeUnit.SECONDS );
        String redirectUrl = contact( fromUrl,ParamEnum.PARAM_TOKEN.getValue(),token );
        return "redirect:"+redirectUrl;

    }else{
        //账号或者密码错误
        //getResponse().setHeader("Access-Control-Allow-Origin", "*"); 允许跨域

        model.addAttribute( "result","error");
        model.addAttribute( "fromUrl",fromUrl );
        return "login";
    }

}


@ResponseBody
@RequestMapping("/validate")
public Object validateToken( @RequestParam(value = "sso_token",required = false) String token) throws IOException {
    if(StringUtils.isNotBlank( token )){
        Object obj = redisTemplate.opsForValue().get( token );
        if(obj!=null){
            //已登录 返回user对象
            return obj;
        }else{
            //未登录或已失效
            return null;

        }

    }else {
        //未登录
        return null;
    }

}

}

如果系统未登录跳转到sso-server登录页面并携带来源url,在sso-server登录页面进行登录操作:

如果登录成功则生成唯一token并保存到cookie中,主意此时设置cookie的域名domain为sso.com ,设置过期时间,然后以token为key,用户信息为value存储到redis中,并设置过期时间,与cookie的过期时间一致

如果登录失败,则跳转到sso-server登录页面并提示错误

2.sso-client单点登录客户端

1.引入sso-server服务端的两个依赖

    <dependency>
        <groupId>com.sso.www</groupId>
        <artifactId>sso-common</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>com.sso.www</groupId>
        <artifactId>sso-entity</artifactId>
        <version>1.0.0</version>
    </dependency>

2.添加插件,使得sso-client在打包的时候能将依赖一起打包

    <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.4.1</version> <configuration>
        <!-- get all project dependencies -->
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs> <!-- MainClass in mainfest make a executable jar -->
        <archive> <manifest> <mainClass>util.Microseer</mainClass> </manifest> </archive>
    </configuration>
        <executions>
            <execution>
                <id>make-assembly</id> <!-- bind to the packaging phase -->
                <phase>package</phase> <goals> <goal>single</goal> </goals>
            </execution>
        </executions>
    </plugin>

使用maven 打包命令打包

install -Dmaven.test.skip=true

这样在target目录就会出现两个jar,以jar-with-dependencies.jar结尾的就是包含依赖的jar,另一个则不包含依赖,因为sso-client客户端还要被其他系统依赖,所以我们需要含有依赖的jar

3.核心代码:

/**
 * Created by wly on 2018/11/20.
 */
public class LoginFilter implements Filter {
@Autowired
private SpringSsoAutoConfig springSsoAutoConfig;
@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;
    User user = (User) request.getSession().getAttribute( SessionEnum.SESSION_LOGIN.getValue() );

    String uri = request.getRequestURI();
    System.out.println("uri=="+uri);
    String ssoLoginUrl = springSsoAutoConfig.getSsoServerLoginUrl();
    String url = ssoLoginUrl + "?"+ ParamEnum.PARAM_FROM_URL.getValue()+"="+springSsoAutoConfig.getSsoClientFromUrl()+uri;

    String token = request.getParameter( ParamEnum.PARAM_TOKEN.getValue() );
    if(user!=null){
        filterChain.doFilter( servletRequest,servletResponse );
    }else if(StringUtils.isNotBlank( token )){
        postUrl(request,response,token,filterChain,url);
    }else{
        response.sendRedirect( url );
        return;
    }

}

@Override
public void destroy() {

}

public  void postUrl(HttpServletRequest request, HttpServletResponse response, String token, FilterChain filterChain, String redirectUrl) throws IOException, ServletException {
    //获取令牌 存储令牌和用户
    Map<String,String> map = new HashMap<>(  );
    map.put( SessionEnum.SESSION_SSO_TOKEN.getValue(),token );
    String jsonStr = UrlUtil.sendPost( springSsoAutoConfig.getSsoServerValidateUrl(), map );
    if(StringUtils.isNotBlank( jsonStr )){
        User user = JSONObject.parseObject( jsonStr, User.class );
        request.getSession().setAttribute( SessionEnum.SESSION_LOGIN.getValue(),user );
        filterChain.doFilter( request,response );
    }else {
        response.sendRedirect( redirectUrl );
    }
}

}

sso-client客户端添加自定义登录过滤器LoginFilter,拦截所有请求,如果已登录则直接放行,如果未登录则判断是否包含token,如果不包含token则重定向到sso-server服务端进行登录,如果包含token则发送post请求给sso-server服务端验证token的有效性和正确性,并将用户信息返回,如果返回值为null则重定向到sso-server服务端进行登录,如果返回用户信息则将用户信息保存到session中,维护用户的登录状态

其他项目只要依赖sso-client就可以做登录拦截了,而不用每个项目都去写一个登录过滤器。

3.sso-yvpai测试系统

1.引入sso-client依赖

    <dependency>
        <groupId>com.client.www</groupId>
        <artifactId>sso-client</artifactId>
        <version>1.0.0</version>
    </dependency>

2.登录过滤器sso-client已经写好了,所以这里的核心代码主要是登录成功后的跳转:

/**
 * Created by wly on 2018/11/19.
 */
@Controller
@RequestMapping("/yvpai")
public class LoginController extends BaseController {
@Autowired
private SpringSsoAutoConfig springSsoAutoConfig;


@RequestMapping("/home")
public String home(Model model)  {
    model.addAttribute( "msg",springSsoAutoConfig.getSsoClientFromUrl()+"的首页" );
    return "home";
}

}

登录成功后访问/yvpai/home 跳转到home.html页面

三、启动测试

我们使用whistle进行域名代理

sso单点登录实现

如果对whistle不了解的同学可以参考我的另一篇博客web调试代理工具Whistle https://blog.csdn.net/abcwanglinyong/article/details/80264271

启动sso-server服务端

启动sso-client客户端

启动yvpai测试系统

启动完成以后访问www.client.com/home

此时因为没有登录会跳转到sso-server登录页面如下:

sso单点登录实现

然后在访问我们的测试系统:www.yvpai.com/yvpai/home

发现此时不需要登录就可以直接访问了,如图:

注意:

1.为了保证数据安全,sso-server服务端应该对用户信息进行加密操作,sso-client客户端获取用户信息后再对其进行解密,而且用户信息应该只保存{id:1}部分,客户端获取id后再去数据库查询用户信息

2.sso-server应该提供操作用户的接口

单点注销

用户点击退出登录按钮:

1.sso-client客户端自己清除本地session

2.sso-client客户端发送post请求给sso-server服务端,服务端清除本地cookie以及redis中对应的信息,然后通知其他子系统清除本地session

关于单点注销这里就不实现了,有兴趣的同学可以自己实现以下。


以上是关于sso单点登录实现的主要内容,如果未能解决你的问题,请参考以下文章

简单代码实现JWT(json web token)完成SSO单点登录

sso单点登录都有哪些实现方式?

sso单点登录实现

可跨域的单点登录(SSO)实现方案(附.NET代码)

学习CAS实现SSO单点登录

单点登录原理和java实现简单的单点登录