以下文章来源Java架构师技术,回复”Spring“获惊喜礼包
大家好,我是Java架构师

文章目录

  • 前言
  • 一、CAS是什么?
  • 二、搭建客户端系统
    1. 引入CAS
    2. 客户端后端搭建
  • 总结

前言

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

一、CAS是什么?

CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。CAS 具有以下特点:
  • 开源的企业级单点登录解决方案。
  • CAS Server 为需要独立部署的 Web 应用。
  • CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。

二、搭建客户端系统

1.引入CAS
参考:https://www.bilibili.com/video/BV1xy4y1r7BU?t=666&p=8
注意其中将证书导入jdk中,一定要注意精确到cacerts这个文件下,不然一直报拒绝写入,另外最好用管理员下的命令窗口
2.客户端后端搭建
1.添加依赖
<dependency>
<groupId>
org.jasig.cas.client
</groupId>
<artifactId>
cas-client-core
</artifactId>
<version>
3.3.2
</version>
</dependency>
<dependency>
<groupId>
joda-time
</groupId>
<artifactId>
joda-time
</artifactId>
<version>
2.10.5
</version>
</dependency>

<dependency>
<groupId>
org.springframework.security
</groupId>
<artifactId>
spring-security-cas
</artifactId>
</dependency>

<dependency>
<groupId>
org.springframework.security
</groupId>
<artifactId>
spring-security-taglibs
</artifactId>
</dependency>
2.配置客户端
server:

  port: 1234

3.添加config(filter)文件
地址全为ip,如果用hosts映射地址,可能会出现问题
package
 com.casclient1.cas.config;


import
 org.jasig.cas.client.session.SingleSignOutFilter;

import
 org.jasig.cas.client.util.HttpServletRequestWrapperFilter;

import
 org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;

import
 org.slf4j.Logger;

import
 org.slf4j.LoggerFactory;

import
 org.springframework.beans.factory.InitializingBean;

import
 org.springframework.boot.web.servlet.FilterRegistrationBean;

import
 org.springframework.context.annotation.Bean;

import
 org.springframework.context.annotation.Configuration;

import
 org.springframework.core.annotation.Order;

import
 org.springframework.web.filter.CharacterEncodingFilter;


import
 javax.servlet.Filter;

import
 java.io.Serializable;

import
 java.util.ArrayList;

import
 java.util.List;



