若依分离版集成CAS单点登录-完整版

2023-05-16

前面用三篇文章介绍了若依前后端分离版集成CAS,实现单点登录功能,同时对功能做了一点优化,一是实现了单点登录成功后重定向页面为用户访问页面;二是解决了单点登出缺陷,三是介绍了解决跨域问题的方法。相信有点经验的朋友可以轻松完成集成,但是读者中肯定有一些小白朋友,将零散的知识糅合在一起存在一点困难,所以在这里贴出完整集成代码,方便大家快速解决问题。

一、后端配置

1、添加cas依赖

在common模块pom添加spring-security-cas依赖:

<!-- spring security cas-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
</dependency>

2、修改配置文件

在admin模块下的application.yml配置文件中添加:

#CAS
cas:
  server:
    host:
      #CAS服务地址
      url: http://127.0.0.1:8888/cas
      #CAS服务登录地址
      login_url: ${cas.server.host.url}/login
      #CAS服务登出地址
      logout_url: ${cas.server.host.url}/logout?service=${app.server.host.url}
  remote:
    server: http://127.0.0.1:8080
# 应用访问地址
app:
  #开启cas
  casEnable: true
  server:
    host:
      url: http://127.0.0.1:${server.port}
  #应用登录地址
  login_url: /
  #应用登出地址
  logout_url: /logout
  #前端登录地址
  web_url: http://127.0.0.1/index

3、修改com.ruoyi.common.core.domain.model.LoginUser.java

由于CAS认证需要authorities属性,此属性不能为空:

@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
  //由于CAS认证需要authorities属性,此属性不能为空,此处为了方便直接new HashSet()
    return new HashSet<>();
}

4、修改com.ruoyi.common.constant.Constants.java

添加CAS认证成功标识:

/**
 * CAS登录成功后的后台标识
 */
public static final String CAS_TOKEN = "cas_token";

/**
 * CAS登录成功后的前台Cookie的Key
 */
public static final String WEB_TOKEN_KEY = "Admin-Token";

5、修改com.ruoyi.framework.web.service.TokenService.java

增加删除用户登录信息方法:

/**
 * cas 删除用户身份信息
 */
public void delClaimsLoginUser(String token)
{
    if (StringUtils.isNotEmpty(token))
    {
        Claims claims = parseToken(token);
        // 解析对应的权限以及用户信息String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
        String userKey = getTokenKey(uuid);
        redisCache.deleteObject(userKey);
    }
}

6、添加CasProperties.java

读取cas配置信息:

package com.ruoyi.framework.config.properties;

/**
 * @author LuoFei
 * @className: CasProperties
 * @projectName RuoYi-Vue-master
 * @description: TODO
 * @date 2022/7/7 9:55
 */
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * CAS的配置参数
 */
@Component
public class CasProperties {
    @Value("${cas.server.host.url}")
    private String casServerUrl;

    @Value("${cas.server.host.login_url}")
    private String casServerLoginUrl;

    @Value("${cas.server.host.logout_url}")
    private String casServerLogoutUrl;

    @Value("${app.casEnable}")
    private boolean casEnable;

    @Value("${app.server.host.url}")
    private String appServerUrl;

    @Value("${app.login_url}")
    private String appLoginUrl;

    @Value("${app.logout_url}")
    private String appLogoutUrl;

    @Value("${app.web_url}")
    private String webUrl;

    public String getWebUrl() {
        return webUrl;
    }

    public String getCasServerUrl() {
        return casServerUrl;
    }

    public void setCasServerUrl(String casServerUrl) {
        this.casServerUrl = casServerUrl;
    }

    public String getCasServerLoginUrl() {
        return casServerLoginUrl;
    }

    public void setCasServerLoginUrl(String casServerLoginUrl) {
        this.casServerLoginUrl = casServerLoginUrl;
    }

    public String getCasServerLogoutUrl() {
        return casServerLogoutUrl;
    }

    public void setCasServerLogoutUrl(String casServerLogoutUrl) {
        this.casServerLogoutUrl = casServerLogoutUrl;
    }

    public boolean isCasEnable() {
        return casEnable;
    }

    public void setCasEnable(boolean casEnable) {
        this.casEnable = casEnable;
    }

    public String getAppServerUrl() {
        return appServerUrl;
    }

    public void setAppServerUrl(String appServerUrl) {
        this.appServerUrl = appServerUrl;
    }

