若依微服务集成CAS,实现单点登录

若依(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。

你可能感兴趣的:(微服务,状态模式,架构)