@Configuration
publicclassFilterConfigimplementsSerializableInitializingBean
{


privatestaticfinal
 Logger LOGGER = LoggerFactory.getLogger(FilterConfig
.class)
;


publicstaticfinal
 String CAS_SIGNOUT_FILTER_NAME = 
"CAS Single Sign Out Filter"
;

publicstaticfinal
 String CAS_AUTH_FILTER_NAME = 
"CAS Filter"
;

publicstaticfinal
 String CAS_IGNOREL_SSL_FILTER_NAME = 
"CAS Ignore SSL Filter"
;

publicstaticfinal
 String CAS_FILTER_NAME = 
"CAS Validation Filter"
;

publicstaticfinal
 String CAS_WRAPPER_NAME = 
"CAS HttpServletRequest Wrapper Filter"
;

publicstaticfinal
 String CAS_ASSERTION_NAME = 
"CAS Assertion Thread Local Filter"
;

publicstaticfinal
 String CHARACTER_ENCODING_NAME = 
"Character encoding Filter"
;


//CAS服务器退出地址
privatestatic
 String casSigntouServerUrlPrefix = 
"https://127.0.0.1:8443/cas/logout"
;

//CAS服务器登录地址
privatestatic
 String casServerLoginUrl = 
"https://127.0.0.1:8443/cas/login"
;

//客户端地址
privatestatic
 String clienthosturl=
"http://127.0.0.1:1234"
;

//CAS服务器地址
privatestatic
 String casValidationServerUrlPrefix = 
"https://127.0.0.1:8443/cas"
;


publicFilterConfig()
{


    }


/**

     * 单点登出功能,放在其他filter之前

     * casSigntouServerUrlPrefix为登出前缀:https://123.207.122.156:8081/cas/logout

     *

     * 
@return
     */

@Bean
@Order
(
0
)

public FilterRegistrationBean getCasSignoutFilterRegistrationBean()
{

        FilterRegistrationBean registration = 
new
 FilterRegistrationBean();

        registration.setFilter(getCasSignoutFilter());

        registration.addUrlPatterns(
"/*"
"*.html"
);

        registration.addInitParameter(
"casServerUrlPrefix"
, casSigntouServerUrlPrefix);

        registration.setName(CAS_SIGNOUT_FILTER_NAME);

        registration.setEnabled(
true
);

return
 registration;

    }


@Bean
(name = CAS_SIGNOUT_FILTER_NAME)

public Filter getCasSignoutFilter()
{

returnnew
 SingleSignOutFilter();

    }


/**

     * 忽略SSL认证

     *

     * 
@return
     */

@Bean
@Order
(
1
)

public FilterRegistrationBean getCasSkipSSLValidationFilterRegistrationBean()
{

        FilterRegistrationBean registration = 
new
 FilterRegistrationBean();

        registration.setFilter(getCasSkipSSLValidationFilter());

        registration.addUrlPatterns(
"/*"
"*.html"
);

        registration.setName(CAS_IGNOREL_SSL_FILTER_NAME);

        registration.setEnabled(
true
);

return
 registration;

    }


@Bean
(name = CAS_IGNOREL_SSL_FILTER_NAME)

public Filter getCasSkipSSLValidationFilter()
{

returnnew
 IgnoreSSLValidateFilter();

    }


/**

     * 负责用户的认证

     * casServerLoginUrl:https://123.207.122.156:8081/cas/login

     * casServerName:https://123.207.122.156:8080/tdw/alerts/

     *

     * 
@return
     */

@Bean
@Order
(
2
)

public FilterRegistrationBean getCasAuthFilterRegistrationBean()
{

        FilterRegistrationBean registration = 
new
 FilterRegistrationBean();

final
 Filter casAuthFilter = getCasAuthFilter();

        registration.setFilter(casAuthFilter);

        registration.addUrlPatterns(
"/*"
"*.html"
);

        registration.addInitParameter(
"casServerLoginUrl"
, casServerLoginUrl);

        registration.addInitParameter(
"serverName"
, clienthosturl);

        registration.setName(CAS_AUTH_FILTER_NAME);

        registration.setEnabled(
true
);

return
 registration;

    }


@Bean
(name = CAS_AUTH_FILTER_NAME)

public Filter getCasAuthFilter()
{

returnnew
 MyAuthenticationFilter();

    }


/**

     * 对Ticket进行校验

     * casValidationServerUrlPrefix要用内网ip

     * casValidationServerUrlPrefix:https://123.207.122.156:8081/cas

     * casServerName:https://123.207.122.156:8080/tdw/alerts/

     *

     * 
@return
     */

@Bean
@Order
(
3
)

public FilterRegistrationBean getCasValidationFilterRegistrationBean()
{

        FilterRegistrationBean registration = 
new
 FilterRegistrationBean();

final
 Filter casValidationFilter = getCasValidationFilter();

        registration.setFilter(casValidationFilter);

        registration.addUrlPatterns(
"/*"
"*.html"
);

        registration.addInitParameter(
"casServerUrlPrefix"
, casValidationServerUrlPrefix);

        registration.addInitParameter(
"serverName"
, clienthosturl);

        registration.setName(CAS_FILTER_NAME);

        registration.setEnabled(
true
);

return
 registration;

    }


@Bean
(name = CAS_FILTER_NAME)

public Filter getCasValidationFilter()
{

returnnew
 Cas20ProxyReceivingTicketValidationFilter();

    }


/**

     * 设置response的默认编码方式:UTF-8。

     *

     * 
@return
     */

@Bean
@Order
(
4
)

public FilterRegistrationBean getCharacterEncodingFilterRegistrationBean()
{

        FilterRegistrationBean registration = 
new
 FilterRegistrationBean();

        registration.setFilter(getCharacterEncodingFilter());

        registration.addUrlPatterns(
"/*"
"*.html"
);

        registration.setName(CHARACTER_ENCODING_NAME);

        registration.setEnabled(
true
);

return
 registration;

    }


@Bean
(name = CHARACTER_ENCODING_NAME)

public Filter getCharacterEncodingFilter()
{

        CharacterEncodingFilter characterEncodingFilter = 
new
 CharacterEncodingFilter();

        characterEncodingFilter.setEncoding(
"UTF-8"
);

return
 characterEncodingFilter;

    }


@Bean
public FilterRegistrationBean casHttpServletRequestWrapperFilter()
{

        FilterRegistrationBean authenticationFilter = 
new
 FilterRegistrationBean();

        authenticationFilter.setFilter(
new
 HttpServletRequestWrapperFilter());

        authenticationFilter.setOrder(
6
);

        List<String> urlPatterns = 
new
 ArrayList<>();

        urlPatterns.add(
"/*"
);

        authenticationFilter.setUrlPatterns(urlPatterns);

return
 authenticationFilter;

    }


@Override
publicvoidafterPropertiesSet()throws Exception 
{


    }

}

