Ruoyi-cloud集成Sa-Token SSO单点登录


https://github.com/dromara/Sa-Token

Sa-Token SSO 模式三
修改本地hosts

127.0.0.1 sa-sso-server.com
127.0.0.1 sa-sso-client1.com
127.0.0.1 sa-sso-client2.com
127.0.0.1 sa-sso-client3.com

服务端

使用的是源码里面的 sa-token-demo-sso-server

package com.pj.sso;

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.sso.SaSsoUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import com.ejlchina.okhttps.OkHttps;

import cn.dev33.satoken.config.SaSsoConfig;
import cn.dev33.satoken.sso.SaSsoHandle;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;

/**
 * Sa-Token-SSO Server端 Controller 
 * @author kong
 *
 */
@RestController
public class SsoServerController {

	/*
	 * SSO-Server端:处理所有SSO相关请求 
	 * 		http://{host}:{port}/sso/auth			-- 单点登录授权地址,接受参数:redirect=授权重定向地址 
	 * 		http://{host}:{port}/sso/doLogin		-- 账号密码登录接口,接受参数:name、pwd 
	 * 		http://{host}:{port}/sso/checkTicket	-- Ticket校验接口(isHttp=true时打开),接受参数:ticket=ticket码、ssoLogoutCall=单点注销回调地址 [可选] 
	 * 		http://{host}:{port}/sso/logout			-- 单点注销地址(isSlo=true时打开),接受参数:loginId=账号id、secretkey=接口调用秘钥 
	 */
	@RequestMapping("/sso/*")
	public Object ssoRequest() {
		return SaSsoHandle.serverRequest();
	}
	
	// 配置SSO相关参数 
	@Autowired
	private void configSso(SaSsoConfig sso) {
		
		// 配置:未登录时返回的View 
		sso.setNotLoginView(() -> {
			return new ModelAndView("sa-login.html");
		});
		
		// 配置:登录处理函数 
		sso.setDoLoginHandle((name, pwd) -> {
			// 此处仅做模拟登录,真实环境应该查询数据进行登录 
			if("sa".equals(name) && "123456".equals(pwd)) {
				StpUtil.login(10001);
				return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
			}
			return SaResult.error("登录失败!");
		});
		
		// 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉) 
		sso.setSendHttp(url -> {
			try {
				// 发起 http 请求 
				System.out.println("发起请求:" + url);
				return OkHttps.sync(url).get().getBody().toString();
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		});
	}

    // 自定义接口:获取userinfo
    @RequestMapping("/sso/userinfo")
    public Object userinfo(String loginId) {
        System.out.println("---------------- 获取userinfo --------");

        // 校验签名,防止敏感信息外泄
        SaSsoUtil.checkSign(SaHolder.getRequest());

        // 自定义返回结果(模拟) name和pwd返回给client前端登录用,因为只有登录成功才返回密码,应该没安全问题
        return SaResult.ok()
                .set("id", loginId)
                .set("name", "admin")
				.set("pwd", "admin123")
                .set("sex", "女")
                .set("age", 18);
    }
	
}

客户端前端

src/api/login.js

export function ssoLogout(satoken) {
  return request({
    url: '/auth/sso/logout',
    headers: {
      isToken: false
    },
    method: 'post',
    data: { satoken}
  })
}

src/permission.js
增加 /sso

const whiteList = ['/sso']

src/router/index.js

 {
   path: '/sso',
   component: () => import('@/views/sso'),
   hidden: true
 }

src/store/modules/user.js

	import { ssoLogout } from '@/api/login'
	
