HZERO-CAS单点登录

源码地址: https://gitee.com/DiDiaoXiaoMoLong/sso_server

0.文档介绍

文档以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

SSO简介

单点登录( Single Sign-On , 简称 SSO )是多个相关但独立的软件系统访问控制的一个属性。通过使用该属性,用户登录与单个ID和密码来访问所连接的一个或多个系统,而不使用不同的用户名或密码,或在某些配置中无缝登录在每个系统上,它是比较流行的服务于企业业务整合的一种解决方案。总结一句话,SSO 使得在多个应用系统中,用户只需要 **登录一次 **就可以访问所有相互信任的应用系统。

举个栗子:阿里巴巴旗下的淘宝网,你在浏览器里登录了,打开阿里云或者天猫就会发现可以不用在登录了,这里就是使用了SSO。

在SSO体系中,主要包括三部分:

User (多个)
Web 应用(多个)
SSO 认证中心( 1 个)
而SSO的实现基本核心原则如下:

所有的登录都在 SSO 认证中心进行
SSO 认证中心通过一些方法来告诉 Web 应用当前访问用户究竟是不是已通过认证的用户
SSO 认证中心和所有的 Web 应用建立一种信任关系, SSO 认证中心对用户身份正确性的判断会通过某种方法告之 Web 应用,而且判断结果必须被 Web 应用信任。

1.域名映射

修改/etc/hosts文件,添加服务端域名(server.cas.com) 以及两个客户端的域名(app1.cas.com , app2.cas.com)

2.编译

解压zip,命令行进去,执行mvn clean package
结束之后会出现 target 文件夹,里面有一个cas.war包,这个war包就是我们要运行的程序。

3.本地配置tomcat通过https访问

3.1生成keystore

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 

3.2根据keystore生成crt文件

#输入第一步中keystore的密码changeit
keytool -export -alias tomcat -file C:/opt/keys/tomcat.cer -keystore C:/opt/keys/tomcat.keystore -validity 3650

3.3信任授权文件到jdk

sudo keytool -import -keystore E:/java/jdk/jre/lib/security/cacerts -file C:/opt/keys/tomcat.cer -alias tomcat -storepass changeit

证书库cacerts的缺省口令为changeit ,这也是为什么我上面的密码都是用的它,防止混淆,直接都设成一样的。

3.4查看cacerts中证书

keytool -list -v -keystore E:/java/jdk/jre/lib/security/cacerts

3.5 浏览器信任证书

HZERO-CAS单点登录_第1张图片

3.5修改tomcat的配置文件server.xml

<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"/>

3.6修改application.properties

拉取源码:https://github.com/apereo/cas-overlay-template版本5.3.14
HZERO-CAS单点登录_第2张图片

##
# 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

3.7 配置IDE的tomcat

HZERO-CAS单点登录_第3张图片

HZERO-CAS单点登录_第4张图片

3.8 访问首页

https://server.cas.com:8443/cas/login

输入application.properties对应的账号和密码

cas.authn.accept.users=casuser::Mellon

HZERO-CAS单点登录_第5张图片

输入已配置的账号密码casuser/Mellon

HZERO-CAS单点登录_第6张图片

3.9 配置支持http请求进行单点使用CAS服务器

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

4.配置CAS服务器从数据库获取用户信息

加入依赖


        <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

4.1数据库信息

HZERO-CAS单点登录_第7张图片

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');

4.2使用数据库访问cas

HZERO-CAS单点登录_第8张图片

HZERO-CAS单点登录_第9张图片

4.3设置加密

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

4.4 退出时跳转service指定的地址

##
# Logout配置
#
cas.logout.followServiceRedirects=true

5.客户端单点登录(SpringBoot+Spring Security+CAS)

5.1 通过阿里云脚手架生成个基础模板

HZERO-CAS单点登录_第10张图片

5.2 配置依赖pom文件


<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>

5.3 配置application.properties

######################## 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

5.4 新建CasProperties

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;

}

5.5 新建UserInfo,记录客户端用户信息

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 + "]";
    }
}

5.6 SecurityConfig核心配置

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;
    }
}

5.7 加载用户信息配置

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;
    }

}

5.8 测试

1.访问http://localhost:8081/hello

HZERO-CAS单点登录_第11张图片
没有权限跳转到CAS单点登录授权平台授权

2.跳转https://server.cas.com:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8081%2Flogin等待输入账号密码

输入CAS授权服务器密码admin/123456

HZERO-CAS单点登录_第12张图片

3.登录成功,单点服务器颁发ticket返回客户端

HZERO-CAS单点登录_第13张图片

4.客户端拿到tiket进行用户认证

HZERO-CAS单点登录_第14张图片

5.认证成功,授权成功,访问资源

HZERO-CAS单点登录_第15张图片

6.用户输入http://localhost:8081/logout退出登录

HZERO-CAS单点登录_第16张图片

7.再次访问资源需要重新登录

HZERO-CAS单点登录_第17张图片

6.流程分析

6.1 cas校验流程

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返回对应的用户信息,业务系统再对该用户进行身份映射再次认证,则完成本次登录

7.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认证中心,全局会话与局部会话有如下约束关系:

局部会话存在,全局会话一定存在
全局会话存在,局部会话不一定存在
全局会话销毁,局部会话必须销毁
既然有登陆那么就自然有注销,单点登录也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁。原理图如下:

HZERO-CAS单点登录_第18张图片

SSO认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作

同样的我们也来分析一下具体的流程:

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

8.手写CAS实现

只是简要介绍下基于java的实现过程,不提供完整源码,明白了原理,我相信你们可以自己实现。sso采用客户端/服务端架构,我们先看sso-client与sso-server要实现的功能(下面:sso认证中心=sso-server)

sso-client

  1. 拦截子系统未登录用户请求,跳转至sso认证中心
  2. 接收并存储sso认证中心发送的令牌
  3. 与sso-server通信,校验令牌的有效性
  4. 建立局部会话
  5. 拦截用户注销请求,向sso认证中心发送注销请求
  6. 接收sso认证中心发出的注销请求,销毁局部会话

sso-server

  1. 验证用户的登录信息
  2. 创建全局会话
  3. 创建授权令牌
  4. 与sso-client通信发送令牌
  5. 校验sso-client令牌有效性
  6. 系统注册
  7. 接收sso-client注销请求,注销所有会话

9.整合HZERO实现单点登录

9.1 单点登录配置

HZERO-CAS单点登录_第19张图片

这里保存的数据会被刷新到缓存,只有缓存中有单点配置才会触发单点登录。

检查application.yml中配置是否启用

hzero.sso.enabled=true

9.2 缓存

域名保存的时候会将域名配置刷新进缓存

HZERO-CAS单点登录_第20张图片

登录授权的时候只会检测缓存中是否存在相关sso配置。

9.3 测试

HZERO-CAS单点登录_第21张图片

你可能感兴趣的:(单点登录,HZERO)