素材巴巴 > 程序开发 >

shiro cas 前后端分离 单点登录 解决方案

程序开发 2023-09-10 13:14:05

shiro+cas 前后端分离 单点登录 解决方案

  • cas服务
  • cas客户端
  • 单点登录实现
  • 写在前面

    在设计开发自己的博客系统时,选择cas+shiro做用户认证和鉴权的框架,实现单点登录,方便接入其他系统。
    曾经在徐州客户现场被cas折磨好久,一直想把cas啃下来。原谅我是个小菜鸟
    搭建这个框架爬了很多坑,也参考了很多大佬的博客,自己慢慢试,跟踪源码,慢慢思考,也有一点收获。
    目前项目还没有完成,可能还有很多坑,有很多地方处理的不是很好,现在只是流程通了,先记录下来,省着时间长忘了思路。也请各位大佬帮忙之处不对的地方

    主要的重点

    源码

    cas服务器
    https://github.com/jsong93/cas-server
    客户端
    https://github.com/jsong93/shiro-client

    cas服务

    cas服务端搭建
    cas md5+盐加密

    cas客户端

    spring boot cas+shiro环境搭建

    单点登录实现

    shiroconfig配置

    package com.jsong.wiki.backend.config;import com.jsong.wiki.backend.bean.ShiroUrl;
     import com.jsong.wiki.backend.filter.MyAuthenticationFilter;
     import com.jsong.wiki.backend.filter.MyFormAuthenticationFilter;
     import com.jsong.wiki.backend.filter.MyUserFilter;
     import com.jsong.wiki.backend.shiro.ShiroCasRealm;
     import org.apache.shiro.cache.ehcache.EhCacheManager;
     import org.apache.shiro.cas.CasFilter;
     import org.apache.shiro.cas.CasSubjectFactory;
     import org.apache.shiro.mgt.SecurityManager;
     import org.apache.shiro.spring.LifecycleBeanPostProcessor;
     import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
     import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
     import org.apache.shiro.web.filter.authc.LogoutFilter;
     import org.apache.shiro.web.filter.authc.UserFilter;
     import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
     import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
     import org.springframework.beans.factory.annotation.Qualifier;
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;
     import java.util.HashMap;
     import java.util.Map;
     /*** shiro配置** @Author: Jsong* @Date: 2020/2/7 21:58* @Description:*/
     @Configuration
     public class ShiroConfig {// shiroFilter@Beanpublic ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,@Qualifier("casFilter") MyAuthenticationFilter casFilter,
     //                                              @Qualifier("logoutFilter") LogoutFilter logoutFilter,@Qualifier("userFilter") MyUserFilter userFilter,MyFormAuthenticationFilter myFormAuthenticationFilter,ShiroUrl shiroUrl) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);
     //        设置登录URL,当用户未认证,但访问了需要认证后才能访问的页面,就会自动重定向到登录URLshiroFilterFactoryBean.setLoginUrl(shiroUrl.getLoginUrl());shiroFilterFactoryBean.setSuccessUrl(shiroUrl.getSuccessUrl());
     //        设置没有权限的URL,当用户认证后,访问一个页面却没有权限时,就会自动重定向到没有权限的URL,若用户未认证访问一个需要权限的URL时,会跳转到登录URLshiroFilterFactoryBean.setUnauthorizedUrl(shiroUrl.getUnauthorizedUrl());Map filters = new HashMap<>();//        org.apache.shiro.web.filter.mgt.DefaultFilter 包含所有的过滤器filters.put("casFilter", casFilter);filters.put("userFilter", userFilter);filters.put("authc", myFormAuthenticationFilter);
     //        filters.put("logoutFilter", logoutFilter);
     //        将Filter添加到Shiro过滤器链中,用于对资源设置权限shiroFilterFactoryBean.setFilters(filters);Map filterChainDefinitionMap = new HashMap();filterChainDefinitionMap.put(shiroUrl.getCasFilterUrlPattern(), "casFilter");
     //        filterChainDefinitionMap.put(shiroUrl.getCasFilterUrlPattern(), "userFilter");
     //        filterChainDefinitionMap.put(shiroUrl.getLogoutUrlPattern(), "logoutFilter");filterChainDefinitionMap.putAll(shiroUrl.getAuthUrlPatternMap());// 配置哪些请求需要受保护,以及访问这些页面需要的权限shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}// 认证filter@Beanpublic MyAuthenticationFilter casFilter(ShiroUrl shiroUrl) {MyAuthenticationFilter casFilter = new MyAuthenticationFilter();// 登录成功urlcasFilter.setSuccessUrl(shiroUrl.getSuccessUrl());// 登录失败urlcasFilter.setFailureUrl(shiroUrl.getFailureUrl());return casFilter;}/**** 配置登录后重定向filter* @date 2020/2/29 20:41* @author Jsong* @param* @return com.jsong.wiki.backend.filter.MyUserFilter*/@Beanpublic MyUserFilter userFilter(){return new MyUserFilter();}@Beanpublic MyFormAuthenticationFilter myFormAuthenticationFilter(){return new MyFormAuthenticationFilter();}// 自定义 casRealm@Beanpublic ShiroCasRealm casRealm(ShiroUrl shiroUrl, EhCacheManager ehCacheManager) {ShiroCasRealm casRealm = new ShiroCasRealm();// cas服务器casRealm.setCasServerUrlPrefix(shiroUrl.getCas().getServerUrlPrefix());// 客户端地址,用于接收tiketcasRealm.setCasService(shiroUrl.getCas().getService());casRealm.setCacheManager(ehCacheManager);return casRealm;}/*** 问题?????* 把这个bean注入后,登录后就会重定向到登出,为什么呢?????** @param shiroUrl* @return*/
     //    @Beanpublic LogoutFilter logoutFilter(ShiroUrl shiroUrl) {LogoutFilter logoutFilter = new LogoutFilter();// 登出后重定向地址logoutFilter.setRedirectUrl(shiroUrl.getLogoutUrl());return logoutFilter;}@Beanpublic EhCacheManager ehCacheManager() {EhCacheManager ehCacheManager = new EhCacheManager();ehCacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");return ehCacheManager;}//    配置securityManager SecurityManager,Shiro的安全管理,主要是身份认证的管理,缓存管理,cookie管理@Beanpublic SecurityManager securityManager(ShiroCasRealm casRealm, EhCacheManager ehCacheManager) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setSubjectFactory(new CasSubjectFactory());securityManager.setCacheManager(ehCacheManager);securityManager.setRealm(casRealm);return securityManager;}//    配置lifecycleBeanPostProcessor,shiro bean的生命周期管理器,可以自动调用Spring IOC容器中shiro bean的生命周期方法(初始化/销毁)
     //    @Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}/*    为了支持Shiro的注解需要定义DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor两个bean配置DefaultAdvisorAutoProxyCreator,必须配置了lifecycleBeanPostProcessor才能使用*/
     //    @DependsOn("lifecycleBeanPostProcessor")
     //    @Beanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();return defaultAdvisorAutoProxyCreator;}//    配置AuthorizationAttributeSourceAdvisor@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}
     }

    非前后端分离的项目,当请求一个需要权限的方法时,需要servlet可以直接重定向到cas登录页面


    重定向问题

    前后端分离的项目,当使用ajax请求一个需要权限的方法时,也会重定向,但会直接将重定向的cas登录页面以字符串的方式返回

    解决办法

    重写FormAuthenticationFilter的onAccessDenied方法,通过subject.isAuthenticated()判断是否登录,如果未登录返回给前端401,前端在重定向到cas登录页面

    package com.jsong.wiki.backend.filter;import com.jsong.wiki.backend.bean.CookieProperties;
     import lombok.extern.log4j.Log4j2;
     import org.apache.shiro.SecurityUtils;
     import org.apache.shiro.subject.Subject;
     import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
     import org.apache.shiro.web.util.WebUtils;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.beans.factory.annotation.Value;
     import org.springframework.stereotype.Component;import javax.servlet.ServletRequest;
     import javax.servlet.ServletResponse;
     import javax.servlet.http.Cookie;
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
     import java.io.IOException;/*** authc 的过滤器** @Author: Jsong* @Date: 2020/3/2 22:24* @Description:*///@Component
     @Log4j2
     public class MyFormAuthenticationFilter extends FormAuthenticationFilter {@Value("${shiro.loginUrl}")private String loginUrl;@Value("${front.url}")private String frontUrl;//    protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
     //        log.info("-----------------redirect login --------------------");
     //        Cookie cookie = new Cookie("front-url", ((HttpServletRequest) request).getHeader("front-url"));
     //        ((HttpServletResponse) response).addCookie(cookie);
     //        saveRequest(request);
     //        redirectToLogin(request, response);
     //    }@Autowiredprivate CookieProperties cookieProperties;/****重写登录重定向* 把请求内容存入cookie,重定向的时候带着cookie保存状态* @date 2020/3/3 14:46* @author Jsong* @param request* @param response* @return boolean*/@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {if (isLoginRequest(request, response)) {if (isLoginSubmission(request, response)) {if (log.isTraceEnabled()) {log.trace("Login submission detected.  Attempting to execute login.");}return executeLogin(request, response);} else {if (log.isTraceEnabled()) {log.trace("Login page view.");}//allow them to see the login page ;)return true;}} else {if (log.isTraceEnabled()) {log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +"Authentication url [" + getLoginUrl() + "]");}log.info("-----------------redirect login --------------------");//            Cookie[] cookies = ((HttpServletRequest) request).getCookies();
     //            for (Cookie cookie : cookies) {
     //                cookie.setDomain(cookieProperties.getDomain());
     //                cookie.setPath(cookieProperties.getPath());
     //                cookie.setHttpOnly(cookieProperties.getHttpOnly());
     //            }//            Cookie cookie = new Cookie(frontUrl, ((HttpServletRequest) request).getHeader(frontUrl));
     //            cookie.setDomain(cookieProperties.getDomain());
     //            cookie.setPath(cookieProperties.getPath());
     //            cookie.setHttpOnly(cookieProperties.getHttpOnly());
     //            ((HttpServletResponse) response).addCookie(cookie);//            saveRequestAndRedirectToLogin(request, response);saveRequest(request);
     //            redirectToLogin(request, response);// 读取到的 /index.jsp 不知道为啥
     //            String loginUrl = getLoginUrl();//            shiro 会把请求内容保存起来,未登录为空
     //            if(WebUtils.getSavedRequest(request)==null){
     //                // 未授权,返回前端状态401 进行重定向
     //            request里获取不到用户信息,不能通过request判断是否登录,不能用这个方法了
     //            ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     //            return false;
     //            }Subject subject = SecurityUtils.getSubject();if (!subject.isAuthenticated()) {// 未授权,返回前端状态401 进行重定向((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}// ajax 重定向会返回html页面字符串WebUtils.issueRedirect(request, response, loginUrl);return false;}}
     }
    /** @Descripttion:* @version:* @Author: jsong* @Date: 2020-02-25 22:30:54* @LastEditors: jsong* @LastEditTime: 2020-03-07 12:04:18*/import axios from "axios";
     import Cookies from "js-cookie";axios.defaults.withCredentials = true;
     const baseUrl = "/blog-backend";
     export const api = axios.create({// headers: { "Access-Control-Allow-Origin": "http://127.0.0.1:28080" },// baseUrl
     });
     api.interceptors.request.use(req => {req.withCredentials = true;console.log("req", req);// Cookies.set(//   "api-uri",//   req.url.indexOf(baseUrl) > -1 ? req.url.substr(13) : null// );Cookies.set("front-url", window.location.href);// req.headers["api-uri"] =//   req.url.indexOf(baseUrl) > -1 ? req.url.substr(13) : null;// req.headers["front-url"] = window.location.href;return req;},err => {console.log("e", err);if (401 === err.request.status) {}}
     );api.interceptors.response.use(res => {// if (res && res.request) {//  const responseURL = res.request.responseURL;//  if (//     responseURL ===//     "http://127.0.0.1:18080/cas/login?service=http://127.0.0.1:18081/blog-backend/shiro-cas"//   ) {// const jsessionid = Cookies.get("JSESSIONID");// console.log("jssionid", jsessionid);// localStorage.setItem("jsessionid", jsessionid);// window.location.href = `${responseURL}&test=1`;//    window.location.href = `${responseURL}`;//   }// console.log("cookie", document.cookie);//  return res;// }return res;},err => {console.log("e", err);if (401 === err.response.status) {const jsessionid = Cookies.get("JSESSIONID");console.log("jssionid", jsessionid);localStorage.setItem("jsessionid", jsessionid);window.location.href ="http://127.0.0.1:18080/cas/login?service=http://127.0.0.1:18081/blog-backend/shiro-cas";}}
     );
    
     ## 时序图
     用户未登录
     ```mermaid
     sequenceDiagram
     页面 ->> shiro客户端: 请求查询接口
     Note right of shiro客户端: 没有认证AuthenticationFilter.isAuthenticated() false
    FormAuthenticationFilter.onAccessDenied() 被拒绝
    保存请求信息AccessControlFilter.saveRequest() 保存请求信息
    AccessControlFilter.redirectToLogin() 重定向到登录页面
    shiro客户端 -->> 页面: 页面 ->> cas服务: 重定向到cas服务 登录 Note right of cas服务: 获取TGC ST
    登录 cas服务 -->> 页面: 页面 ->> shiro客户端: 携带ST Note right of shiro客户端: 根据ST创建token
    AuthenticatingFilter.createToken()
    重定向到前端页面 shiro客户端 -->> 页面:

    用户已登录

    页面 shiro客户端 请求查询接口 AuthenticationFilter.isAuthenticated() true 已认证 访问接口成功 返回数据 页面 shiro客户端

    测试

    在csdn模仿简书 还是简书好看 哈哈哈
    在这里插入图片描述

    遇到一个奇怪的问题

    登录成功后重定向到前端的url是字符串时,下次访问不会再去cas server重新认证
    当url是@Value获取的值时,下次访问会到cas server重新认证
    详细问题


    标签:

    素材巴巴 Copyright © 2013-2021 http://www.sucaibaba.com/. Some Rights Reserved. 备案号:备案中。