    // 退出系统
    LogOut({ commit, state }) {
      return new Promise((resolve, reject) => {   
        logout(state.token).then(() => {
          commit('SET_TOKEN', '')
          commit('SET_ROLES', [])
          commit('SET_PERMISSIONS', [])
          removeToken()
          resolve()
          // sso登录退出
          let satoken = localStorage.satoken;
          ssoLogout(satoken).then(res => {
          });
        }).catch(error => {
          reject(error)
        })
      })
    },

src/views/login.vue

<template>
  <div class="login">
    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">若依后台管理系统</h3>
      <el-form-item prop="username">
        <el-input
          v-model="loginForm.username"
          type="text"
          auto-complete="off"
          placeholder="账号"
        >
          <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          v-model="loginForm.password"
          type="password"
          auto-complete="off"
          placeholder="密码"
          @keyup.enter.native="handleLogin"
        >
          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
        </el-input>
      </el-form-item>
      <el-form-item prop="code" v-if="captchaEnabled">
        <el-input
          v-model="loginForm.code"
          auto-complete="off"
          placeholder="验证码"
          style="width: 63%"
          @keyup.enter.native="handleLogin"
        >
          <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
        </el-input>
        <div class="login-code">
          <img :src="codeUrl" @click="getCode" class="login-code-img"/>
        </div>
      </el-form-item>
      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
      <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          size="medium"
          type="primary"
          style="width:100%;"
          @click.native.prevent="handleLogin"
        >
          <span v-if="!loading">登 录</span>
          <span v-else>登 录 中...</span>
        </el-button>
        <div style="float: right;" v-if="register">
          <router-link class="link-type" :to="'/register'">立即注册</router-link>
        </div>
      </el-form-item>
      <!-- <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          size="medium"
          type="primary"
          style="width:100%;"
          @click.native.prevent="handleSsoLogin"
        >
          <span>登 录2</span>      
        </el-button>
      </el-form-item>
      <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          size="medium"
          type="primary"
          style="width:100%;"
          @click.native.prevent="handleSsoLogout"
        >
          <span>注销</span>      
        </el-button>
      </el-form-item>
      <p>当前是否登录:<b>{{ isLogin }}</b></p> -->
    </el-form>
    
    <!--  底部  -->
    <div class="el-login-footer">
      <span>Copyright © 2018-2022 ruoyi.vip All Rights Reserved.</span>
    </div>
  </div>
</template>

<script>
import { getCodeImg, ssoLogout } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
import request from '@/utils/request'

export default {
  name: "Login",
  data() {
    return {
      codeUrl: "",
      loginForm: {
        username: "admin",
        password: "admin123",
        rememberMe: false,
        code: "",
        uuid: "",
        login: false,
      },
      loginRules: {
        username: [
          { required: true, trigger: "blur", message: "请输入您的账号" }
        ],
        password: [
          { required: true, trigger: "blur", message: "请输入您的密码" }
        ],
        code: [{ required: true, trigger: "change", message: "请输入验证码" }]
      },
      loading: false,
      // 验证码开关
      captchaEnabled: true,
      // 注册开关
      register: false,
      redirect: undefined,
      saname:"",
      sapwd:"",
      isLogin:false
    };
  },
  watch: {
    $route: {
      handler: function(route) {
        this.redirect = route.query && route.query.redirect;
        this.saname = route.query && route.query.saname;
        this.sapwd = route.query && route.query.sapwd; 
      },
      immediate: true
    }
  },
  created() {
    this.getCode();
    this.getCookie();
    // 是否自动登录
    // this.ssoIsLogin();
  },
  methods: {
    getCode() {
      getCodeImg().then(res => {
        this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
        if (this.captchaEnabled) {
          this.codeUrl = "data:image/gif;base64," + res.img;
          this.loginForm.uuid = res.uuid;
        }
      });
    },
    getCookie() {
      const username = Cookies.get("username");
      const password = Cookies.get("password");
      const rememberMe = Cookies.get('rememberMe')
      this.loginForm = {
        username: username === undefined ? this.loginForm.username : username,
        password: password === undefined ? this.loginForm.password : decrypt(password),
        rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
      };
    },
    handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true;
          if (this.loginForm.rememberMe) {
            Cookies.set("username", this.loginForm.username, { expires: 30 });
            Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
            Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
          } else {
            Cookies.remove("username");
            Cookies.remove("password");
            Cookies.remove('rememberMe');
          }
          this.$store.dispatch("Login", this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
          }).catch(() => {
            this.loading = false;
            if (this.captchaEnabled) {
              this.getCode();
            }
          });
        }
      });
    },
    handleSsoLogin(){
      //let _url = location.href + '&saname='+ this.loginForm.username + '&sapwd='+ this.loginForm.password;
      let _url = location.href;
      this.$router.push({ path: '/sso', query: { back: encodeURIComponent(_url) } });
    },
    handleSsoLogout(){
      let satoken = localStorage.satoken; 
      ssoLogout(satoken).then(res => {
        this.ssoIsLogin();
      });
    },
    ssoIsLogin(){
      request({
        url: '/auth/sso/isLogin',
        headers: {
          isToken: false
        },
        method: 'post'
      }).then(res => {
        this.isLogin = res.data;
        if(res.data){
          console.log("sso登录成功");
          // 跳转若依
          this.handleRuoyiAutoLogin();
        }
        else{
          console.log("sso未登录");
          // 跳转sso登录
          this.handleSsoLogin();
        }
      });
    },
    handleRuoyiAutoLogin(){
      request({
        url: '/auth/sso/myinfo',
        headers: {
          isToken: false
        },
        method: 'post'
      }).then(res => {
        if(res.code==200){
          this.loginForm.username = res.name;
          this.loginForm.password = res.pwd;
          debugger;
          this.$store.dispatch("Login", this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
          }).catch(() => {
            this.loading = false;
            if (this.captchaEnabled) {
              this.getCode();
            }
          });
        }
      });
    }
  }
};
</script>

