若依(RuoYi)微服务是一款基于Spring Cloud Alibaba开发的企业级微服务框架,采用前后端分离方式,使用了常用的微服务组件,如Feign、Nacos、Sentinel、Seata等,提供了丰富的微服务治理功能,如服务注册、发现、路由、负载均衡、熔断降级、限流等。借助若依微服务框架可以让我们快速构建起一个高效、可靠、可扩展的分布式系统,提高了开发效率和系统性能。
借助Spring Cloud Alibaba,若依微服务框架完成了后端的微服务改造,但是前端仍是一个单体服务,随着业务的增长,前端必然变的庞大、臃肿,不可避免的需要对前端进行拆分,然而前端拆分后面临的一个问题是登录信息如何同步?登录信息是以token存储在cookie中的,无法共享。
为了解决前端登录信息同步的问题,这里考虑通过集成CAS的方式,实现统一认证。
研究了若依微服务的认证功能后发现,若依微服务的认证并未使用Spring Security,仅仅使用了Spring Security的加密功能,无法直接套用若依分离版集成CAS的方式。所以通过结合分离版集成思路及CAS官方集成方法完成若依微服务集成CAS。集成方法如下:
1、添加CAS依赖
在auth模块中添加cas依赖:
<!-- Cas Core -->
<dependency><groupId>org.jasig.cas.client</groupId><artifactId>cas-client-core</artifactId><version>3.6.4</version>
</dependency>
2、修改配置文件
在nacos中修改ruoyi-auth-dev.yml(或直接修改auth模块下bootstrap.yml文件),增加cas配置:
cas:enable: trueserver:url:prefix: http://127.0.0.1:8888/caslogin: http://127.0.0.1:8888/cas/loginclient:url: http://127.0.0.1:8080/auth
3、修改Constants.java
修改common-core模块下
com.ruoyi.common.core.constant.Constants.java,增加CAS认证成功标识:
/*** CAS登录成功后的后台标识**/
public static final String CAS_TOKEN = "cas_token";/*** CAS登录成功后的前台Cookie的Key**/
public static final String WEB_TOKEN_KEY = "Cloud-Token";/*** CAS登录成功后的前台Cookie的Expires-In**/
public static final String WEB_TOKEN_EXPIRES = "Cloud-Expires-In";
4、添加CasProperties.java
在auth模块中添加CasProperties.java文件,获取CAS配置信息:
package com.ruoyi.auth.cas.config.properties;import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;/*** @author LuoFei* @className: CasProperty* @projectName RuoYi-Cloud-master* @description: cas配置参数* @date 2022/7/21 10:11**/
@Configuration
@RefreshScope
public class CasProperties {@Value("${cas.enable}")private Boolean enabled;@Value("${cas.server.url.prefix}")private String casServerUrlPrefix;@Value("${cas.server.url.login}")private String casServerLoginUrl;@Value("${cas.client.url}")private String serverName;public Boolean getEnabled() {return enabled;}public void setEnabled(Boolean enabled) {this.enabled = enabled;}public String getCasServerUrlPrefix() {return casServerUrlPrefix;}public void setCasServerUrlPrefix(String casServerUrlPrefix) {this.casServerUrlPrefix = casServerUrlPrefix;}public String getCasServerLoginUrl() {return casServerLoginUrl;}public void setCasServerLoginUrl(String casServerLoginUrl) {this.casServerLoginUrl = casServerLoginUrl;}public String getServerName() {return serverName;}public void setServerName(String serverName) {this.serverName = serverName;}
}
5、添加NoCasFilter.java
在auth模块中添加NoCasFilter.java文件,在未启用CAS时直接放行:
package com.ruoyi.auth.cas.filter;import javax.servlet.*;
import java.io.IOException;/*** @author LuoFei* @className: NoCasFilter* @projectName RuoYi-Cloud-master* @description: 单点登录停用辅助过滤器* @date 2022/7/21 11:19**/
public final class NoCasFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {filterChain.doFilter(request, response);}
}
6、 添加
CustomSessionMappingStorage.java
在auth模块中添加
CustomSessionMappingStorage.java文件,实现单点登出:
package com.ruoyi.auth.cas.storage;import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.security.service.TokenService;
import org.apache.catalina.session.StandardSessionFacade;
import org.jasig.cas.client.session.SessionMappingStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;/*** @author LuoFei* @className: CustomSessionMappingStorage* @projectName RuoYi-Vue-master* @description: 单点登录-前后端分离-单点登出删除token* @date 2022/4/28 12:56**/
@Component
public class CustomSessionMappingStorage implements SessionMappingStorage {private final Map<String, HttpSession> MANAGED_SESSIONS = new HashMap();private final Map<String, String> ID_TO_SESSION_KEY_MAPPING = new HashMap();private final Logger logger = LoggerFactory.getLogger(this.getClass());@Autowiredprivate TokenService tokenService;public CustomSessionMappingStorage() {}@Overridepublic synchronized void addSessionById(String mappingId, HttpSession session) {this.ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId);this.MANAGED_SESSIONS.put(mappingId, session);}@Overridepublic synchronized void removeBySessionById(String sessionId) {this.logger.debug("Attempting to remove Session=[{}]", sessionId);String key = (String)this.ID_TO_SESSION_KEY_MAPPING.get(sessionId);if (this.logger.isDebugEnabled()) {if (key != null) {this.logger.debug("Found mapping for session. Session Removed.");} else {this.logger.debug("No mapping for session found. Ignoring.");}}this.MANAGED_SESSIONS.remove(key);this.ID_TO_SESSION_KEY_MAPPING.remove(sessionId);}@Overridepublic synchronized HttpSession removeSessionByMappingId(String mappingId) {StandardSessionFacade session = (StandardSessionFacade) this.MANAGED_SESSIONS.get(mappingId);if (session != null) {this.removeBySessionById(session.getId());try {String token = (String) session.getAttribute(Constants.CAS_TOKEN);tokenService.delLoginUser(token);} catch (IllegalStateException e) {this.logger.error("已成功登出");}}return session;}
}
7、增加casLogin方法
在auth模块的TokenController.java文件中添加casLogin方法,实现登录功能:
/*** 单点登录成功创建token* @param request* @param response* @throws IOException* @throws ServletException**/
@GetMapping("casLogin")
public void casLogin(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {String username = request.getRemoteUser();HttpSession httpSession = request.getSession();String url = request.getParameter("redirect");LoginUser userInfo = sysLoginService.login(username);Map<String, Object> token = tokenService.createToken(userInfo);Cookie tokenCookie = new Cookie(Constants.WEB_TOKEN_KEY, (String) token.get("access_token"));//必须设置path,否则获取不到cookietokenCookie.setPath("/");response.addCookie(tokenCookie);Cookie expiresCookie = new Cookie(Constants.WEB_TOKEN_EXPIRES, ((Long) token.get("expires_in")).toString());expiresCookie.setPath("/");response.addCookie(expiresCookie);//设置后端认证成功标识httpSession.setAttribute(Constants.CAS_TOKEN, token.get("access_token"));//登录成功后跳转到前端访问页面response.sendRedirect(url);
}
8、添加CasConfig.java文件
在auth模块中添加CasConfig.java文件:
package com.ruoyi.auth.cas.config;import com.ruoyi.auth.cas.config.properties.CasProperties;
import com.ruoyi.auth.cas.filter.NoCasFilter;
import com.ruoyi.auth.cas.storage.CustomSessionMappingStorage;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author LuoFei* @className: CasConfig* @projectName RuoYi-Cloud-master* @description: TODO* @date 2022/7/19 14:17**/
@Configuration
public class CasConfig {@Autowiredprivate CasProperties casProperties;@Autowiredprivate CustomSessionMappingStorage customSessionMappingStorage;/*** 单点登出过滤器* @return**/@Beanpublic FilterRegistrationBean logoutFilter() {FilterRegistrationBean authenticationFilter = new FilterRegistrationBean<>();SingleSignOutFilter signOutFilter = new SingleSignOutFilter();signOutFilter.setSessionMappingStorage(customSessionMappingStorage);signOutFilter.setIgnoreInitConfiguration(true);authenticationFilter.setFilter(signOutFilter);authenticationFilter.addUrlPatterns("/*");Map<String, String> initParameters = new HashMap<>();initParameters.put("casServerUrlPrefix", casProperties.getCasServerUrlPrefix());authenticationFilter.setInitParameters(initParameters);authenticationFilter.setOrder(1);if (casProperties.getEnabled()) {return authenticationFilter;}return new FilterRegistrationBean<>(new NoCasFilter());}/*** 单点登录认证入口* @return**/@Beanpublic FilterRegistrationBean authenticationFilterRegistrationBean() {FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();authenticationFilter.setFilter(new AuthenticationFilter());Map<String, String> initParameters = new HashMap<>();initParameters.put("casServerLoginUrl", casProperties.getCasServerLoginUrl());initParameters.put("serverName", casProperties.getServerName());authenticationFilter.setInitParameters(initParameters);authenticationFilter.setOrder(2);List<String> urlPatterns = new ArrayList<>();urlPatterns.add("/casLogin");authenticationFilter.setUrlPatterns(urlPatterns);if (casProperties.getEnabled()) {return authenticationFilter;}return new FilterRegistrationBean<>(new NoCasFilter());}/*** 单点登录验证入口* @return**/@Beanpublic FilterRegistrationBean validationFilterRegistrationBean() {FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();authenticationFilter.setFilter(new Cas20ProxyReceivingTicketValidationFilter());Map<String, String> initParameters = new HashMap<>();initParameters.put("casServerUrlPrefix", casProperties.getCasServerUrlPrefix());initParameters.put("serverName", casProperties.getServerName());initParameters.put("encoding", "UTF-8");initParameters.put("useSession", "true");authenticationFilter.setInitParameters(initParameters);authenticationFilter.setOrder(3);List<String> urlPatterns = new ArrayList<>();urlPatterns.add("/*");authenticationFilter.setUrlPatterns(urlPatterns);if (casProperties.getEnabled()) {return authenticationFilter;}return new FilterRegistrationBean<>(new NoCasFilter());}/*** 单点登录获取登录信息* @return**/@Beanpublic FilterRegistrationBean casHttpServletRequestWrapperFilter() {FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();authenticationFilter.setFilter(new HttpServletRequestWrapperFilter());authenticationFilter.setOrder(4);List<String> urlPatterns = new ArrayList<>();urlPatterns.add("/*");authenticationFilter.setUrlPatterns(urlPatterns);if (casProperties.getEnabled()) {return authenticationFilter;}return new FilterRegistrationBean<>(new NoCasFilter());}
}
9、放行casLogin请求
修改common-security模块中的WebMvcConfig.java,放行casLogin请求:
/** 不需要拦截地址 **/
public static final String[] excludeUrls = { "/casLogin", "/login", "/logout", "/refresh", "/register" };
修改nacos中ruoyi-gateway-dev.yml,在网关中放行casLogin请求:
# 不校验白名单ignore:whites:- /auth/casLogin- /auth/getToken- /auth/logout- /auth/login- /auth/register- /auth/updatePassword- /*/v2/api-docs- /csrf
至此,即完成若依微服务后端CAS集成工作。
若依微服务前端集成与分离版相同,请参考若依分离版集成CAS。