查看原文
其他

SpringBoot+Vue+CAS 前后端分离实现单点登录方案

点击关注 👉 Java架构师技术 2023-09-18
以下文章来源Java架构师技术,回复”Spring“获惊喜礼包
上一篇推文:VPN 的技术原理是什么?

大家好,我是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映射地址,可能会出现问题

    牛逼啊!接私活必备的 N 个开源项目!赶快收藏吧

    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
    public class FilterConfig implements Serializable, InitializingBean {

        private static final Logger LOGGER = LoggerFactory.getLogger(FilterConfig.class);

        public static final String CAS_SIGNOUT_FILTER_NAME = "CAS Single Sign Out Filter";
        public static final String CAS_AUTH_FILTER_NAME = "CAS Filter";
        public static final String CAS_IGNOREL_SSL_FILTER_NAME = "CAS Ignore SSL Filter";
        public static final String CAS_FILTER_NAME = "CAS Validation Filter";
        public static final String CAS_WRAPPER_NAME = "CAS HttpServletRequest Wrapper Filter";
        public static final String CAS_ASSERTION_NAME = "CAS Assertion Thread Local Filter";
        public static final String CHARACTER_ENCODING_NAME = "Character encoding Filter";

     //CAS服务器退出地址
        private static String casSigntouServerUrlPrefix = "https://127.0.0.1:8443/cas/logout";
        //CAS服务器登录地址
        private static String casServerLoginUrl = "https://127.0.0.1:8443/cas/login";
        //客户端地址
        private static String clienthosturl="http://127.0.0.1:1234";
        //CAS服务器地址
        private static String casValidationServerUrlPrefix = "https://127.0.0.1:8443/cas";

        public FilterConfig() {

        }

        /**
         * 单点登出功能,放在其他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() {
            return new 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() {
            return new 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() {
            return new 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() {
            return new 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
        public void afterPropertiesSet() 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;

    public class MyAuthenticationFilter extends AbstractCasFilter {
        private String casServerLoginUrl;
        private boolean renew = false;
        private boolean gateway = false;
        private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
        private AuthenticationRedirectStrategy authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy();
        private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = null;
        private static final Map<String, Class<? extends UrlPatternMatcherStrategy>> PATTERN_MATCHER_TYPES = new HashMap();

        public MyAuthenticationFilter() {
        }

        @Override
        protected void initInternal(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
        public void init() {
            super.init();
            CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
        }

        @Override
        public final void doFilter(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);
                    }
                }
            }
        }

        public final void setRenew(boolean renew) {
            this.renew = renew;
        }

        public final void setGateway(boolean gateway) {
            this.gateway = gateway;
        }

        public final void setCasServerLoginUrl(String casServerLoginUrl) {
            this.casServerLoginUrl = casServerLoginUrl;
        }

        public final void setGatewayStorage(GatewayResolver gatewayStorage) {
            this.gatewayStorage = gatewayStorage;
        }

        private boolean isRequestUrlExcluded(HttpServletRequest request) {
            if (this.ignoreUrlPatternMatcherStrategyClass == null) {
                return false;
            } else {
                StringBuffer urlBuffer = request.getRequestURL();
                if (request.getQueryString() != null) {
                    urlBuffer.append("?").append(request.getQueryString());
                }

                String requestUri = urlBuffer.toString();
                return this.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
    public class TestController {
        /**
         * 测试
         * @return
         */

        @GetMapping("/test")
        @ResponseBody
        public Result<UserDomain>  login(HttpServletRequest httpServletRequest){
            System.out.println("sss");
            return new Result<>(new UserDomain(httpServletRequest.getRemoteUser()));
        }

        @GetMapping("/checkTicket")
        public void index(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 >

        <header style="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>
    <script type="text/ecmascript-6">

      export default {

        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"
                    } else if (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--

    来源:blog.csdn.net/weixin_43483911/article/details/117811270

    版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

    往期惊喜:

    何谓架构?

    一个很酷的后台权限管理系统

    一个很酷的博客系统

    一个很酷的快速开发代码生成器系统

    看看人家那 IM 即时通讯系统,那叫一个优雅(附源码)

    支付宝架构师眼中的高并发架构

    负载均衡 LVS vs Nginx 对比!还傻傻分不清?

    看看人家那企业级脚手架系统,那叫一个优雅

    牛逼!这个前后端分离的后台系统开源了!

    这份日志格式规范,拿走不谢(Java版)

    牛逼,团队工作的神器开源了!

    Spring Boot 实现万能文件在线预览

    从 7 分钟到 10 秒,Mybatis 批处理真的很强!

    这是我见过最好的支付系统!


    扫码关注我们的Java架构师技术

    带你全面深入Java

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存