一、单点登录SSO介绍
目前,每个企业或平台都有多个系统。由于历史原因,每个系统都是从不同的厂家采购的,所以系统是相互独立的,有自己的用户认证和认证系统。用户登录系统时,需要记住各个系统的用户名和密码,同时管理员还需要为同一个用户设置多个系统登录帐号,这显然给用户带来了不便系统的。我们期望的是,如果有多个系统,你只需要登录一次就可以访问多个系统,并且只需要在其中一个系统上注销并登录,那么所有系统都会被注销,而无需重复操作,这是单点登录(Single Sign On (SSO) 系统实现的功能。
单点登录是系统功能的定义,实现单点登录功能有两种开源流行的方法:CAS和OAuth2。过去,我们用的最多的是 CAS。现在随着 SpringCloud 的普及,越来越多的人选择使用它。Spring Security 提供的 OAuth2 认证授权服务器实现了单点登录功能。
OAuth2 是一种授权协议标准。任何人都可以基于此标准开发 Oauth2 授权服务器。现在百度开放平台、腾讯开放平台等大部分开放平台都是基于OAuth2协议实现的。OAuth2.0定义有四种授权类型,最新版本的OAuth2.1协议定义了七种授权类型,其中两种由于安全问题不再推荐:
[OAuth2.1 推荐的五种授权类型] [OAuth2.1 中不推荐/禁止使用的两种授权类型] [Spring Security 对 OAuth2 协议的支持]:
从SpringSecurity官网可以看出,通过对OAuth2的长期支持以及对实际业务场景的考虑,大部分系统都不需要授权服务器。因此,Spring 官方不再推荐使用 spring-security-oauth2。将security-oauth2中的OAuth2登录、客户端、资源服务器等功能提取出来,集成到Spring Security中,并创建一个单独的spring-authorization-server项目来实现授权服务器功能。
目前我们最了解的是Spring Security OAuth对OAuth2协议的实现和支持。这里我们需要将 Spring Security OAuth 和 Spring Security 区分为两个项目。过去,OAth2 相关的功能都是在 Spring Security OAuth 项目中实现的,但是自从 SpringSecurity5.以 @>X 开始,SpringSecurity 项目开始逐渐增加 Spring Security OAuth 中的功能。从SpringSecurity5.2开始,增加了OAuth2.0登录、客户端、资源服务器功能。不过授权服务器的功能并不是要集成到SpringSecurity项目中,而是新建一个spring-authorization-server项目作为单独的授权服务器:详细介绍。spring-security 实现了 OAuth2.1 协议,
Spring 未来的计划是将 Spring Security OAuth 中当前的所有功能构建到 Spring Security 5.x 中。在 Spring Security 实现与 Spring Security OAuth 的功能对等后,他们将继续支持错误和安全修复至少一年。
【GitEgg框架单点登录实施方案】:
由于spring-authorization-server 0.2.3最新发布版本,部分功能还在修复完善中,不足以应用于实际生产环境。因此,我们目前使用 spring-security -oauth2 作为授权服务器。spring-authorization-server后续稳定版发布后,会进行迁移升级。
【spring-security-oauth2默认实现的授权类型】:
在GitEgg微服务框架中cas登录成功之后跳转的路径 cas登录成功之后跳转的路径 ,gitegg-oauth引入了spring-security-oauth2,在代码中使用了Oauth2的密码授权和刷新token授权,并自定义扩展了【短信验证码授权类型】和【图形认证】密码授权],实际上是密码授权的扩展授权类型。
目前,基本上所有的SpringCloud微服务授权方式都使用OAuth2密码授权方式来获取token。你可能会有疑问,为什么上面最新的 Oauth2 协议不推荐甚至禁止使用密码授权类型,而我们的 GitEgg 框架系统的系统管理接口也使用密码授权方式来获取令牌呢?由于不推荐密码授权类型,第三方客户端会收集用户名和密码,存在安全隐患。在我们的例子中,我们的客户端是我们自己的系统管理界面,而不是第三方客户端。所有用户名和密码都是我们自己系统的用户名和密码。只要系统安全得到很好的保护,我们就可以最大限度的避免用户名和密码泄露给第三方的风险。
在使用spring-security-oauth2实现单点登录之前,我们首先要了解单点登录SSO和OAuth的区别和联系2、spring-security-oauth2:二、SpringSecurity单点登录-关于服务端和客户端实现流程分析单点登录业务流程时序图:
系统(单点登录客户端)首次访问受保护资源触发单点登录流程描述三、使用【Authorization Code Authorization]和【Refresh Token Authorization】实现SSO服务器1、自定义 SSO 服务器页面
当我们的 gitegg-oauth 被用作授权服务器时,我们想要自定义我们自己的登录页面和其他信息。接下来,我们自定义登录、主页、错误提示页面和密码恢复页面。其他需要的页面可以自己定义,比如授权确认页面。我们这里的业务不需要用户二次确认,所以这里没有定制这个页面。
org.springframework.boot
spring-boot-starter-thymeleaf
/**
* 单点登录-登录页
* @return
*/
@GetMapping("/login") public String login() {
return "login";
}
/**
* 单点登录-首页:当直接访问单点登录系统成功后进入的页面。从客户端系统进入的,直接返回到客户端页面
* @return
*/
@GetMapping("/index") public String index() {
return "index";
}
/**
* 单点登录-错误页
* @return
*/
@GetMapping("/error") public String error() {
return "error";
}
/**
* 单点登录-找回密码页
* @return
*/
@GetMapping("/find/pwd") public String findPwd() {
return "findpwd";
}
统一身份认证平台
<div
<div
<div
<div
<span
<span
<span
<p
GitEgg Cloud 统一身份认证平台
<div
<div
<i
<path d="M858.5 763.6c-18.9-44.8-46.1-85-80.6-119.5-34.5-34.5-74.7-61.6-119.5-80.6-0.4-0.2-0.8-0.3-1.2-0.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-0.4 0.2-0.8 0.3-1.2 0.5-44.8 18.9-85 46-119.5 80.6-34.5 34.5-61.6 74.7-80.6 119.5C146.9 807.5 137 854 136 901.8c-0.1 4.5 3.5 8.2 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c0.1 4.4 3.6 7.8 8 7.8h60c4.5 0 8.1-3.7 8-8.2-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z" p-
<div
<i
<path d="M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240z m460 600H232V536h560v304z" p- d="M484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53c12.1-8.7 20-22.9 20-39 0-26.5-21.5-48-48-48s-48 21.5-48 48c0 16.1 7.9 30.3 20 39z" p-
<button type="submit"
<div
<div
<i
<path d="M744 62H280c-35.3 0-64 28.7-64 64v768c0 35.3 28.7 64 64 64h464c35.3 0 64-28.7 64-64V126c0-35.3-28.7-64-64-64z m-8 824H288V134h448v752z" p- d="M512 784m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" p-
<div
<i
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5z" p- d="M833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6c20.2 15.7 48.5 15.7 68.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z" p-
<div
获取验证码
<button type="submit"
<div
Copyrights © 2021 GitEgg All Rights Reserved.
var countdown=60;
jQuery(function ($) {
countdown = 60;
$('.account-form').bootstrapValidator({
message: '输入错误',
feedbackIcons: {
valid: 'glyphicon glyphicon-ok',
invalid: 'glyphicon glyphicon-remove',
validating: 'glyphicon glyphicon-refresh'
},
fields: {
username: {
container: '.input-account-wrapper',
message: '输入错误',
validators: {
notEmpty: {
message: '用户账号不能为空'
},
stringLength: {
min: 2,
max: 32,
message: '账号长度范围2-32个字符。'
},
regexp: {
regexp: /^[a-zA-Z0-9_.]+$/,
message: '用户名只能由字母、数字、点和下划线组成'
}
}
},
password: {
container: '.input-psw-wrapper',
validators: {
notEmpty: {
message: '密码不能为空'
},
stringLength: {
min: 5,
max: 32,
message: '密码长度范围6-32个字符。'
}
}
}
}
});
$('.mobile-form').bootstrapValidator({
message: '输入错误',
feedbackIcons: {
valid: 'glyphicon glyphicon-ok',
invalid: 'glyphicon glyphicon-remove',
validating: 'glyphicon glyphicon-refresh'
},
fields: {
phone: {
message: '输入错误',
container: '.input-phone-wrapper',
validators: {
notEmpty: {
message: '手机号不能为空'
},
regexp: {
regexp: /^1d{10}$/,
message: '手机号格式错误'
}
}
},
code: {
container: '.input-sms-wrapper',
validators: {
notEmpty: {
message: '验证码不能为空'
},
stringLength: {
min: 6,
max: 6,
message: '验证码长度为6位。'
}
}
}
}
});
var options={
beforeSerialize: beforeFormSerialize,
success: formSuccess,//提交成功后执行的回掉函数
error: formError,//提交失败后执行的回掉函数
headers : {"TenantId" : 0},
clearForm: true,//提交成功后是否清空表单中的字段值
restForm: true,//提交成功后是否充值表单中的字段值,即恢复到页面加载是的状态
timeout: 6000//设置请求时间,超过时间后,自动退出请求,单位(毫秒)
}
var mobileOptions={
success: mobileFormSuccess,//提交成功后执行的回掉函数
error: mobileFormError,//提交失败后执行的回掉函数
headers : {"TenantId" : 0},
clearForm: true,//提交成功后是否清空表单中的字段值
restForm: true,//提交成功后是否充值表单中的字段值,即恢复到页面加载是的状态
timeout: 6000//设置请求时间,超过时间后,自动退出请求,单位(毫秒)
}
function beforeFormSerialize(){
$("#account-err").html("");
$("#username").val($.trim($("#username").val()));
$("#password").val($.md5($.trim($("#password").val())));
}
function formSuccess(response){
$(".account-form").data('bootstrapValidator').resetForm();
if (response.success)
{
window.location.href = response.targetUrl;
}
else
{
$("#account-err").html(response.message);
}
}
function formError(response){
$("#account-err").html(response);
}
function mobileFormSuccess(response){
$(".mobile-form").data('bootstrapValidator').resetForm();
if (response.success)
{
window.location.href = response.targetUrl;
}
else
{
$("#mobile-err").html(response.message);
}
}
function mobileFormError(response){
$("#mobile-err").html(response);
}
$(".account-form").ajaxForm(options);
$(".mobile-form").ajaxForm(mobileOptions);
$(".nav-left a").click(function(e){
$(".account-login").show();
$(".mobile-login").hide();
});
$(".nav-right a").click(function(e){
$(".account-login").hide();
$(".mobile-login").show();
});
$("#forget").click(function(e){
window.location.href = "/find/pwd";
});
$('.title-list li').click(function(){
var liindex = $('.title-list li').index(this);
$(this).addClass('on').siblings().removeClass('on');
$('.login_right div.login-form-container').eq(liindex).fadeIn(150).siblings('div.login-form-container').hide();
var liWidth = $('.title-list li').width();
if (liindex == 0)
{
$('.login_right .title-list p').css("transform","translate3d(0px, 0px, 0px)");
}
else {
$('.login_right .title-list p').css("transform","translate3d("+liWidth+"px, 0px, 0px)");
}
});
});
function sendCode(){
$(".mobile-form").data('bootstrapValidator').validateField('phone');
if(!$(".mobile-form").data('bootstrapValidator').isValidField("phone"))
{
return;
}
if(countdown != 60)
{
return;
}
sendmsg();
var phone = $.trim($("#phone").val());
var tenant
$.ajax({
//请求方式
type : "POST",
//请求的媒体类型
contentType: "application/x-www-form-urlencoded;charset=UTF-8",
dataType: 'json',
//请求地址
url : "/code/sms/login",
//数据,json字符串
data : {
tenantId: tenantId,
phoneNumber: phone,
code: "aliValidateLogin"
},
//请求成功
success : function(result) {
$("#smsId").val(result.data);
},
//请求失败,包含具体的错误信息
error : function(e){
console.log(e);
}
});
};
function sendmsg(){
if(countdown==0){
$("#sendBtn").css("color","#181818");
$("#sendBtn").html("获取验证码");
countdown=60;
return false;
}
else{
$("#sendBtn").css("color","#74777b");
$("#sendBtn").html("重新发送("+countdown+")");
countdown--;
}
setTimeout(function(){
sendmsg();
},1000);
}
2、授权服务器配置
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
}
# 以下配置为新增
whiteUrls:
- "/gitegg-oauth/oauth/login"
- "/gitegg-oauth/oauth/find/pwd"
- "/gitegg-oauth/oauth/error"
authUrls:
- "/gitegg-oauth/oauth/index"
whiteUrls:
- "/*/v2/api-docs"
- "/gitegg-oauth/oauth/public_key"
- "/gitegg-oauth/oauth/token_key"
- "/gitegg-oauth/find/pwd"
- "/gitegg-oauth/code/sms/login"
- "/gitegg-oauth/change/password"
- "/gitegg-oauth/error"
- "/gitegg-oauth/oauth/sms/captcha/send"
# 新增OAuth2认证接口,此处网关放行,由认证中心进行认证
tokenUrls:
- "/gitegg-oauth/oauth/token"
package com.gitegg.oauth.filter;
import cn.hutool.core.bean.BeanUtil;
import com.gitegg.oauth.token.PhoneAuthenticationToken;
import com.gitegg.platform.base.constant.AuthConstant;
import com.gitegg.platform.base.domain.GitEggUser;
import com.gitegg.platform.base.result.Result;
import com.gitegg.service.system.client.feign.IUserFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义登陆
* @author GitEgg
*/
public class GitEggLoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_RESTFUL_TYPE_PHONE = "phone";
public static final String SPRING_SECURITY_RESTFUL_TYPE_QR = "qr";
public static final String SPRING_SECURITY_RESTFUL_TYPE_DEFAULT = "user";
// 登陆类型:user:用户密码登陆;phone:手机验证码登陆;qr:二维码扫码登陆
private static final String SPRING_SECURITY_RESTFUL_TYPE_KEY = "type";
// 登陆终端:1:移动端登陆,包括微信公众号、小程序等;0:PC后台登陆
private static final String SPRING_SECURITY_RESTFUL_MOBILE_KEY = "mobile";
private static final String SPRING_SECURITY_RESTFUL_USERNAME_KEY = "username";
private static final String SPRING_SECURITY_RESTFUL_PASSWORD_KEY = "password";
private static final String SPRING_SECURITY_RESTFUL_PHONE_KEY = "phone";
private static final String SPRING_SECURITY_RESTFUL_VERIFY_CODE_KEY = "code";
private static final String SPRING_SECURITY_RESTFUL_QR_CODE_KEY = "qrCode";
@Autowired
private IUserFeign userFeign;
private boolean postOnly = true;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String type = obtainParameter(request, SPRING_SECURITY_RESTFUL_TYPE_KEY);
String mobile = obtainParameter(request, SPRING_SECURITY_RESTFUL_MOBILE_KEY);
AbstractAuthenticationToken authRequest;
String principal;
String credentials;
// 手机验证码登陆
if(SPRING_SECURITY_RESTFUL_TYPE_PHONE.equals(type)){
principal = obtainParameter(request, SPRING_SECURITY_RESTFUL_PHONE_KEY);
credentials = obtainParameter(request, SPRING_SECURITY_RESTFUL_VERIFY_CODE_KEY);
principal = principal.trim();
authRequest = new PhoneAuthenticationToken(principal, credentials);
}
// 账号密码登陆
else {
principal = obtainParameter(request, SPRING_SECURITY_RESTFUL_USERNAME_KEY);
credentials = obtainParameter(request, SPRING_SECURITY_RESTFUL_PASSWORD_KEY);
Result result = userFeign.queryUserByAccount(principal);
if (null != result && result.isSuccess()) {
GitEggUser gitEggUser = new GitEggUser();
BeanUtil.copyProperties(result.getData(), gitEggUser, false);
if (!StringUtils.isEmpty(gitEggUser.getAccount())) {
principal = gitEggUser.getAccount();
credentials = AuthConstant.BCRYPT + gitEggUser.getAccount() + credentials;
}
}
authRequest = new UsernamePasswordAuthenticationToken(principal, credentials);
}
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private void setDetails(HttpServletRequest request,
AbstractAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
private String obtainParameter(HttpServletRequest request, String parameter) {
String result = request.getParameter(parameter);
return result == null ? "" : result;
}
}
四、实现单点登录客户端
spring-security-oauth2 提供 OAuth2 授权服务器和单点登录客户端的实现。一般通过几行注释就可以实现单点登录功能。
1、新建单点登录客户端项目,引入oauth2客户端相关jar包
org.springframework.boot
spring-boot-starter-oauth2-client
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2、新建WebSecurityConfig类并添加@EnableOAuth2Sso注解
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
3、配置SSO服务器相关信息
server:
port: 8080
servlet:
context-path: /ssoclient1
security:
oauth2:
client:
# 配置在授权服务器配置的客户端id和secret
client-id: ssoclient
client-secret: 123456
# 获取token的url
access-token-uri: http://127.0.0.1/gitegg-oauth/oauth/token
# 授权服务器的授权地址
user-authorization-uri: http://127.0.0.1/gitegg-oauth/oauth/authorize
resource:
jwt:
# 获取公钥的地址,验证token需使用,系统启动时会初始化,不会每次验证都请求
key-uri: http://127.0.0.1/gitegg-oauth/oauth/token_key
评论:
1、令牌返回格式在 GitEgg 框架中自定义。Spring Security为token获取的/oauth/token默认返回一个ResponseEntity,在使用自己的系统登录和单点登录时需要进行转换。
2、Gateway认证需要的公钥地址是gitegg-oauth/oauth/public_key,单点登录客户端需要公钥地址
/oauth/token_key,两者返回的格式不同,请注意区分。
3、在请求/oauth/tonen和/oauth/token_key时,默认需要Basic认证,即需要在请求中添加client_id和client_security参数。
源地址:
吉蒂:
GitHub:
© 版权声明
本站下载的源码均来自公开网络收集转发二次开发而来,
若侵犯了您的合法权益,请来信通知我们1413333033@qq.com,
我们会及时删除,给您带来的不便,我们深表歉意。
下载用户仅供学习交流,若使用商业用途,请购买正版授权,否则产生的一切后果将由下载用户自行承担,访问及下载者下载默认同意本站声明的免责申明,请合理使用切勿商用。
THE END
喜欢就支持一下吧
暂无评论内容