SSO单点登录

SSO简介

web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了。
在这里插入图片描述
单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的,这个限制就是cookie的域(通常对应网站的域名),浏览器发送http请求时会自动携带与该域匹配的cookie,而不是所有cookie。

单点登录涉及sso认证中心与众子系统,子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须集成sso的客户端,sso认证中心则是sso服务端,整个单点登录过程实质是sso客户端与服务端通信的过程。sso认证中心与sso客户端通信方式有多种,httpClient,web service、rpc、restful api都可以。

什么是单点登录

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

登录

sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理,用下图说明
在这里插入图片描述

下面对上图简要描述

  1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  2. sso认证中心发现用户未登录,将用户引导至登录页面
  3. 用户输入用户名密码提交登录申请
  4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
  5. sso认证中心带着令牌跳转会最初的请求地址(系统1)
  6. 系统1拿到令牌,去sso认证中心校验令牌是否有效
  7. sso认证中心校验令牌,返回有效,注册系统1
  8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  9. 用户访问系统2的受保护资源
  10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
  12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
  13. sso认证中心校验令牌,返回有效,注册系统2
  14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系

  1. 局部会话存在,全局会话一定存在
  2. 全局会话存在,局部会话不一定存在
  3. 全局会话销毁,局部会话必须销毁

注销

单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下面的图来说明
在这里插入图片描述
sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作

下面对上图简要说明

  1. 用户向系统1发起注销请求
  2. 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
  3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
  4. sso认证中心向所有注册系统发起注销请求
  5. 各注册系统接收sso认证中心的注销请求,销毁局部会话
  6. sso认证中心引导用户至登录页面

单一服务器模式 登录

(1)使用session对象实现登录
登录成功之后,把用户数据放到session里面,session.setAttrinute(“user”,user)
判断是否登录,从session获取数据,可以获取登录信息,session.getAttrinute(“user”,user)

单点登录常见的三种方式

session广播机制实现

在一个模块中登录将用户信息写入到session,其他的模块进行一个复制

session过期时间:如果登录之后不做任何操作默认30分钟就失效了

缺点:如果只有几个模块,做个复制session没有什么问题,但是如果项目中有二十个模块,那我们的session就会复制二十次,而每次复制对于我们的资源都是一个极大的消耗。
在这里插入图片描述

cookie + redis实现

cookie是客户端技术存放在浏览器中,每次发送请求,都会带着cookie值进行发送。
redis基于key-value存储。

可以设置redis的过期时间做到跟session一样的效果

实现过程:
1、在项目中的任何一个模块进行登录,登录之后,把数据放到两个地方
redis:在key:生成唯一值(ip,用户id,UUID等等) 在value: 用户数据
cookie:把redis里面生成的key值放到cookie里面

2、访问项目中其他模块,发送请求带着cookie进行发送,获取到cookie值,拿着cookie做事情:
(1)把cookie获取值,到redis进行查询,根据key进行查询,如果查询到数据就是登录

token实现

token是什么?
按照一定规划生成字符串,字符串包括用户信息

实现过程:
1、在项目中的任何一个模块进行登录,登录之后,按照规则生成字符串,把登录必须用户包含到生成字符串里面,把字符串返回
(1)可以把字符串通过cookie返回
(2)把字符串通过地址栏返回

2、再去访问项目其他模块,每次访问在地址栏带着生成字符串,在访问模块里面获取地址栏字符串,根据字符串获取用户信息。可以获取到就是登录

token实现单点登录及注册Demo

点击跳转到码云仓库

技术桟

spring boot
mybatis-plus
jwt
swagger

pom

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>

        <!--   jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <!--swagger ui-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>

application.properties

# 服务端口
server.port=8150