4.filter类中的MyAuthenticationFilter是重写cas jar包中的AuthenticationFilter,原因是CAS源码无法认证直接重定向,而ajax请求又不能直接重定向,导致前端302,而302vue response拦截器是拦截不到的。所以就想到不让cas给我重定向,给我返回状态码,告诉前端认证失败,让前端直接跳转cas服务器登录地址。扩展:接私活儿
修改cas源码过滤器,复制源码AuthenticationFilter这个过滤器,重写他,其实这里只改了重定向的代码其他都一样。上MyAuthenticationFilter代码
package
 com.casclient1.cas.config;


import
 org.jasig.cas.client.authentication.*;

import
 org.jasig.cas.client.util.AbstractCasFilter;

import
 org.jasig.cas.client.util.CommonUtils;

import
 org.jasig.cas.client.util.ReflectUtils;

import
 org.jasig.cas.client.validation.Assertion;


import
 javax.servlet.FilterConfig;

import
 javax.servlet.*;

import
 javax.servlet.http.HttpServletRequest;

import
 javax.servlet.http.HttpServletResponse;

import
 javax.servlet.http.HttpSession;

import
 java.io.IOException;

import
 java.util.HashMap;

import
 java.util.Map;


publicclassMyAuthenticationFilterextendsAbstractCasFilter
{

private
 String casServerLoginUrl;

privateboolean
 renew = 
false
;

privateboolean
 gateway = 
false
;

private
 GatewayResolver gatewayStorage = 
new
 DefaultGatewayResolverImpl();

private
 AuthenticationRedirectStrategy authenticationRedirectStrategy = 
new
 DefaultAuthenticationRedirectStrategy();

private
 UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = 
null
;

privatestaticfinal
 Map<String, Class<? extends UrlPatternMatcherStrategy>> PATTERN_MATCHER_TYPES = 
new
 HashMap();


publicMyAuthenticationFilter()
{

    }


@Override
protectedvoidinitInternal(FilterConfig filterConfig)throws ServletException 
{

if
 (!
this
.isIgnoreInitConfiguration()) {

super
.initInternal(filterConfig);

this
.setCasServerLoginUrl(
this
.getPropertyFromInitParams(filterConfig, 
"casServerLoginUrl"
, (String)
null
));

this
.logger.trace(
"Loaded CasServerLoginUrl parameter: {}"
this
.casServerLoginUrl);

this
.setRenew(
this
.parseBoolean(
this
.getPropertyFromInitParams(filterConfig, 
"renew"
"false"
)));

this
.logger.trace(
"Loaded renew parameter: {}"
this
.renew);

this
.setGateway(
this
.parseBoolean(
this
.getPropertyFromInitParams(filterConfig, 
"gateway"
"false"
)));

this
.logger.trace(
"Loaded gateway parameter: {}"
this
.gateway);

            String ignorePattern = 
this
.getPropertyFromInitParams(filterConfig, 
"ignorePattern"
, (String)
null
);

this
.logger.trace(
"Loaded ignorePattern parameter: {}"
, ignorePattern);

            String ignoreUrlPatternType = 
this
.getPropertyFromInitParams(filterConfig, 
"ignoreUrlPatternType"
"REGEX"
);

this
.logger.trace(
"Loaded ignoreUrlPatternType parameter: {}"
, ignoreUrlPatternType);

if
 (ignorePattern != 
null
) {

                Class<? extends UrlPatternMatcherStrategy> ignoreUrlMatcherClass = (Class)PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType);

if
 (ignoreUrlMatcherClass != 
null
) {

this
.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy) ReflectUtils.newInstance(ignoreUrlMatcherClass.getName(), 
new
 Object[
0
]);

                } 
else
 {

try
 {

this
.logger.trace(
"Assuming {} is a qualified class name..."
, ignoreUrlPatternType);

this
.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy)ReflectUtils.newInstance(ignoreUrlPatternType, 
new
 Object[
0
]);

                    } 
catch
 (IllegalArgumentException var6) {

this
.logger.error(
"Could not instantiate class [{}]"
, ignoreUrlPatternType, var6);

                    }

                }


if
 (
this
.ignoreUrlPatternMatcherStrategyClass != 
null
) {

this
.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern);

                }

            }


            String gatewayStorageClass = 