    public String getAppLoginUrl() {
        return appLoginUrl;
    }

    public void setAppLoginUrl(String appLoginUrl) {
        this.appLoginUrl = appLoginUrl;
    }

    public String getAppLogoutUrl() {
        return appLogoutUrl;
    }

    public void setAppLogoutUrl(String appLogoutUrl) {
        this.appLogoutUrl = appLogoutUrl;
    }
}

7、添加CasUserDetailsService.java

在framework.web.service包下添加:

package com.ruoyi.framework.web.service;

/**
 * @author LuoFei
 * @className: CasUserDetailsService
 * @projectName RuoYi-Vue-master
 * @description: TODO
 * @date 2022/7/7 9:57
 */

import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;


/**
 * 用于加载用户信息 实现UserDetailsService接口,或者实现AuthenticationUserDetailsService接口
 */

@Service
public class CasUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {

    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
        String username = token.getName();
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user)) {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户:" + username + " 不存在");
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}

8、添加CasAuthenticationSuccessHandler.java

在framework.security.handle包下添加:

package com.ruoyi.framework.security.handle;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.framework.config.properties.CasProperties;
import com.ruoyi.framework.web.service.TokenService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tomcat.util.http.SameSiteCookies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author LuoFei
 * @className: CasAuthenticationSuccessHandler
 * @projectName RuoYi-Vue-master
 * @description: TODO
 * @date 2022/7/7 10:00
 */
@Service
public class CasAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    protected final Log logger = LogFactory.getLog(this.getClass());

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Autowired
    private TokenService tokenService;

    @Autowired
    private CasProperties casProperties;

    /**
     * 令牌有效期(默认30分钟)
     */
    @Value("${token.expireTime}")
    private int expireTime;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws ServletException, IOException {
        String targetUrlParameter = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl()
                || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
            requestCache.removeRequest(request, response);
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }

        clearAuthenticationAttributes(request);
        LoginUser userDetails = (LoginUser) authentication.getPrincipal();
        String token = tokenService.createToken(userDetails);
        //往Cookie中设置token
//        ResponseCookie cookie = ResponseCookie.from(Constants.WEB_TOKEN_KEY, token).secure(true)
//                .path("/").maxAge(expireTime * 60).sameSite(SameSiteCookies.NONE.getValue()).build();
//        response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());
        Cookie casCookie = new Cookie(Constants.WEB_TOKEN_KEY, token);
        casCookie.setMaxAge(expireTime * 60);
        casCookie.setPath("/");
        response.addCookie(casCookie);

        //设置后端认证成功标识
        HttpSession httpSession = request.getSession();
        httpSession.setAttribute(Constants.CAS_TOKEN, token);
        //登录成功后跳转到前端访问页面
        String url = request.getParameter("redirect");
        getRedirectStrategy().sendRedirect(request, response, url);
    }
}

9、添加CustomCasAuthenticationEntryPoint.java

在framework.security.entrypoint包下添加:

package com.ruoyi.framework.security.entrypoint;

import com.ruoyi.common.utils.StringUtils;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author LuoFei
 * @className: CustomCasAuthenticationEntryPoint
 * @projectName RuoYi-Vue-master
 * @description: TODO
 * @date 2022/5/25 17:27
 */
public class CustomCasAuthenticationEntryPoint extends CasAuthenticationEntryPoint {

    private String serviceUrlBak=null;
    @Override
    protected String createServiceUrl(HttpServletRequest request, HttpServletResponse response) {
        if (serviceUrlBak==null) {
            serviceUrlBak = getServiceProperties().getService();
        }
        //将前端登录成功后跳转页面加入CAS请求中
        if(serviceUrlBak!=null){
            String queryString=request.getQueryString();
            if (StringUtils.isNotNull(queryString)) {
                String serviceUrl = "";
                if (queryString.contains("redirect")) {
                    if (StringUtils.isNotBlank(queryString)) {
                        serviceUrl = "?" + queryString;
                    }
                }
                getServiceProperties().setService(serviceUrlBak + serviceUrl);
            }
        }
        return super.createServiceUrl(request, response);
    }
}

10、添加CustomSessionMappingStorage.java

在framework.security.entrypoint包下添加:

