若依(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依赖:
org.jasig.cas.client
cas-client-core
3.6.4
2、修改配置文件
在nacos中修改ruoyi-auth-dev.yml(或直接修改auth模块下bootstrap.yml文件),增加cas配置:
cas:
enable: true
server:
url:
prefix: http://127.0.0.1:8888/cas
login: http://127.0.0.1:8888/cas/login
client:
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 {
@Override
public 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 MANAGED_SESSIONS = new HashMap();
private final Map ID_TO_SESSION_KEY_MAPPING = new HashMap();
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private TokenService tokenService;
public CustomSessionMappingStorage() {
}
@Override
public synchronized void addSessionById(String mappingId, HttpSession session) {
this.ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId);
this.MANAGED_SESSIONS.put(mappingId, session);
}
@Override
public 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);
}
@Override
public 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 token = tokenService.createToken(userInfo);
Cookie tokenCookie = new Cookie(Constants.WEB_TOKEN_KEY, (String) token.get("access_token"));
//必须设置path,否则获取不到cookie
tokenCookie.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 {
@Autowired
private CasProperties casProperties;
@Autowired
private CustomSessionMappingStorage customSessionMappingStorage;
/**
* 单点登出过滤器
* @return
**/
@Bean
public FilterRegistrationBean logoutFilter() {
FilterRegistrationBean authenticationFilter = new FilterRegistrationBean<>();
SingleSignOutFilter signOutFilter = new SingleSignOutFilter();
signOutFilter.setSessionMappingStorage(customSessionMappingStorage);
signOutFilter.setIgnoreInitConfiguration(true);
authenticationFilter.setFilter(signOutFilter);
authenticationFilter.addUrlPatterns("/*");
Map 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
**/
@Bean
public FilterRegistrationBean authenticationFilterRegistrationBean() {
FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();
authenticationFilter.setFilter(new AuthenticationFilter());
Map initParameters = new HashMap<>();
initParameters.put("casServerLoginUrl", casProperties.getCasServerLoginUrl());
initParameters.put("serverName", casProperties.getServerName());
authenticationFilter.setInitParameters(initParameters);
authenticationFilter.setOrder(2);
List urlPatterns = new ArrayList<>();
urlPatterns.add("/casLogin");
authenticationFilter.setUrlPatterns(urlPatterns);
if (casProperties.getEnabled()) {
return authenticationFilter;
}
return new FilterRegistrationBean<>(new NoCasFilter());
}
/**
* 单点登录验证入口
* @return
**/
@Bean
public FilterRegistrationBean validationFilterRegistrationBean() {
FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();
authenticationFilter.setFilter(new Cas20ProxyReceivingTicketValidationFilter());
Map 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 urlPatterns = new ArrayList<>();
urlPatterns.add("/*");
authenticationFilter.setUrlPatterns(urlPatterns);
if (casProperties.getEnabled()) {
return authenticationFilter;
}
return new FilterRegistrationBean<>(new NoCasFilter());
}
/**
* 单点登录获取登录信息
* @return
**/
@Bean
public FilterRegistrationBean casHttpServletRequestWrapperFilter() {
FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();
authenticationFilter.setFilter(new HttpServletRequestWrapperFilter());
authenticationFilter.setOrder(4);
List 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。