springboot+vue+cas实现单点登录、退出

1.添加依赖包

<!-- 单点登录 20230426-->
<dependency>
    <groupId>org.jasig.cas.client</groupId>
    <artifactId>cas-client-core</artifactId>
    <version>3.4.1</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.10</version>
</dependency>

2.添加cas过滤器

/**
 * @author huangquanguang
 * @date 2023/4/25 13:57
 * @description cas过滤器配置
 */
@Configuration
@ConditionalOnProperty(prefix = "cas",name = "is-open",havingValue = "true")
public class CasFilterConfig implements Serializable, InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(CasFilterConfig.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";

    @Value("${cas.server-url-prefix:http://127.0.0.1:8443/cas}")
    public String casServerUrlPrefix;
    @Value("${cas.client-host-url:http://127.0.0.1}")
    public String casClientHostUrl;
    @Value("${cas.client-host-api-url:http://127.0.0.1/api/api-demo/cas/ssoLogin}")
    public String casClientHostApiUrl;

    public CasFilterConfig() {

    }

    /**
     * 单点登出功能,放在其他filter之前
     * casSigntouServerUrlPrefix为登出前缀:https://127.0.0.1/cas/logout
     *
     * @return
     */
    @Bean
    @Order(0)
    public FilterRegistrationBean getCasSignoutFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(getCasSignoutFilter());
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("casServerLoginUrl", casServerUrlPrefix + "/login");
        initParameters.put("serverName", casClientHostUrl);
        //忽略的url,"|"分隔多个url
        initParameters.put("ignorePattern", "/logout/success|/index|/test|/login");
        registration.setInitParameters(initParameters);
        registration.addUrlPatterns("/*");
        registration.addInitParameter("casServerUrlPrefix", casServerUrlPrefix + "/logout");
        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("/*");
        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://127.0.0.1/api/api-demo/cas/login
     * casServerName:https://127.0.0.1/
     *
     * @return
     */
    @Bean
    @Order(2)
    public FilterRegistrationBean getCasAuthFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        final Filter casAuthFilter = getCasAuthFilter();
        registration.setFilter(casAuthFilter);
        registration.addUrlPatterns("/*");
        registration.addInitParameter("casServerLoginUrl", casServerUrlPrefix + "/login");
        registration.addInitParameter("serverName", casClientHostUrl);
        registration.setName(CAS_AUTH_FILTER_NAME);
        registration.setEnabled(true);
        return registration;
    }

    @Bean(name = CAS_AUTH_FILTER_NAME)
    public Filter getCasAuthFilter() {
        return new MyAuthenticationFilter();
    }

    /**
     * 对Ticket进行校验
     * casServerUrlPrefix
     *
     * @return
     */
    @Bean
    @Order(3)
    public FilterRegistrationBean getCasValidationFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        final Filter casValidationFilter = getCasValidationFilter();
        registration.setFilter(casValidationFilter);
        registration.addUrlPatterns("/*");
        registration.addInitParameter("casServerUrlPrefix", casServerUrlPrefix);
        registration.addInitParameter("serverName", casClientHostUrl);
        registration.addInitParameter("encoding", "UTF-8");
        registration.setName(CAS_FILTER_NAME);
        registration.setEnabled(true);
        return registration;
    }

    @Bean(name = CAS_FILTER_NAME)
    public Filter getCasValidationFilter() {
        //按照对方提供的文档使用Cas10TicketValidationFilter
//        return new Cas20ProxyReceivingTicketValidationFilter();
        return new Cas10TicketValidationFilter();
    }

    /**
     * 设置response的默认编码方式:UTF-8。
     *
     * @return
     */
    @Bean
    @Order(4)
    public FilterRegistrationBean getCharacterEncodingFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(getCharacterEncodingFilter());
        registration.addUrlPatterns("/*");
        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 {

    }
}

3.重写拦截器自定义状态码、在前端进行重定向