package com.ruoyi.framework.security.storage;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.framework.web.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: TODO
 * @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());

    @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);
    }

    /**
     * 根据CAS发送的id,查找后端用户session中的token,并删除
     * @param mappingId
     * @return
     */
    @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.delClaimsLoginUser(token);
            } catch (IllegalStateException e) {
                this.logger.error("已成功登出");
            }
        }
        return session;
    }
}

11、修改SecurityConfig.java

添加cas的处理逻辑:

package com.ruoyi.framework.config;

import com.ruoyi.framework.config.properties.CasProperties;
import com.ruoyi.framework.security.entrypoint.CustomCasAuthenticationEntryPoint;
import com.ruoyi.framework.security.handle.CasAuthenticationSuccessHandler;
import com.ruoyi.framework.security.storage.CustomSessionMappingStorage;
import com.ruoyi.framework.web.service.CasUserDetailsService;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.filter.CorsFilter;
import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;

/**
 * spring security配置
 *
 * @author ruoyi
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 允许匿名访问的地址
     */
    @Autowired
    private PermitAllUrlProperties permitAllUrl;

    /**
     * cas
     */
    @Autowired
    private CasProperties casProperties;

    @Autowired
    private CasUserDetailsService customUserDetailsService;

    @Autowired
    private CasAuthenticationSuccessHandler casAuthenticationSuccessHandler;

    @Autowired
    private CustomSessionMappingStorage customSessionMappingStorage;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        // 注解标记允许匿名访问的url
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
        permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());

        if (!casProperties.isCasEnable()) {
            httpSecurity
                    // CSRF禁用,因为不使用session
                    .csrf().disable()
                    // 认证失败处理类
                    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                    // 基于token,所以不需要session
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                    // 过滤请求
                    .authorizeRequests()
                    // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                    .antMatchers("/login", "/register", "/captchaImage", "/updatePassword").anonymous()
                    // 静态资源,可匿名访问
                    .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                    .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated()
                    .and()
                    .headers().frameOptions().disable();
            // 添加Logout filter
            httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
            // 添加JWT filter
            httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
            // 添加CORS filter
            httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
            httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
        }
        //开启cas
        if (casProperties.isCasEnable()) {
            httpSecurity
                    // CSRF禁用,因为不使用session
                    .csrf().disable()
                    // 基于token,所以不需要session
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                    // 过滤请求
                    .authorizeRequests()
                    // 对于登录login 验证码captchaImage 允许匿名访问
                    //.antMatchers("/login", "/captchaImage").anonymous()
                    .antMatchers(
                            HttpMethod.GET,
                            "/*.html",
                            "/**/*.html",
                            "/**/*.css",
                            "/**/*.js"
                    ).permitAll()
                    /*.antMatchers("/profile/**").anonymous()
                    .antMatchers("/common/download**").anonymous()
                    .antMatchers("/common/download/resource**").anonymous()*/
                    .antMatchers("/swagger-ui.html").anonymous()
                    .antMatchers("/swagger-resources/**").anonymous()
                    .antMatchers("/webjars/**").anonymous()
                    .antMatchers("/*/api-docs").anonymous()
                    .antMatchers("/druid/**").anonymous()
                    /*.antMatchers("/websocket/**").anonymous()
                    .antMatchers("/magic/web/**").anonymous()*/
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated()
                    .and()
                    .headers().frameOptions().disable();
            //单点登录登出
            httpSecurity.logout().permitAll().logoutSuccessHandler(logoutSuccessHandler);
            // Custom JWT based security filter
            httpSecurity.addFilter(casAuthenticationFilter())
                    .addFilterBefore(authenticationTokenFilter, CasAuthenticationFilter.class)
                    .addFilterBefore(casLogoutFilter(), CasAuthenticationFilter.class)
                    .addFilterBefore(singleSignOutFilter(), JwtAuthenticationTokenFilter.class)
                    .exceptionHandling()

                    //认证失败
                    .authenticationEntryPoint(casAuthenticationEntryPoint());

            // 添加CORS filter
            httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
            httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
            // disable page caching
            httpSecurity.headers().cacheControl();
        }
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        if (!casProperties.isCasEnable()) {
            auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
        }
        // cas
        if (casProperties.isCasEnable()) {
            super.configure(auth);
            auth.authenticationProvider(casAuthenticationProvider());
        }
    }

    /**
     * 认证的入口
     */
    @Bean
    public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
        CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CustomCasAuthenticationEntryPoint();
        casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
        casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
        return casAuthenticationEntryPoint;
    }

    /**
     * 指定service相关信息
     */
    @Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService(casProperties.getAppServerUrl()+casProperties.getAppLoginUrl());
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }

    /**
     * CAS认证过滤器
     */
    @Bean
    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
        casAuthenticationFilter.setAuthenticationManager(authenticationManager());
        casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
        casAuthenticationFilter.setAuthenticationSuccessHandler(casAuthenticationSuccessHandler);
        return casAuthenticationFilter;
    }

    /**
     * cas 认证 Provider
     */
    @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
        casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService);
        casAuthenticationProvider.setServiceProperties(serviceProperties());
        casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
        casAuthenticationProvider.setKey("casAuthenticationProviderKey");
        return casAuthenticationProvider;
    }

    @Bean
    public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
        return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl());
    }

    /**
     * 单点登出过滤器
     * 使用customSessionMappingStorage处理cas发送的登出请求
     */
    @Bean
    public SingleSignOutFilter singleSignOutFilter() {
        SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
        //singleSignOutFilter.setLogoutCallbackPath(casProperties.getWebUrl());
        singleSignOutFilter.setSessionMappingStorage(customSessionMappingStorage);
        singleSignOutFilter.setIgnoreInitConfiguration(true);
        return singleSignOutFilter;
    }

    /**
     * 请求单点退出过滤器
     */
    @Bean
    public LogoutFilter casLogoutFilter() {
        LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(),
                new SecurityContextLogoutHandler());
        logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
        return logoutFilter;
    }

    @Bean
    public ServletListenerRegistrationBean singleSignOutHttpSessionListener() {
        ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
        bean.setListener(new SingleSignOutHttpSessionListener());
//        bean.setName(""); //默认为bean name
        bean.setEnabled(true);
        //bean.setOrder(Ordered.HIGHEST_PRECEDENCE); //设置优先级
        return bean;
    }
}