# 服务名
spring.application.name=ssodemo

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/alibaba?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/alibaba/ssodemo/mapper/xml/*.xml

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

主程序

package com.alibaba.ssodemo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {"com.alibaba"})
@MapperScan("com.alibaba.ssodemo.mapper")
public class SsodemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SsodemoApplication.class, args);
    }

}

entity

package com.alibaba.ssodemo.entity;


import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Date;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class UcenterMember implements Serializable {
        private static final long serialVersionUID = 1L;

        @TableId(value = "id", type = IdType.ID_WORKER_STR)
        private String id;
        private String openid;
        private String mobile;
        private String password;
        private String nickname;
        private Integer sex;
        private Integer age;
        private String avatar;
        private String sign;
        private Boolean isDisabled;
        private Boolean isDeleted;
        @TableField(fill = FieldFill.INSERT)
        private Date gmtCreate;
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private Date gmtModified;
}

vo

package com.alibaba.ssodemo.entity.vo;

import lombok.Data;

@Data
public class RegisterVo {
    private String nickname;    //昵称
    private String mobile;      //手机号
    private String password;    //密码
}

自定为异常

package com.alibaba.ssodemo.exception;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor //有参构造方法
@NoArgsConstructor //无参构造方法
public class ServiceException extends RuntimeException {

    private Integer code;//状态码

    private String msg;//异常信息

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

controller

package com.alibaba.ssodemo.controller;

import com.alibaba.ssodemo.entity.UcenterMember;
import com.alibaba.ssodemo.entity.vo.RegisterVo;
import com.alibaba.ssodemo.service.UcenterMemberService;
import com.alibaba.ssodemo.utils.JwtUtils;
import com.alibaba.ssodemo.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/ssodemo/")
public class UcenterMemberController {

    @Autowired
    private UcenterMemberService ucenterMemberService;

    //登录
    @PostMapping("/login")
    public R loginUser(@RequestBody UcenterMember member){
        //调用service方法实现登录,返回token值,使用jwt生成
        String token = ucenterMemberService.login(member);
        return R.ok().data("token",token);
    }

    //注册
    @PostMapping("register")
    public R register(@RequestBody RegisterVo registerVo){
        ucenterMemberService.register(registerVo);
        return R.ok();
    }

    //根据token获取用户的信息
    @GetMapping("getMemberInfo")
    public R getMemberInfo(HttpServletRequest response){
        //调用工具类,根据response对象获取头信息,返回用户id
        String idByJwtToken = JwtUtils.getMemberIdByJwtToken(response);
        UcenterMember byId = ucenterMemberService.getById(idByJwtToken);
        return R.ok().data("byId",byId);
    }
}

mapper

package com.alibaba.ssodemo.mapper;

import com.alibaba.ssodemo.entity.UcenterMember;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface UcenterMemberMapper extends BaseMapper<UcenterMember> {
}

servuce

package com.alibaba.ssodemo.service;

import com.alibaba.ssodemo.entity.UcenterMember;
import com.alibaba.ssodemo.entity.vo.RegisterVo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface UcenterMemberService extends IService<UcenterMember> {

    //登录方法
    String login(UcenterMember member);

    //注册的方法
    void register(RegisterVo registerVo);
}

serviceimpl

package com.alibaba.ssodemo.service.impl;

import com.alibaba.ssodemo.entity.UcenterMember;
import com.alibaba.ssodemo.entity.vo.RegisterVo;
import com.alibaba.ssodemo.exception.ServiceException;
import com.alibaba.ssodemo.mapper.UcenterMemberMapper;
import com.alibaba.ssodemo.service.UcenterMemberService;
import com.alibaba.ssodemo.utils.JwtUtils;
import com.alibaba.ssodemo.utils.MD5;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

@Service
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {

    //登录的方法
    @Override
    public String login(UcenterMember member) {
        String mobile = member.getMobile();//手机号
        String password = member.getPassword();//密码

        //对手机号及密码做非空判断
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password))
            throw new ServiceException(20001,"手机号或密码不能为空");

        //判断手机号是正确
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        UcenterMember ucenterMember = baseMapper.selectOne(wrapper);
        //判断查询对象是否为空
        if (ucenterMember == null)
            throw new ServiceException(20001,"输入的手机号错误");

        //判断密码是否正确
        if (!MD5.encrypt(password).equals(ucenterMember.getPassword()))
            throw new ServiceException(20001,"输入的密码有误");

        //登录成功,根据用户的id及昵称生成token
        String jwtToken = JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname());

        return jwtToken;
    }

    //注册的方法
    @Override
    public void register(RegisterVo registerVo) {
        //获取注册的数据
        String mobile = registerVo.getMobile();//手机号
        String nickname = registerVo.getNickname();//昵称
        String password = registerVo.getPassword();//秘密

        //对传过来的参数进行判断
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(nickname) || StringUtils.isEmpty(password))
            throw new ServiceException(20001,"注册失败,请填写相应的数据");

        //对相同的手机号不进行添加
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        Integer integer = baseMapper.selectCount(wrapper);

        if (integer > 0 )
            throw new ServiceException(20001,"手机号已经注册过了");

        //将数据加入数据库中
        UcenterMember ucenterMember = new UcenterMember();
        ucenterMember.setNickname(nickname);
        ucenterMember.setMobile(mobile);
        ucenterMember.setPassword(MD5.encrypt(password));
        ucenterMember.setIsDisabled(false);//用户不禁用
        baseMapper.insert(ucenterMember);
    }
}

utils

Jwt生成令牌

package com.alibaba.ssodemo.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * Jwt生成令牌
 */
