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