二、前端配置

前端使用Vue3+Element-Plus+Vite,请大家至gitee网站获取。

1、修改settings.js

添加cas登录和登出地址:

/**
* 开启cas
*/
casEnable: true,

/**
*  单点登录url
*/
casloginUrl: 'http://127.0.0.1:8787/?redirect=http://127.0.0.1:88',
/**
*  单点登出url
*/
caslogoutUrl: 'http://127.0.0.1:8888/cas/logout?service=http://127.0.0.1:88',

2、修改permission.js

判断没有token时访问cas登录页面:

import router from './router'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'
import { isRelogin } from '@/utils/request'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
// 引入settings,使用cas配置
import defaultSettings from'@/settings'

NProgress.configure({ showSpinner: false });

const whiteList = ['/login', '/auth-redirect', '/bind', '/register'];

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    to.meta.title && useSettingsStore().setTitle(to.meta.title)
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      if (useUserStore().roles.length === 0) {
        isRelogin.show = true
        // 判断当前用户是否已拉取完user_info信息
        useUserStore().getInfo().then(() => {
          isRelogin.show = false
          usePermissionStore().generateRoutes().then(accessRoutes => {
            // 根据roles权限生成可访问的路由表
            accessRoutes.forEach(route => {
              if (!isHttp(route.path)) {
                router.addRoute(route) // 动态添加可访问路由表
              }
            })
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch(err => {
          useUserStore().logOut().then(() => {
            ElMessage.error(err)
            next({ path: '/' })
          })
        })
      } else {
        next()
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      if (to.path === '/login' && defaultSettings.casEnable) {
        // 访问原系统登录地址,且开启CAS,重定向到cas登录页
        window.location.href = defaultSettings.casloginUrl+to.fullPath
      } else {
        // 在免登录白名单,直接进入
        next()
      }
    } else {
      //cas
      if (!defaultSettings.casEnable) {
        next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      }
      if (defaultSettings.casEnable) {
        // 开启CAS,全部重定向到cas登录页
        window.location.href = defaultSettings.casloginUrl+to.fullPath
      }
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

3、修改request.js

修改utils目录下request.js文件“响应拦截器”,登出后不做响应:

// 响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    // 二进制数据则直接返回
    if(res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer'){
      return res.data
    }
    if (code === 401) {
      if (!isRelogin.show) {
        isRelogin.show = true;
        ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).then(() => {
        isRelogin.show = false;
        useUserStore().logOut().then(() => {
          //cas
          if (!defaultSettings.casEnable) {
            location.href = '/index';
          }
        })
      }).catch(() => {
        isRelogin.show = false;
      });
    }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
      ElMessage({
        message: msg,
        type: 'error'
      })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      ElNotification.error({
        title: msg
      })
      return Promise.reject('error')
    } else {
      return  Promise.resolve(res.data)
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    }
    else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    }
    else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    ElMessage({
      message: message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

4、修改Navbar.vue

修改layout/components目录下Navbar.vue文件的logout()方法,登出后不做响应:

function logout() {
  ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    userStore.logOut().then(() => {
      //cas
      if (!settings.casEnable) {
        location.href = '/index';
      }
    })
  }).catch(() => { });
}

5、修改user.js

修改store/modules目录下user.js的logOut()方法:

// 退出系统
logOut() {
    return new Promise((resolve, reject) => {
      logout(this.token).then(() => {
        this.token = ''
        this.roles = []
        this.permissions = []
        removeToken()
        resolve()
        //cas
        if (defaultSettings.casEnable) {
          window.location.href = defaultSettings.caslogoutUrl
        }
      }).catch(error => {
        reject(error)
      })
    })
}

至此,已完成若依前后端分离版集成CAS实现单点登录,并解决了之前提到的问题。祝大家启动成功!

跨域问题请参考上篇文章。

参考:https://gitee.com/ggxforever/RuoYi-Vue-cas

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

若依分离版集成CAS单点登录-完整版 的相关文章

  • 笔试题记录

    1 逆波兰表达式 是称为 后缀表达式 xff0c 把运算量写在前面 xff0c 把算符写在后面 写出a b c d 43 e f g h 43 i j k 的逆波兰表达式 拆开写各个部分的 xff1a 按优先级 xff08 1 xff09
  • 牛客网 赛码在线编程中数据读取问题

    一 数据读取的方式 xff08 python3 xff09 1 input 读取输入数据 while True try inputs 61 input except break 2 网站的数据输入是是一个含有多行数据的input文件 in文
  • 贪心算法 leetcode编程题

    1 452 用最少数量的箭引爆气球 在二维空间中有许多球形的气球 对于每个气球 xff0c 提供的输入是水平方向上 xff0c 气球直径的开始和结束坐标 由于它是水平的 xff0c 所以纵坐标并不重要 xff0c 因此只要知道开始和结束的横
  • 排序题 LeetCode题

    1 1370 上升下降字符串 给你一个字符串 s xff0c 请你根据下面的算法重新构造字符串 xff1a 从 s 中选出 最小 的字符 xff0c 将它 接在 结果字符串的后面 从 s 剩余字符中选出 最小 的字符 xff0c 且该字符比
  • BP算法公式推导

    令表示第层的第个神经元到第层的第个神经元的连接权值 xff0c 表示第层第个神经元的输入 xff0c 表示第层第个神经元的输出 xff0c 表示层第个神经元的偏置 xff0c C表示代价函数 则有 xff1a 其中 表示激活函数 训练多层网
  • 卷积神经网络中的Separable Convolution_转载

    卷积神经网络在图像处理中的地位已然毋庸置疑 卷积运算具备强大的特征提取能力 相比全连接又消耗更少的参数 xff0c 应用在图像这样的二维结构数据中有着先天优势 然而受限于目前移动端设备硬件条件 xff0c 显著降低神经网络的运算量依旧是网络
  • 二分查找方法 leetcode题

    这里用来 记录 使用二分查找方法 的题目 1 704 二分查找 给定一个 n 个元素有序的 xff08 升序 xff09 整型数组 nums 和一个目标值 target xff0c 写一个函数搜索 nums 中的 target xff0c
  • 动态规划题 leetcode

    1 62 不同路径 一个机器人位于一个 m x n 网格的左上角 xff08 起始点在下图中标记为 Start xff09 机器人每次只能向下或者向右移动一步 机器人试图达到网格的右下角 xff08 在下图中标记为 Finish xff09
  • 字典序 Leetcode题目

    1 440 字典序的第K小数字 给定整数 n 和 k xff0c 找到 1 到 n 中字典序第 k 小的数字 注意 xff1a 1 k n 109 示例 xff1a 输入 n 13 k 2 输出 10 解释 字典序的排列是 1 10 11
  • Git:从Git下载项目到本地及项目重建

    从Git上下载项目 重建项目
  • 子序列题目 Leetcode题目

    1 53 最大子序和 面试题 16 17 连续数列 给定一个整数数组 nums xff0c 找到一个具有最大和的连续子数组 xff08 子数组最少包含一个元素 xff09 xff0c 返回其最大和 示例 xff1a 输入 2 1 3 4 1
  • 整数问题(求和 拆分 替换等) leetcode问题

    1 829 连续整数求和 给定一个正整数 N xff0c 试求有多少组连续正整数满足所有数字之和为 N 示例 1 输入 5 输出 2 解释 5 61 5 61 2 43 3 xff0c 共有两组连续整数 5 2 3 求和后为 5 示例 2
  • B 中等题 LeetCode

    中等题 按出题指数排列 1 2 两数相加 给出两个 非空 的链表用来表示两个非负的整数 其中 xff0c 它们各自的位数是按照 逆序 的方式存储的 xff0c 并且它们的每个节点只能存储 一位 数字 如果 xff0c 我们将这两个数相加起来
  • 回文串 leetcode

    1 125 验证回文串 给定一个字符串 xff0c 验证它是否是回文串 xff0c 只考虑字母和数字字符 xff0c 可以忽略字母的大小写 说明 xff1a 本题中 xff0c 我们将空字符串定义为有效的回文串 示例 1 输入 34 A m
  • 回溯法 题目 leetcode

    1 22 括号生成 数字 n 代表生成括号的对数 xff0c 请你设计一个函数 xff0c 用于能够生成所有可能的并且 有效的 括号组合 示例 xff1a 输入 xff1a n 61 3 输出 xff1a 34 34 34 34 34 34
  • 买卖股票系列问题 Leetcode题目

    leetcode 刷题记录 一 买卖股票系列问题 1 121 买卖股票的最佳时机 给定一个数组 xff0c 它的第 i 个元素是一支给定股票第 i 天的价格 如果你最多只允许完成一笔交易 xff08 即买入和卖出一支股票一次 xff09 x
  • 区间问题 LeetCode

    表示重点复习题 xff0c 没做出来 区间问题 1 56 合并区间 给出一个区间的集合 xff0c 请合并所有重叠的区间 示例 1 输入 intervals 61 1 3 2 6 8 10 15 18 输出 1 6 8 10 15 18 解
  • 网格DFS LeetCode

    岛屿问题 DFS 200 岛屿数量 给你一个由 1 xff08 陆地 xff09 和 0 xff08 水 xff09 组成的的二维网格 xff0c 请你计算网格中岛屿的数量 岛屿总是被水包围 xff0c 并且每座岛屿只能由水平方向和 或竖直
  • 括号题目 Leetcode

    括号系列问题 Leetcode 20 有效的括号 给定一个只包括 39 39 xff0c 39 39 xff0c 39 39 xff0c 39 39 xff0c 39 39 xff0c 39 39 的字符串 s xff0c 判断字符串是否有
  • 回溯 组合 排列 生成 LeetCode

    回溯 组合 排列 生成 算法题目 77 组合 给定两个整数 n 和 k xff0c 返回 1 n 中所有可能的 k 个数的组合 示例 输入 n 61 4 k 61 2 输出 2 4 3 4 2 3 1 2 1 3 1 4 解法1 回溯 递归

随机推荐

  • Centos 7安装Python3环境

    目录 1 安装依赖环境2 下载Python压缩包3 解压Python压缩包4 编译安装4 1 编译前安装相关软件4 2 创建安装目录4 3 生成编译脚本4 4 编译和安装4 5 检查 5 建立软链接6 设置Python3的环境变量6 1 设
  • Linux命令c++filt

    一个简单的linux命令 xff0c 确实不值得大费周折 xff0c 但是 xff0c 在实际的开发过程中 xff0c 却帮助很大 xff0c 在编译cgi xff0c 修改函数的调用之后获得函数的符号名 xff0c 就可以看到这个函数的定
  • 子序列 子数组问题 Leetcode

    子序列 子数组问题 53 最大子序和 给定一个整数数组 nums xff0c 找到一个具有最大和的连续子数组 xff08 子数组最少包含一个元素 xff09 xff0c 返回其最大和 示例 1 xff1a 输入 xff1a nums 61
  • 用devstack快速部署 openstack

    本人openstack小白一名 xff0c 在部署openstack时出现了很多不可逾越的错误 xff0c 只好借助于工具 在用devstack安装openstack之前 xff0c 先用的openshit进行安装 但是出现了无法连接数据库
  • Debain 10安装使用中文输入法

    Debain 10安装使用中文输入法 近来因工作需要 xff0c 需在虚拟机上搭建服务 xff0c 公司虚拟机上用的是Debain系统 xff0c 在使用的过程中觉着没有中文输入法极其不爽 xff0c 于是按照网上多篇教程一步步尝试至最终切
  • 【算法题】素数伴侣-用匈牙利算法解决二分图匹配问题

    描述 输入一个字符串 xff0c 内容是一个不重复的整数数组 若数组中两个数相加为素数 xff0c 那么这两个数可以配对 xff0c 即素数伴侣 一个数字只能配对一次 输出最大配对数 示例 输入 xff1a 4 2 5 6 13 输出 xf
  • MAX30102

    include 34 myiic h 34 include 34 delay h 34 初始化IIC void IIC Init void GPIO InitTypeDef GPIO InitStructure RCC gt APB2ENR
  • linux系统开机自动挂载分区

    以挂载分区 dev sdb1为例 1 查看所有的分区 xff1a sudo fdisk l 1 修改分区类型 fdisk dev sdb 输入m可以查看帮助 xff0c 修改分区类型需要使用t命令 xff0c 并输入分区类型标号 2 查看分
  • Python文件处理相关函数

    文件 xff0c 文件夹 xff0c 压缩包处理模块模块 shutil模块 引入 xff1a import shutil shutil是对OS中文件操作的补充 xff1a 移动 复制 打包 压缩 解压 1 copy文件内容到另一个文件 xf
  • Python多线程

    线程的创建 加入 锁 待补充完善
  • noVNC压缩包下载

    1 浏览器器输入地址 Git地址 xff1a GitHub Where the world builds software GitHub https github com 2 搜索noVNC 3 选择novnc noVNC 4 点击发行版本
  • Python数据类型(结构)

    Python复合数据类型之一 列表 即写在方括号之间 用逗号隔开的数值列表 列表内的数值不必全是相同的类型 list1 span class token operator 61 span span class token punctuati
  • Python条件循环控制语句

    Python 条件语句 通过一条或多条语句的执行结果 xff08 True或者False xff09 来决定执行的代码块 Python程序语言指定任何非0和非空 xff08 null xff09 值为true xff0c 0 或者 null
  • Python面向对象编程

    Python面向对象编程
  • Python基础

    span class token comment 这是单行注释 span span class token triple quoted string string 39 39 39 多行注释 多行注释 39 39 39 span span
  • Python运算符

    Python 运算符 算术运算符 比运 关系 算符较 赋值运算符 逻辑运算符 位运算符 成员运算符 身份运算符 1 Python算术运算符 下假设变量 xff1a a 61 10 xff0c b 61 20 43 加 两个对象相加 a 43
  • Ubuntu环境搭建(NFS、ftp、ssh)

    文章目录 1 安装FTP2 安装NFS服务3 安装SSH 1 安装FTP 在开发的过程中会频繁的在 Windows 和 Ubuntu 下进行文件传输 xff0c 比如在 Windwos 下进行 代码编写 xff0c 然后将编写好的代码拿到
  • boa服务器之boa.conf文件的基本配置详解

    1 Port span class hljs number 80 span 2 User root span class hljs keyword 3 Group span root 4 ErrorLog dev console 5 Acc
  • 【文献阅读】VQA入门——Tips and Tricks for Visual Question Answering: Learnings from the 2017 Challenge

    本人在读研一 xff0c 想要学习多模态这一块的工作 我在这里记录下我看的第一篇论文 Tips and Tricks for Visual Question Answering Learnings from the 2017 Challen
  • 若依分离版集成CAS单点登录-完整版

    前面用三篇文章介绍了若依前后端分离版集成CAS xff0c 实现单点登录功能 xff0c 同时对功能做了一点优化 xff0c 一是实现了单点登录成功后重定向页面为用户访问页面 xff1b 二是解决了单点登出缺陷 xff0c 三是介绍了解决跨