public class JwtUtils {

    //常量
    public static final long EXPIRE = 1000 * 60 * 60 * 24;  //tokent过期时间
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";   //秘钥

    //生成token字符串的方法
    public static String getJwtToken(String id, String nickname){

        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("guli-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .claim("nickname", nickname)
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}

MD5加密

package com.alibaba.ssodemo.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * MD5加密方法
 */
public final class MD5 {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }

    public static void main(String[] args) {
        System.out.println("123456");
    }

}

统一返回值

package com.alibaba.ssodemo.utils;

import lombok.Data;

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

/**
 * 统一返回值
 */
@Data
public class R {
    private Boolean succes;

    private Integer code;

    private String message;

    private Map<String,Object> data = new HashMap<String,Object>();

    //构造方法私有
    private R(){}

    //成功方法
    public static R ok(){
        R r = new R();
        r.setSucces(true);
        r.setCode(ResultCode.SUCCESS);
        r.setMessage("成功");
        return r;
    }

    //失败方法
    public static R error(){
        R r = new R();
        r.setSucces(false);
        r.setCode(ResultCode.ERROR);
        r.setMessage("失败");
        return r;
    }

    public Boolean getSucces() {
        return succes;
    }

    public void setSucces(Boolean succes) {
        this.succes = succes;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Map<String, Object> getData() {
        return data;
    }

    public void setData(Map<String, Object> data) {
        this.data = data;
    }
    public R success(Boolean success){
        this.setSucces(success);
        return this;
    }

    public R message(String message){
        this.setMessage(message);
        return this;
    }

    public R code(Integer code){
        this.setCode(code);
        return this;
    }

    public R data(String key, Object value){
        this.data.put(key, value);
        return this;
    }

    public R data(Map<String, Object> map){
        this.setData(map);
        return this;
    }
}

返回值状态码

package com.alibaba.ssodemo.utils;

/**
 * 返回值状态码
 */
public interface ResultCode {

    public static Integer SUCCESS = 20000;  //成功

    public static Integer ERROR = 20001;    //失败
}

swagger

package com.alibaba.ssodemo.config;

import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration//配置类
@EnableSwagger2//swagger配置
public class SwaggerConfig {
    @Bean
    public Docket webApiConfig(){

        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")
                .apiInfo(webApiInfo())
                .select()
                //.paths(Predicates.not(PathSelectors.regex("/admin/.*")))
                .paths(Predicates.not(PathSelectors.regex("/error.*")))
                .build();
    }

    private ApiInfo webApiInfo(){
        return new ApiInfoBuilder()
                .title("网站-API文档")
                .description("本文档描述了单点登录接口定义")
                .version("1.0")
                .contact(new Contact("Helen", "http://bing.com", "hq1162759071@foxmail.com"))
                .build();
    }
}

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值