this
.getPropertyFromInitParams(filterConfig, 
"gatewayStorageClass"
, (String)
null
);

if
 (gatewayStorageClass != 
null
) {

this
.gatewayStorage = (GatewayResolver)ReflectUtils.newInstance(gatewayStorageClass, 
new
 Object[
0
]);

            }


            String authenticationRedirectStrategyClass = 
this
.getPropertyFromInitParams(filterConfig, 
"authenticationRedirectStrategyClass"
, (String)
null
);

if
 (authenticationRedirectStrategyClass != 
null
) {

this
.authenticationRedirectStrategy = (AuthenticationRedirectStrategy)ReflectUtils.newInstance(authenticationRedirectStrategyClass, 
new
 Object[
0
]);

            }

        }


    }


@Override
publicvoidinit()
{

super
.init();

        CommonUtils.assertNotNull(
this
.casServerLoginUrl, 
"casServerLoginUrl cannot be null."
);

    }


@Override
publicfinalvoiddoFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException 
{

        HttpServletRequest request = (HttpServletRequest)servletRequest;

        HttpServletResponse response = (HttpServletResponse)servletResponse;

if
 (
this
.isRequestUrlExcluded(request)) {

this
.logger.debug(
"Request is ignored."
);

            filterChain.doFilter(request, response);

        } 
else
 {

            HttpSession session = request.getSession(
false
);

            Assertion assertion = session != 
null
 ? (Assertion)session.getAttribute(
"_const_cas_assertion_"
) : 
null
;

if
 (assertion != 
null
) {

                filterChain.doFilter(request, response);

            } 
else
 {

                String serviceUrl = 
this
.constructServiceUrl(request, response);

                String ticket = 
this
.retrieveTicketFromRequest(request);

boolean
 wasGatewayed = 
this
.gateway && 
this
.gatewayStorage.hasGatewayedAlready(request, serviceUrl);

if
 (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {

this
.logger.debug(
"no ticket and no assertion found"
);

                    String modifiedServiceUrl;

if
 (
this
.gateway) {

this
.logger.debug(
"setting gateway attribute in session"
);

                        modifiedServiceUrl = 
this
.gatewayStorage.storeGatewayInformation(request, serviceUrl);

                    } 
else
 {

                        modifiedServiceUrl = serviceUrl;

                    }


this
.logger.debug(
"Constructed service url: {}"
, modifiedServiceUrl);


                    String xRequested =request.getHeader(
"x-requested-with"
);

if
(
"XMLHttpRequest"
.equals(xRequested)){

                        response.getWriter().write(
"{\"code\":202, \"msg\":\"no ticket and no assertion found\"}"
);

                    }
else
{

                        String urlToRedirectTo = CommonUtils.constructRedirectUrl(
this
.casServerLoginUrl, 
this
.getServiceParameterName(), modifiedServiceUrl, 
this
.renew, 
this
.gateway);

this
.logger.debug(
"redirecting to \"{}\""
, urlToRedirectTo);

this
.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);

                    }

                } 
else
 {

                    filterChain.doFilter(request, response);

                }

            }

        }

    }


publicfinalvoidsetRenew(boolean renew)
{

this
.renew = renew;

    }


publicfinalvoidsetGateway(boolean gateway)
{

this
.gateway = gateway;

    }


publicfinalvoidsetCasServerLoginUrl(String casServerLoginUrl)
{

this
.casServerLoginUrl = casServerLoginUrl;

    }


publicfinalvoidsetGatewayStorage(GatewayResolver gatewayStorage)
{

this
.gatewayStorage = gatewayStorage;

    }


privatebooleanisRequestUrlExcluded(HttpServletRequest request)
{

if
 (
this
.ignoreUrlPatternMatcherStrategyClass == 
null
) {

returnfalse
;

        } 
else
 {

            StringBuffer urlBuffer = request.getRequestURL();

if
 (request.getQueryString() != 
null
) {

                urlBuffer.append(
"?"
).append(request.getQueryString());

            }


            String requestUri = urlBuffer.toString();

returnthis
.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);

        }

    }