src/views/sso.vue

<template>
  <div class="">    
  </div>
</template>

<script>
import { getCodeImg, getRedirectUrl } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
import request from '@/utils/request'

export default {
  name: "Sso",
  data() {
    return {
      baseUrl: "http://localhost/auth",
      back: undefined,
      ticket: undefined
    };
  },
  watch: {
    $route: {
      handler: function(route) {
        this.back = route.query && route.query.back;
        this.ticket = route.query && route.query.ticket;        
      },
      immediate: true
    }
  },
  created() {    
    if(this.ticket) {
      this.doLoginByTicket(this.ticket);
    } else {
      this.goSsoAuthUrl();
    }
  },
  methods: {    
    goSsoAuthUrl() {      
      request({
        url: '/auth/sso/getSsoAuthUrl',
        headers: {
          isToken: false
        },
        method: 'post',
        params: { clientLoginUrl:location.href }
      }).then(res => {        
				location.href = res.data;
      });

    },
    doLoginByTicket() {
      request({
        url: '/auth/sso/doLoginByTicket',
        headers: {
          isToken: false
        },
        method: 'post',
        params: { ticket: this.ticket }
      }).then(res => {              
        if(res.code == 200) {
						localStorage.setItem('satoken', res.data);            
            let _back =  decodeURIComponent(this.back); 
						location.href = _back; 
					} else {
						alert(res.msg);
					}
      });      
    },

  }
};
</script>

<style rel="stylesheet/scss" lang="scss">
</style>

客户端后端

