源码地址: https://gitee.com/DiDiaoXiaoMoLong/sso_server
文档以CAS作为单点登录服务器主要介绍了以下方面
1.配置HTTPS搭建单点登录服务器
2.使用数据库构建用户授权中心
3.基于SpringBoot+SpringSecurity+CAS搭建客户端单点登录连接已配置的服务器
4.如何支持HTTP客户端连接HTTPS单点服务器
5.整合HZERO,基于HZERO+CAS的单点登录实现
在多服务统一帐号的应用集中,单点登录是必不可少的。CAS就是成熟的单点登录框架之一。Github地址 https://github.com/apereo/cas。现在我们就通过一系列快速简单的构建方式实现一个简单的单点登录系统集。
首先下载cas,下载最新版本 https://github.com/apereo/cas-overlay-template
单点登录( Single Sign-On , 简称 SSO )是多个相关但独立的软件系统访问控制的一个属性。通过使用该属性,用户登录与单个ID和密码来访问所连接的一个或多个系统,而不使用不同的用户名或密码,或在某些配置中无缝登录在每个系统上,它是比较流行的服务于企业业务整合的一种解决方案。总结一句话,SSO 使得在多个应用系统中,用户只需要 **登录一次 **就可以访问所有相互信任的应用系统。
举个栗子:阿里巴巴旗下的淘宝网,你在浏览器里登录了,打开阿里云或者天猫就会发现可以不用在登录了,这里就是使用了SSO。
在SSO体系中,主要包括三部分:
User (多个)
Web 应用(多个)
SSO 认证中心( 1 个)
而SSO的实现基本核心原则如下:
所有的登录都在 SSO 认证中心进行
SSO 认证中心通过一些方法来告诉 Web 应用当前访问用户究竟是不是已通过认证的用户
SSO 认证中心和所有的 Web 应用建立一种信任关系, SSO 认证中心对用户身份正确性的判断会通过某种方法告之 Web 应用,而且判断结果必须被 Web 应用信任。
修改/etc/hosts文件,添加服务端域名(server.cas.com) 以及两个客户端的域名(app1.cas.com , app2.cas.com)
解压zip,命令行进去,执行mvn clean package
结束之后会出现 target 文件夹,里面有一个cas.war包,这个war包就是我们要运行的程序。
keytool -genkey -alias tomcat -keyalg RSA -validity 3650 -keystore C:/opt/keys/tomcat.keystore
-alias tomcat :表示秘钥库的别名是tomcat,实际操作都用别名识别,所以这个参数很重要。
-validity 3650 : 表示证书有效期10年。
秘钥库口令 我输入的是 changeit 。
名字与姓氏输入服务器域名,其它一路回车,最后如果显示正确 输入 ‘y’ 就行了。
之后可以使用以下命令查看生成秘钥库的文件内容:
keytool -list -keystore C:/opt/keys/tomcat.keystore
#输入第一步中keystore的密码changeit
keytool -export -alias tomcat -file C:/opt/keys/tomcat.cer -keystore C:/opt/keys/tomcat.keystore -validity 3650
sudo keytool -import -keystore E:/java/jdk/jre/lib/security/cacerts -file C:/opt/keys/tomcat.cer -alias tomcat -storepass changeit
证书库cacerts的缺省口令为changeit ,这也是为什么我上面的密码都是用的它,防止混淆,直接都设成一样的。
keytool -list -v -keystore E:/java/jdk/jre/lib/security/cacerts
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="200" SSLEnabled="true" scheme="https"
secure="true" clientAuth="false" sslProtocol="TLS"
keystoreFile="C:/opt/keys/tomcat.keystore"
keystorePass="changeit"/>
拉取源码:https://github.com/apereo/cas-overlay-template版本5.3.14
##
# CAS Server Context Configuration
#
server.context-path=/cas
server.port=8443
server.ssl.key-store=file:C:/opt/keys/tomcat.keystore
server.ssl.key-store-password=199707
server.ssl.key-password=199707
##
# CAS Authentication Credentials
#
cas.authn.accept.users=casuser::Mellon
https://server.cas.com:8443/cas/login
输入application.properties对应的账号和密码
cas.authn.accept.users=casuser::Mellon
输入已配置的账号密码casuser/Mellon
1.修改WEB-INF/classes/services/HTTPSandIMAPS-10000001.json
"serviceId" : "^(https|imaps)://.*",
修改为
"serviceId" : "^(https|http|imaps)://.*",
2.在WEB-INF/classes/application.properties添加配置
cas.tgc.secure=false
cas.serviceRegistry.initFromJson=true
加入依赖
<dependency>
<groupId>org.apereo.casgroupId>
<artifactId>cas-server-support-jdbcartifactId>
<version>${cas.version}version>
dependency>
<dependency>
<groupId>org.apereo.casgroupId>
<artifactId>cas-server-support-jdbc-driversartifactId>
<version>${cas.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.9version>
dependency>
编辑WEB-INF/classes/application.properties,加入配置:
##
# CAS Authentication Credentials
#
#cas.authn.accept.users=casuser::Mellon
cas.authn.jdbc.query[0].sql=SELECT * FROM cas_user_base WHERE user_name=?
cas.authn.jdbc.query[0].url=jdbc:mysql://172.28.8.102:3306/hzero_study?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=UTC
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.jdbc.query[0].user=hzero
# 此次是数据库密码
cas.authn.jdbc.query[0].password=hzero
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
cas.authn.jdbc.query[0].fieldPassword=user_psd
create table hzero_study.cas_user_base
(
id int auto_increment
primary key,
user_name varchar(45) null,
user_psd varchar(45) null
);
INSERT INTO hzero_study.cas_user_base (id, user_name, user_psd) VALUES (1, 'admin', '123456');
INSERT INTO hzero_study.cas_user_base (id, user_name, user_psd) VALUES (2, 'guest', '123456');
cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
#SHA加密的策略
cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=SHA
##
# Logout配置
#
cas.logout.followServiceRedirects=true
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.pigicgroupId>
<artifactId>demo-cas-clientartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>demo-cas-clientname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.0.6.RELEASEspring-boot.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.jasig.cas.clientgroupId>
<artifactId>cas-client-coreartifactId>
<version>3.5.0version>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-casartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-taglibsartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>${spring-boot.version}version>
plugin>
plugins>
build>
project>
######################## common config : ########################
spring.application.name=demo-cas-client
server.port=8081
management.endpoints.jmx.exposure.include=*
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
######################## CAS单点登录 : ########################
#CAS服务地址
security.cas.server.host.url=https://server.cas.com:8443/cas
#CAS服务登录地址
security.cas.server.host.login_url=${security.cas.server.host.url}/login
#CAS服务登出地址
security.cas.server.host.logout_url=${security.cas.server.host.url}/logout?service=${security.app.server.host.url}
#应用访问地址
security.app.server.host.url=http://localhost:8081
#应用登录地址
security.app.login.url=/login
#应用登出地址
security.app.logout.url=/logout
package com.pigic.democasclient.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* CAS的配置参数
*/
@Getter
@Setter
@Component
public class CasProperties {
@Value("${security.cas.server.host.url}")
private String casServerUrl;
@Value("${security.cas.server.host.login_url}")
private String casServerLoginUrl;
@Value("${security.cas.server.host.logout_url}")
private String casServerLogoutUrl;
@Value("${security.app.server.host.url}")
private String appServerUrl;
@Value("${security.app.login.url}")
private String appLoginUrl;
@Value("${security.app.logout.url}")
private String appLogoutUrl;
}
package com.pigic.democasclient.config;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* 用户信息
*/
public class UserInfo implements UserDetails {
private static final long serialVersionUID = -1041327031937199938L;
private String id;
private String username;
private String email;
private String countryCode;
private String mobile;
private String nickname;
private String role;
private String realName;
private String password;
private boolean isAccountNonExpired = true; //是否过期
private boolean isAccountNonLocked = true;//账户未锁定为true
private boolean isCredentialsNonExpired = true;//证书不过期为true
private boolean isEnabled = true;//是否可用
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(this.getRole() == null || this.getRole().length() <1){
return AuthorityUtils.commaSeparatedStringToAuthorityList("");
}
else{
return AuthorityUtils.commaSeparatedStringToAuthorityList(this.getRole());
}
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getCountryCode() {
return countryCode;
}
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
public void setAccountNonExpired(boolean isAccountNonExpired) {
this.isAccountNonExpired = isAccountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
public void setAccountNonLocked(boolean isAccountNonLocked) {
this.isAccountNonLocked = isAccountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return isCredentialsNonExpired;
}
public void setCredentialsNonExpired(boolean isCredentialsNonExpired) {
this.isCredentialsNonExpired = isCredentialsNonExpired;
}
@Override
public boolean isEnabled() {
return isEnabled;
}
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
}
@Override
public String toString() {
return "UserInfo [id=" + id + ", username=" + username + ", email=" + email + ", countryCode=" + countryCode
+ ", mobile=" + mobile + ", nickname=" + nickname + ", role=" + role + ", realName=" + realName
+ ", password=" + password + ", isAccountNonExpired=" + isAccountNonExpired + ", isAccountNonLocked="
+ isAccountNonLocked + ", isCredentialsNonExpired=" + isCredentialsNonExpired + ", isEnabled="
+ isEnabled + "]";
}
}
package com.pigic.democasclient.config;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
/**
* Security的配置
*/
@Configuration
@EnableWebSecurity //启用web权限
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CasProperties casProperties;
/**定义认证用户信息获取来源,密码校验规则等*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.authenticationProvider(casAuthenticationProvider());
//inMemoryAuthentication 从内存中获取
//auth.inMemoryAuthentication().withUser("chengli").password("123456").roles("USER")
//.and().withUser("admin").password("123456").roles("ADMIN");
//jdbcAuthentication从数据库中获取,但是默认是以security提供的表结构
//usersByUsernameQuery 指定查询用户SQL
//authoritiesByUsernameQuery 指定查询权限SQL
//auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery(query).authoritiesByUsernameQuery(query);
//注入userDetailsService,需要实现userDetailsService接口
//auth.userDetailsService(userDetailsService);
}
/**定义安全策略*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//配置安全策略
.antMatchers("/test","/login").permitAll()//定义/请求不需要验证
.and()
.authorizeRequests().anyRequest().authenticated()//其余的所有请求都需要验证
.and()
.logout()
.permitAll()//定义logout不需要验证
.and()
.formLogin();//使用form表单登录
http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint())
.and()
.addFilter(casAuthenticationFilter())
.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
//http.csrf().disable(); //禁用CSRF
}
/**认证的入口*/
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
/**指定service相关信息*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
serviceProperties.setSendRenew(false);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/**CAS认证过滤器*/
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
return casAuthenticationFilter;
}
/**cas 认证 Provider*/
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService());
//casAuthenticationProvider.setUserDetailsService(customUserDetailsService()); //这里只是接口类型,实现的接口不一样,都可以的。
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(ticketValidator());
casAuthenticationProvider.setKey("casAuthenticationProviderKey");
return casAuthenticationProvider;
}
/*@Bean
public UserDetailsService customUserDetailsService(){
return new CustomUserDetailsService();
}*/
/**用户自定义的AuthenticationUserDetailsService*/
@Bean
public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService(){
return new CustomUserDetailsService();
}
@Bean
public TicketValidator ticketValidator() {
return new Cas30ServiceTicketValidator(casProperties.getCasServerUrl());
}
/**单点登出过滤器*/
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(casProperties.getCasServerUrl());
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
/**请求单点退出过滤器*/
@Bean
public LogoutFilter casLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
return logoutFilter;
}
}
package com.pigic.democasclient.config;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 用于加载用户信息 实现UserDetailsService接口,或者实现AuthenticationUserDetailsService接口
*/
@Service
public class CustomUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
// 结合具体的逻辑去实现用户认证,并返回继承UserDetails的用户对象;
System.out.println("当前的用户名是:"+token.getName());
//获取用户信息
UserInfo userInfo = new UserInfo();
userInfo.setUsername(token.getName());
userInfo.setRole("ROLE_USER");
Map<String, Object> userAttributess = token.getAssertion().getPrincipal().getAttributes();
//System.out.println(userAttributess.toString());
if (userAttributess != null) {
userInfo.setId( String.valueOf(userAttributess.get("id")));
userInfo.setNickname( String.valueOf(userAttributess.get("nickname")));
userInfo.setRealName( String.valueOf(userAttributess.get("real_name")));
userInfo.setEmail( String.valueOf(userAttributess.get("email")));
userInfo.setCountryCode( String.valueOf(userAttributess.get("country_code")));
}
System.out.println(userInfo.toString());
return userInfo;
}
}
输入CAS授权服务器密码admin/123456
1.用户访问业务系统(client)
2.无权限时跳转到cas(服务端),并且携带参数为service=${客户端url},所以我们看到登录页时浏览器路径为 https://server.cas.com:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8081%2Flogin
3.当用户在cas处登录完后,会颁发一个ticket带回client端,如http://localhost:8081/login?ticket=ST-2-VnIQ8G0JO2-1-rGlYcQ6ajEiHA0DESKTOP-UAP7ROF
4.业务系统获取到ticket后,拿这个ticket请求cas获取对应的登录用户
cas返回对应的用户信息,业务系统再对该用户进行身份映射再次认证,则完成本次登录
在SSO中有一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。那其他的系统如何访问受保护的资源?这里就是通过认证中心间接授权通过令牌来实现,当SSO验证了用户信息的正确性后,就会创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8PHiwlYn-1587275547317)(…/Image/20180627215422759.png)]
上面是一张SSO登录原理图,下面我们来分析一下具体的流程:
首先用户访问系统1受保护的资源,系统1发现未登陆,跳转至SSO认证中心,并将自己的参数传递过去
SSO认证中心发现用户未登录,将用户引导至登录页面
用户输入用户名和密码提交至SSO认证中心
SSO认证中心校验用户信息,创建用户与SSO认证中心之间的会话,称为全局会话,同时创建授权令牌
SSO认证中心带着令牌跳转会最初的请求地址(系统1)
系统1拿到令牌,去SSO认证中心校验令牌是否有效
SSO认证中心校验令牌,返回有效,注册系统1的地址
系统1使用该令牌创建与用户的会话,称为局部会话,返回给用户受保护资源
用户访问系统2受保护的资源
系统2发现用户未登录,跳转至SSO认证中心,并将自己的地址作为参数传递过去
SSO认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
系统2拿到令牌,去SSO认证中心校验令牌是否有效
SSO认证中心校验令牌,返回有效,注册系统2地址
系统2使用该令牌创建与用户的局部会话,返回给用户受保护资源
用户登录成功之后,会与SSO认证中心及各个子系统建立会话,用户与SSO认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过SSO认证中心,全局会话与局部会话有如下约束关系:
局部会话存在,全局会话一定存在
全局会话存在,局部会话不一定存在
全局会话销毁,局部会话必须销毁
既然有登陆那么就自然有注销,单点登录也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁。原理图如下:
SSO认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作
同样的我们也来分析一下具体的流程:
用户向系统1发起注销请求
系统1根据用户与系统1建立的会话id拿到令牌,向SSO认证中心发起注销请求
SSO认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
SSO认证中心向所有注册系统发起注销请求
各注册系统接收SSO认证中心的注销请求,销毁局部会话
SSO认证中心引导用户至登录页面
只是简要介绍下基于java的实现过程,不提供完整源码,明白了原理,我相信你们可以自己实现。sso采用客户端/服务端架构,我们先看sso-client与sso-server要实现的功能(下面:sso认证中心=sso-server)
sso-client
sso-server
这里保存的数据会被刷新到缓存,只有缓存中有单点配置才会触发单点登录。
检查application.yml中配置是否启用
hzero.sso.enabled=true
域名保存的时候会将域名配置刷新进缓存
登录授权的时候只会检测缓存中是否存在相关sso配置。