/**
 * @author huangquanguang
 * @date 2023/4/25 13:57
 * @description 重写拦截器自定义状态码,实现前后端分离的单点登录
 */
public class MyAuthenticationFilter extends AbstractCasFilter {

    @Value("${cas.server-url-prefix:http://127.0.0.1:8443/cas}")
    public String casServerUrlPrefix;
    @Value("${cas.client-host-url:http://127.0.0.1}")
    public String casClientHostUrl;
    @Value("${cas.client-host-ticket-url:http://127.0.0.1/api/api-demo/cas/checkTicket}")
    public String casClientHostTicketUrl;
    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() {
        super(Protocol.CAS1);
    }

    //升级cas-client版本后
//    @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.casServerUrlPrefix + "/login", "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");
                    String casHeader = request.getHeader("casHeader");
                    //这里是重点、前端根据状态码判断是否跳转
                    if ("true".equals(casHeader)) {
                        response.getWriter().write("{\"code\":202, \"msg\":\"no ticket and no assertion found\", \"url\":\""+ casClientHostTicketUrl +"\"}");
                    } else {
                        String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerUrlPrefix + "/login", "service", 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 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);
    }
}

4.忽略ssl认证

public class IgnoreSSLValidateFilter implements Filter {
    static {
        //执行设置,禁用ssl认证
        try {
            TrustManager[] trustAllCerts = {new X509TrustManager() {
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] arg0, String arg1)
                        throws CertificateException {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] arg0, String arg1)
                        throws CertificateException {
                }
            }};
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

            HostnameVerifier allHostsValid = new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }

}

5.接口编写

/**
 * @author huangquanguang
 * @date 2023/4/25 13:57
 * @description cas单点登录集成
 */
@Controller
@RequestMapping("/cas")
public class CasLoginController {

    @Autowired
    private AuthClient authClient;
    @Value("${cas.server-url-prefix:http://127.0.0.1:8443/cas}")
    public String casServerUrlPrefix;
    @Value("${cas.client-host-url:http://127.0.0.1}")
    public String casClientHostUrl;
    @Value("${cas.client-host-api-url:http://127.0.0.1/api/api-demo/cas/ssoLogin}")
    public String casClientHostApiUrl;
    @Value("${cas.client-host-ticket-url:http://127.0.0.1/api/api-demo/cas/checkTicket}")
    public String casClientHostTicketUrl;

    /**
     * 登录
     * @return
     */
    @GetMapping("/ssoLogin")
    @ResponseBody
    public JsonResult login(HttpServletRequest httpServletRequest){
        HttpSession session = httpServletRequest.getSession(false);
        if (session != null) {
            org.jasig.cas.client.validation.Assertion assertion = (Assertion) session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
            String username = assertion.getPrincipal().getName();
            //调用auth接口获取jxbp的accessToken
            JsonResult tysfrz = authClient.tysfrz(username);
            return JsonResult.Success().setData(tysfrz.getData());
        }
        //处理登录的逻辑
        return JsonResult.Success().setData(httpServletRequest.getRemoteUser());
    }

    @GetMapping("/checkTicket")
    public void index(HttpServletResponse response,HttpServletRequest httpServletRequest) throws IOException {
        // 前端页面地址
        response.sendRedirect(casClientHostUrl+"/appPortal/casLogin");
    }

    /**
     * 注销
     * @return
     */
    @RequestMapping("/logout")
    public String logout(){
        return "redirect:"+ casServerUrlPrefix +"/logout?service="+ casClientHostTicketUrl;
    }

    @RequestMapping("/checkStatus")
    @ResponseBody
    public JsonResult checkStatus(HttpServletRequest request, HttpServletResponse response) {
        String user = request.getRemoteUser();
        if (StringUtils.isNotBlank(user)) {
            return JsonResult.Success();
        } else {
            return JsonResult.Fail("登录已过期");
        }
    }
}

6.前端首页处理