aihub-auth/pom.xml

    <properties>
        <sa-token-version>1.30.0</sa-token-version>
    </properties>

        <!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>${sa-token-version}</version>
        </dependency>

        <!-- Sa-Token 插件:整合SSO -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-sso</artifactId>
            <version>${sa-token-version}</version>
        </dependency>

        <!-- Sa-Token 插件:整合redis (使用jackson序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>${sa-token-version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- Http请求工具(在模式三的单点注销功能下用到,如不需要可以注释掉) -->
        <dependency>
            <groupId>com.ejlchina</groupId>
            <artifactId>okhttps</artifactId>
            <version>3.5.3</version>
            <exclusions>
                <exclusion>
                    <groupId>com.squareup.okio</groupId>
                    <artifactId>okio</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.squareup.okio</groupId>
            <artifactId>okio</artifactId>
            <version>2.8.0</version>
        </dependency>

aihub-auth/src/main/resources/bootstrap.yml

# sa-token配置
sa-token:
  # SSO-相关配置
  sso:
    # SSO-Server端 统一认证地址
    auth-url: http://sa-sso-server.com:9000/sso/auth
    # 使用Http请求校验ticket
    is-http: true
    # SSO-Server端 ticket校验地址
    check-ticket-url: http://sa-sso-server.com:9000/sso/checkTicket
    # 是否打开单点注销接口
    is-slo: true
    # 单点注销地址
    slo-url: http://sa-sso-server.com:9000/sso/logout
    # 接口调用秘钥
    secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
    # SSO-Server端 查询userinfo地址
    userinfo-url: http://sa-sso-server.com:9000/sso/userinfo

  # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
  alone-redis:
    # Redis数据库索引
    database: 1
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0

aihub-auth/src/main/java/com/aihub/auth/controller/CorsFilter.java

package com.aihub.auth.controller;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 跨域过滤器
 * @author kong 
 */
@Component
@Order(-200)
public class CorsFilter implements Filter {

	static final String OPTIONS = "OPTIONS";

	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		
		// 允许指定域访问跨域资源
		response.setHeader("Access-Control-Allow-Origin", "*");
		// 允许所有请求方式
		response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
		// 有效时间
		response.setHeader("Access-Control-Max-Age", "3600");
		// 允许的header参数
		response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");

		// 如果是预检请求,直接返回
		if (OPTIONS.equals(request.getMethod())) {
			System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
			response.getWriter().print("");
			return;
		}

		// System.out.println("*********************************过滤器被使用**************************");
		chain.doFilter(req, res);
	}

	@Override
	public void init(FilterConfig filterConfig) {
	}

	@Override
	public void destroy() {
	}

}

aihub-auth/src/main/java/com/aihub/auth/controller/H5Controller.java

package com.aihub.auth.controller;

import cn.dev33.satoken.config.SaSsoConfig;
import cn.dev33.satoken.sso.SaSsoHandle;
import cn.dev33.satoken.sso.SaSsoUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.ejlchina.okhttps.OkHttps;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 前后台分离架构下集成SSO所需的代码 (SSO-Client端)
 * <p>(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)</p>
 * @author kong
 *
 */
@RestController
public class H5Controller {

	/*
	 * SSO-Client端:处理所有SSO相关请求
	 * 		http://{host}:{port}/sso/login			-- Client端登录地址,接受参数:back=登录后的跳转地址
	 * 		http://{host}:{port}/sso/logout			-- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址
	 * 		http://{host}:{port}/sso/logoutCall		-- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
	 */
	@RequestMapping("/sso/*")
	public Object ssoRequest() {
		return SaSsoHandle.clientRequest();
	}

	// 配置SSO相关参数
	@Autowired
	private void configSso(SaSsoConfig sso) {
		// 配置Http请求处理器
		sso.setSendHttp(url -> {
			System.out.println("发起请求:" + url);
			return OkHttps.sync(url).get().getBody().toString();
		});
	}

	// 当前是否登录 
	@RequestMapping("/sso/isLogin")
	public Object isLogin() {
		return SaResult.data(StpUtil.isLogin());
	}

	// 查询我的账号信息
	@RequestMapping("/sso/myinfo")
	public Object myinfo() {
		Object userinfo = SaSsoUtil.getUserinfo(StpUtil.getLoginId());
		System.out.println("--------info:" + userinfo);
		return userinfo;
	}
	
	// 返回SSO认证中心登录地址 
	@RequestMapping("/sso/getSsoAuthUrl")
	public SaResult getSsoAuthUrl(String clientLoginUrl) {
		String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, "");
		return SaResult.data(serverAuthUrl);
	}
	
	// 根据ticket进行登录 
	@RequestMapping("/sso/doLoginByTicket")
	public SaResult doLoginByTicket(String ticket) {
		Object loginId = SaSsoHandle.checkTicket(ticket, "/sso/doLoginByTicket");
		if(loginId != null) {
			StpUtil.login(loginId);
			return SaResult.data(StpUtil.getTokenValue());
		}
		return SaResult.error("无效ticket:" + ticket); 
	}

	// 全局异常拦截 
	@ExceptionHandler
	public SaResult handlerException(Exception e) {
		e.printStackTrace(); 
		return SaResult.error(e.getMessage());
	}
	
}

aihub-gateway/src/main/java/com/aihub/gateway/filter/ValidateCodeFilter.java

 //sso自动登录去掉验证码
 private final static String[] VALIDATE_URL = new String[] { "/auth/loginxxx", "/auth/register" };
  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绯虹剑心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值