static
 {

        PATTERN_MATCHER_TYPES.put(
"CONTAINS"
, ContainsPatternUrlPatternMatcherStrategy
.class)
;

        PATTERN_MATCHER_TYPES.put(
"REGEX"
, RegexUrlPatternMatcherStrategy
.class)
;

        PATTERN_MATCHER_TYPES.put(
"EXACT"
, ExactUrlPatternMatcherStrategy
.class)
;

    }

}

测试Controller
package
 com.casclient1.cas.controller;


import
 com.casclient1.cas.domain.UserDomain;

import
 com.casclient1.cas.tools.Result;

import
 org.springframework.stereotype.Controller;

import
 org.springframework.web.bind.annotation.GetMapping;

import
 org.springframework.web.bind.annotation.RequestMapping;

import
 org.springframework.web.bind.annotation.ResponseBody;


import
 javax.servlet.http.HttpServletRequest;

import
 javax.servlet.http.HttpServletResponse;

import
 java.io.Console;

import
 java.io.IOException;


@Controller
publicclassTestController
{

/**

     * 测试

     * 
@return
     */

@GetMapping
(
"/test"
)

@ResponseBody
public Result<UserDomain>  login(HttpServletRequest httpServletRequest)
{

        System.out.println(
"sss"
);

returnnew
 Result<>(
new
 UserDomain(httpServletRequest.getRemoteUser()));

    }


@GetMapping
(
"/checkTicket"
)

publicvoidindex(HttpServletResponse response)throws IOException 
{

// 前端页面地址
        response.sendRedirect(
"http://127.0.0.1:8088/Home"
);

    }

/**

     * 注销

     * 
@return
     */

@RequestMapping
(
"/logout"
)

public String logout()
{

return"redirect:https://127.0.0.1:8443/cas/logout"
;

    }

    }

3.前端
<template xmlns=
"http://www.w3.org/1999/html"
>

<div >

<headerstyle="height: 60px">
<span>
客户端2验证:{{name}}
</span>
<button @click="logout">
安全退出
</button>
</header>
<router-view></router-view>
<!--


  <my-vue v-bind:lineID="lineID"></my-vue>-->

</div>

<
/template>

<style lang="scss">

</
style>

<scripttype="text/ecmascript-6">

exportdefault
 {


    data() {


return
 {

name
:
'ss'
      }


    },

mounted(){

var
 _this = 
this
;

this
.$http.get(
'/test'
, {
headers
: {
'x-requested-with'
'XMLHttpRequest'
}})

            .then(
function (response
{

console
.log(
"sss"
);

if
 (response.data.code === 
202
) {

debugger
console
.log(
"sss"
);

window
.location.href = 
"http://127.0.0.1:1235/checkTicket"
                } 
elseif
 (response.data.code === 
200
) {

console
.log(
"sss"
);

                    _this.name = response.data.data.name

                }

console
.log(response);

            })

            .catch(
function (error
{

console
.log(error);

            });

        },

methods
: {

          logout() {

window
.location.href = 
"http://127.0.0.1:1234/logout"

          },

      }

  }

</script>

5.效果
未登录:
点击客户端1超链接
登录成功
点击客户端2超链接,直接进入,无需登录。另外,搜索公众号Linux就该这样学后台回复“git书籍”,获取一份惊喜礼包。
退出

总结

网上有很多CAS单点登录的demo,但是对于前后端分离讲的比较详细的很少,前后端分离,必定会出现跨域,导致CAS登录无法重定向等等原因,结合和网上一些想法和部门代码后,大致做了一个比较完善,但很基础的单点登录系统,当然单点登录不光有CAS,还有JWT(1.所有服务靠约定来生成token,2.要么集中生成集中判断,所有服务都能生成都认这个,要么一个服务管控全局),OAuth2等等。
欢迎有需要的同学试试,如果本文对您有帮助,也请帮忙点个 赞 + 在看 啦!❤️
在 GitHub猿 还有更多优质项目系统学习资源,欢迎分享给其他同学吧!
最后,整理了100多套项目,赠送读者。扫码下方二维码,后台回复赚钱即可获取。
--END--
继续阅读
阅读原文