• 前后端分离模式下,SpringBoot + CAS 单点登录实现方案


    1.CAS服务端构建

    1.1.war包部署

    cas5.3版本

    https://github.com/apereo/cas-overlay-template

    构建完成后将war包部署到tomcat即可

    1.2.配置文件修改

    支持http协议

    修改apache-tomcat-8.5.53\webapps\cas\WEB-INF\classes\services目录下的HTTPSandIMAPS-10000001.json,在serviceId中添加http即可

    1. {
    2.   "@class" : "org.apereo.cas.services.RegexRegisteredService",
    3.   "serviceId" : "^(https|http|imaps)://.*",
    4.   "name" : "HTTPS and IMAPS",
    5.   "id" : 10000001,
    6.   "description" : "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
    7.   "evaluationOrder" : 10000
    8. }

    apache-tomcat-8.5.53\webapps\cas\WEB-INF\classes下application.properties添加配置

    1. cas.tgc.secure=false
    2. cas.serviceRegistry.initFromJson=true

    配置默认登录用户名密码及登出重定向

    修改apache-tomcat-8.5.53\webapps\cas\WEB-INF\classes下application.properties配置

    1. cas.authn.accept.users=admin::admin
    2. #配置允许登出后跳转到指定页面
    3. cas.logout.followServiceRedirects=true

    1.3.启动

    2.客户端构建

    2.1.pom依赖

    1. <dependency>
    2.     <groupId>net.unicon.cas</groupId>
    3.     <artifactId>cas-client-autoconfig-support</artifactId>
    4.     <version>2.3.0-GA</version>
    5. </dependency>

    2.2.yml配置

    client-host-url配置的地址和前端ajax调用的地址必须一致,统一使用ip:porthostname:port;如果本地后端配置localhost,前端使用ip,会造成Ticket验证失败

    1. cas:
    2.   server-url-prefix: http://172.19.25.113:8080/cas
    3.   server-login-url: http://172.19.25.113:8080/cas/login
    4.   client-host-url: http://172.19.25.113:1010
    5.   validation-type: cas
    6.   use-session: true
    7.   authentication-url-patterns:
    8.     /auth

    2.3.后端代码

    启动类添加@EnableCasClient注解

    1. @EnableCasClient
    2. @SpringBootApplication
    3. public class SpringbootCasDemoApplication {
    4.     public static void main(String[] args) {
    5.         SpringApplication.run(SpringbootCasDemoApplication.class, args);
    6.     }
    7. }

    自定义AuthenticationFilter重定向策略

    1. public class CustomAuthRedirectStrategy implements AuthenticationRedirectStrategy {
    2.     @Override
    3.     public void redirect(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s) throws IOException {
    4.         httpServletResponse.setCharacterEncoding("utf-8");
    5.         httpServletResponse.setContentType("application/json; charset=utf-8");
    6.         PrintWriter out = httpServletResponse.getWriter();
    7.         out.write("401");
    8.     }
    9. }

    Cors及CasClient相关filter初始化参数配置

    1. @Configuration
    2. public class CasAuthConfig extends CasClientConfigurerAdapter {
    3.     @Override
    4.     public void configureAuthenticationFilter(FilterRegistrationBean authenticationFilter) {
    5.         Map<StringString> initParameters = authenticationFilter.getInitParameters();
    6.         initParameters.put("authenticationRedirectStrategyClass""cc.jasonwang.springbootcasdemo.config.CustomAuthRedirectStrategy");
    7.     }
    8.     @Override
    9.     public void configureValidationFilter(FilterRegistrationBean validationFilter) {
    10.         Map<StringString> initParameters = validationFilter.getInitParameters();
    11.         initParameters.put("encodeServiceUrl""false");
    12.     }
    13.     @Bean
    14.     public FilterRegistrationBean corsFilter() {
    15.         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    16.         CorsConfiguration config = new CorsConfiguration();
    17.         config.setAllowCredentials(true);
    18.         config.addAllowedOrigin("*");
    19.         config.addAllowedHeader("*");
    20.         config.addAllowedMethod("*");
    21.         source.registerCorsConfiguration("/**", config);
    22.         FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
    23.         registrationBean.setFilter(new CorsFilter(source));
    24.         registrationBean.setOrder(-2147483648);
    25.         return registrationBean;
    26.     }
    27. }

    Controller

    1. @RestController
    2. public class HelloController {
    3.     @Value("${cas.server-url-prefix}")
    4.     private String casServerUrlPrefix;
    5.     @GetMapping("/auth")
    6.     public void auth(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
    7.         Assertion assertion = (Assertion) session.getAttribute("_const_cas_assertion_");
    8.         response.setHeader("Content-type""application/json;charset=UTF-8");
    9.         response.setCharacterEncoding("utf-8");
    10.         response.setStatus(200);
    11.         if (assertion != null) {
    12.             String redirectUrl= request.getParameter("redirectUrl");
    13.             try {
    14.                 response.setHeader("Content-type""text/html;charset=UTF-8");
    15.                 response.sendRedirect(redirectUrl);
    16.             } catch (IOException e) {
    17.                 e.printStackTrace();
    18.             }
    19.         } else {
    20.             try {
    21.                 response.getWriter().print("401");
    22.             } catch (IOException e) {
    23.                 e.printStackTrace();
    24.             }
    25.         }
    26.     }
    27.     @GetMapping("/logout")
    28.     public RedirectView logout(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
    29.         session.invalidate();
    30.         String indexPageUrl = "http://127.0.0.1";
    31.         return new RedirectView( casServerUrlPrefix + "/logout?service=" + indexPageUrl);
    32.     }
    33. }

    2.4.页面

    1. <!DOCTYPE html>
    2. <html lang="en" dir="ltr">
    3.   <head>
    4.     <meta charset="utf-8">
    5.     <title></title>
    6.   </head>
    7.   <body>
    8.       <span>单点地址:</span><input class="url" type="text"/><br>
    9.       <button type="button" class="button">登录</button><br>
    10.       <div class="response" style="width: 200px;height:200px;border: 1px solid #3333;"></div>
    11.      <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    12.       <script type="text/javascript">
    13.         $(".button").click(function(){
    14.           $.get("http://172.19.25.113:1010/auth"function(data){
    15.             $(".response").text(data)
    16.             if(data == 401){
    17.               window.location.href = "http://localhost:8080/cas/login?service=http://172.19.25.113:1010/auth?redirectUrl=http://127.0.0.1"
    18.             }
    19.           })
    20.         })
    21.       </script>
    22.   </body>
    23. </html>

    这里只是验证前后端分离下页面url跳转问题,页面没有放在nginx服务上

    3.问题记录

    3.1在前后端分离情况下,AuthenticationFilter重定向问题,导致前端发生跨域

    • https://www.jianshu.com/p/7b51d04f3327

    (1)描述

    cas前后端不分离的情况下是能够直接跳转的,然而前后端分离后,前端ajax访问后端在经过AuthenticationFilter时,验证未登录会重定向到CAS登录,导致前端发生跨域问题

    (2)解决思路

    AuthenticationFilter中不进行重定向,验证未登录就直接返回一个错误状态码;由前端获取到状态码后进行判断,再跳转到CAS登录地址

    AuthenticationFilter

    1. public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    2.     HttpServletRequest request = (HttpServletRequest)servletRequest;
    3.     HttpServletResponse response = (HttpServletResponse)servletResponse;
    4.     if (this.isRequestUrlExcluded(request)) {
    5.         this.logger.debug("Request is ignored.");
    6.         filterChain.doFilter(request, response);
    7.     } else {
    8.      // 获取Assertion 验证是否登录
    9.         HttpSession session = request.getSession(false);
    10.         Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
    11.         if (assertion != null) {
    12.             filterChain.doFilter(request, response);
    13.         } else {
    14.             String serviceUrl = this.constructServiceUrl(request, response);
    15.             String ticket = this.retrieveTicketFromRequest(request);
    16.             boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
    17.             if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
    18.                 this.logger.debug("no ticket and no assertion found");
    19.                 String modifiedServiceUrl;
    20.                 if (this.gateway) {
    21.                     this.logger.debug("setting gateway attribute in session");
    22.                     modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
    23.                 } else {
    24.                     modifiedServiceUrl = serviceUrl;
    25.                 }
    26.                 this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
    27.                 String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
    28.                 this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
    29.                 // 通过这个方法进行重定向
    30.                 this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
    31.             } else {
    32.                 filterChain.doFilter(request, response);
    33.             }
    34.         }
    35.     }
    36. }

    DefaultAuthenticationRedirectStrategy

    1. public final class DefaultAuthenticationRedirectStrategy implements AuthenticationRedirectStrategy {
    2.     public DefaultAuthenticationRedirectStrategy() {
    3.     }
    4.     public void redirect(HttpServletRequest request, HttpServletResponse response, String potentialRedirectUrl) throws IOException {
    5.      //response重定向
    6.         response.sendRedirect(potentialRedirectUrl);
    7.     }
    8. }

    (3)实现

    自定义重定向策略,将DefaultAuthenticationRedirectStrategy替换掉

    CustomAuthRedirectStrategy

    1. public class CustomAuthRedirectStrategy implements AuthenticationRedirectStrategy {
    2.     @Override
    3.     public void redirect(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s) throws IOException {
    4.         httpServletResponse.setCharacterEncoding("utf-8");
    5.         httpServletResponse.setContentType("application/json; charset=utf-8");
    6.         PrintWriter out = httpServletResponse.getWriter();
    7.         out.write("401");
    8.     }
    9. }
    10. @Configuration
    11. public class CasAuthConfig extends CasClientConfigurerAdapter {
    12.     @Override
    13.     public void configureAuthenticationFilter(FilterRegistrationBean authenticationFilter) {
    14.         Map<StringString> initParameters = authenticationFilter.getInitParameters();
    15.         initParameters.put("authenticationRedirectStrategyClass""cc.jasonwang.springbootcasdemo.config.CustomAuthRedirectStrategy");
    16.  }
    17. }

    3.2AuthenticationFilter自定义重定向策略实现后,前端仍然发生跨域问题

    Spring 里那么多种 CORS 的配置方式,到底有什么区别

    (1)描述

    原使用WebMvcConfigurationSupport实现CORS,AuthenticationFilter输出状态码后,前端仍然发生跨域问题

    1. @Configuration
    2. public class CorsConfig extends WebMvcConfigurationSupport {
    3.     @Override
    4.     public void addCorsMappings(CorsRegistry registry) {
    5.         registry.addMapping("/**")
    6.                 .allowedOrigins("*")
    7.                 .allowedHeaders("*")
    8.                 .allowedMethods("*")
    9.                 .maxAge(3600)
    10.                 .allowCredentials(true);
    11.     }
    12. }

    (2)解决思路

    通过查找资料发现:

    实现 WebMvcConfigurationSupport.addCorsMappings 方法来进行的 CORS 配置,最后会在 Spring 的 Interceptor 或 Handler 中生效

    注入 CorsFilter 的方式会让 CORS 验证在 Filter 中生效

    (3)实现

    修改CORS实现方式

    1. @Bean
    2. public FilterRegistrationBean corsFilter() {
    3.     UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    4.     CorsConfiguration config = new CorsConfiguration();
    5.     config.setAllowCredentials(true);
    6.     config.addAllowedOrigin("*");
    7.     config.addAllowedHeader("*");
    8.     config.addAllowedMethod("*");
    9.     source.registerCorsConfiguration("/**", config);
    10.     FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
    11.     registrationBean.setFilter(new CorsFilter(source));
    12.     registrationBean.setOrder(-2147483648);
    13.     return registrationBean;
    14. }

    3.3前端跳转CAS登录并传递redirectUrl参数,Ticket票据验证问题

    (1)原因

    Cas20ProxyReceivingTicketValidationFilter在进行Ticket验证时,CAS重定向的service地址进行了URLEncoder编码,而CAS使用Ticket获取到存储的service地址未进行编码,导致两个service不一致,造成Ticket票据验证失败

    (2)debug定位问题

    AbstractTicketValidationFilter

    AbstractUrlBasedTicketValidator

    找到CAS服务器接口地址后,便想到在CAS服务器端看下接口是怎么实现的,下面就是在CAS服务器debug后的结果

    CAS Server

    在web.xml中找到了servlet映射

    定位到SafeDispatcherServlet,根据目录结构和类文件名称找到了ServiceValidateController

    ServiceValidateController

    (3)实现

    Cas20ProxyReceivingTicketValidationFilter添加encodeServiceUrl=false初始化参数

    1. @Configuration
    2. public class CasAuthConfig extends CasClientConfigurerAdapter {
    3.     @Override
    4.     public void configureAuthenticationFilter(FilterRegistrationBean authenticationFilter) {
    5.         Map<StringString> initParameters = authenticationFilter.getInitParameters();
    6.         initParameters.put("authenticationRedirectStrategyClass""cc.jasonwang.springbootcasdemo.config.CustomAuthRedirectStrategy");
    7.     }
    8.     @Override
    9.     public void configureValidationFilter(FilterRegistrationBean validationFilter) {
    10.         Map<StringString> initParameters = validationFilter.getInitParameters();
    11.         initParameters.put("encodeServiceUrl""false");
    12.     }
    13. }

    最后说一句(别白嫖,求关注)

  • 相关阅读:
    如何使用C/C++刷新在终端上已经打印的内容
    m基于中继协助的认知无线电频谱切换机制的matlab仿真分析
    HTML表格合并行和列
    5G时代带动陶瓷PCB成长——GPS陶瓷天线调试方法 (一)
    技巧 | Python绘制2022年卡塔尔世界杯决赛圈预测图
    【趣味实践】自动补帧算法——RIFE的使用
    微机原理3
    IDEA 常用技巧
    Ajax--Ajax加强--XMLHttpRequest的基本使用
    CRD2 值得一读的知识蒸馏与对比学习结合的paper 小陈读paper
  • 原文地址:https://blog.csdn.net/BASK2312/article/details/128198612