<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">
	import LoginApi from '@/api/login'
	import Vue from "vue";
	import {ACCESS_TOKEN, CODE, PATH} from '@/store/mutation-types'
	import {timeFix} from '@/utils/util'
	import {mapState} from 'vuex';

	export default {

		data() {

			return {
				name:'ss'
			}

		},
		computed:{
			...mapState({
				//用户对象
				user: state => state.appSetting.user,
				//是否根租户
				isRootTenant: state => state.appSetting.user.tenantId==ROOT_TENANT,
				//当前租户ID
				tenantId:state => state.appSetting.user.tenantId,
				//是否管理员
				isAdmin:state => state.appSetting.user.admin,
				//是否根租户管理员
				isRootAdmin:state => state.appSetting.user.admin && state.appSetting.user.tenantId==ROOT_TENANT
			})
		},
		mounted(){
			let header = {headers: {'casHeader': true}};//一定要添加这个请求头
			let url = "http://127.0.0.1/api/api-demo/cas/ssoLogin";
			LoginApi.casLogin(url,header).then(res => {
				// 单点登录用户未登录,打开认证中心登录地址并将前端地址作为参数传回,以便登录成功跳转到前端页面
				if (res.code === 202) {
					//console.log(response);
					window.location.href = res.url
				} else if (res.code === 200) {
					var token = res.data.access_token;
					//设置登录token。
					Vue.ls.set(ACCESS_TOKEN, token, 12 * 60 * 60 * 1000);
					this.name = this.user.fullName
					// this.handRedirect(token);
				}
				console.log(res);
			})
		},
		created() {
			// this.times = setInterval(() => {
			// 	this.checkStatus();
			// }, 1000 * 60);
		},
		methods: {
			handRedirect(token) {
				this.$notification.success({
					message: '欢迎',
					description: `${timeFix()},欢迎回来`,
					duration: 1,
					onClose: function () {
						location.href = PATH + '/' + CODE + '/home/index';
					}
				})
			},
			logout() {
				//清除系统登录信息session
				Vue.ls.remove(ACCESS_TOKEN);
				window.location.href = "http://127.0.0.1/api/api-demo/cas/logout"
			},
			checkStatus(){
				LoginApi.checkStatus("http://127.0.0.1/api/api-demo/cas/checkStatus").then(res => {
					if(res.code!=200){
						Vue.ls.remove(ACCESS_TOKEN);
						window.location.href = "http://127.0.0.1/api/api-demo/cas/logout"
					}
				})
			}
		}
	}
</script>

7.cas相关地址配置

##cas单点登录配置
#是否开启cas单点登录
checkTicketcas.is-open=true
#单点登录前缀
cas.server-url-prefix=http://127.0.0.1:8443/cas
#自定义项目后台地址
cas.client-host-url=http://127.0.0.1
#自定义项目后台单点登录接口
cas.client-host-api-url=http://127.0.0.1/api/api-demo/cas/ssoLogin
#检查票据接口地址
cas.client-host-ticket-url=http://127.0.0.1/api/api-demo/cas/

8.nginx配置

必须把前端访问地址和后端接口放在相同的域名下

#应用开发 前端
location /appPortal {
    proxy_set_header Host       $host;
    proxy_pass http://127.0.0.1:8083;
}
#gateway网关 后端
location /api/ {
    proxy_set_header Host       $host;
    #proxy_pass http://192.168.4.178:9900/;
    proxy_pass http://127.0.0.1:9900/;
}

9.测试效果

首次访问时,跳转统一身份登录页

http://127.0.0.1:8443/cas/login?service=http%3A%2F%2F127.0.0.1%3A7206%2Fcas%2FcheckTicket

登录成功后,跳转回来首页

http://127.0.0.1/appPortal/casLogin

在这里插入图片描述
点击安全退出后、再回到原来的登录页

http://127.0.0.1:8443/cas/login?service=http%3A%2F%2F127.0.0.1%3A7206%2Fcas%2FcheckTicket
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值