RuoYi-Vue前后端分离版集成Cas单点登录

前端 0

后端

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 相关配置 start# CAS服务器配置cas:  server:    host:      #CAS服务地址      url: http://host:port/sso      #CAS ticket 验证 服务地址      ticket_validator_url: http://host:port/sso      #CAS服务登录地址      login_url: ${cas.server.host.url}/login      #CAS服务登出地址      logout_url: ${cas.server.host.url}/logout?service=${cas.server.host.url}/login?service=${app.server.host.url}#应用访问地址app:  #项目名称  name: Xxx  #是否开启CAS  casEnable: true  server:    host:      #项目地址      url: http://host:${server.port}  #应用登录地址  login_url: /  #应用登出地址  logout_url: /logout  #前端回调地址  callback_url: /cas/index  #前端登录地址  web_url: http://host:port/xxx_vue# CAS 相关配置 end

3.修改LoginUser.java

  • 由于CAS认证需要authorities属性,此属性不能为空,此处为了方便直接new HashSet():
package com.ruoyi.common.core.domain.model;import java.util.Collection;import java.util.HashSet;import java.util.Map;import java.util.Set;import lombok.Data;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import com.alibaba.fastjson2.annotation.JSONField;import com.ruoyi.common.core.domain.entity.SysUser;/** * 登录用户身份权限 *  * @author ruoyi */@Datapublic class LoginUser implements UserDetails{    private static final long serialVersionUID = 1L;    /**     * 用户ID     */    private Long userId;    /**     * 部门ID     */    private Long deptId;    /**     * 用户唯一标识     */    private String token;    /**     * 登录时间     */    private Long loginTime;    /**     * 过期时间     */    private Long expireTime;    /**     * 登录IP地址     */    private String ipaddr;    /**     * 登录地点     */    private String loginLocation;    /**     * 浏览器类型     */    private String browser;    /**     * 操作系统     */    private String os;    /**     * 权限列表     */    private Set<String> permissions;    /**     * 用户信息     */    private SysUser user;    // CAS用户信息    private Map<String, Object> attributes;    public LoginUser()    {    }    public LoginUser(SysUser user, Set<String> permissions)    {        this.user = user;        this.permissions = permissions;    }    public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)    {        this.userId = userId;        this.deptId = deptId;        this.user = user;        this.permissions = permissions;    }    public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions, Map<String, Object> attributes) {        this.userId = userId;        this.deptId = deptId;        this.user = user;        this.permissions = permissions;        this.attributes = attributes;    }    @JSONField(serialize = false)    @Override    public String getPassword()    {        return user.getPassword();    }    @Override    public String getUsername()    {        return user.getUserName();    }    /**     * 账户是否未过期,过期无法验证     */    @JSONField(serialize = false)    @Override    public boolean isAccountNonExpired()    {        return true;    }    /**     * 指定用户是否解锁,锁定的用户无法进行身份验证     *      * @return     */    @JSONField(serialize = false)    @Override    public boolean isAccountNonLocked()    {        return true;    }    /**     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证     *      * @return     */    @JSONField(serialize = false)    @Override    public boolean isCredentialsNonExpired()    {        return true;    }    /**     * 是否可用 ,禁用的用户不能身份验证     *      * @return     */    @JSONField(serialize = false)    @Override    public boolean isEnabled()    {        return true;    }    @Override    public Collection<? extends GrantedAuthority> getAuthorities() {        return new HashSet<>();    }}

4.修改 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.添加 CasProperties.java

  • 在framework模块下添加读取cas配置信息:
package com.ruoyi.framework.config.properties;import lombok.Data;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;/** * @author Wen先森 * @description CAS的配置参数 * @date 2024/7/26 16:58 */@Data@Componentpublic class CasProperties {    @Value("${cas.server.host.url}")    private String casServerUrl;    @Value("${cas.server.host.ticket_validator_url}")    private String casServerTicketValidatorUrl;    @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.callback_url}")    private String callbackUrl;    @Value("${app.web_url}")    private String webUrl;}

6.添加 UserDetailsServiceCasImpl

  • 在framework模块下添加:
package com.ruoyi.framework.web.service.impl;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.framework.web.service.SysPermissionService;import com.ruoyi.system.service.ISysUserService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;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;import java.util.Map;/** * @author Wen先森 * @description 用于加载用户信息,实现UserDetailsService接口,或者实现AuthenticationUserDetailsService接口。 * @date 2024/7/26 17:02 */@Servicepublic class UserDetailsServiceCasImpl implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceCasImpl.class);    private final ISysUserService userService;    private final SysPermissionService permissionService;    public UserDetailsServiceCasImpl(ISysUserService userService, SysPermissionService permissionService) {        this.userService = userService;        this.permissionService = 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, token.getAssertion().getPrincipal().getAttributes());    }    public UserDetails createLoginUser(SysUser user, Map<String, Object> attributes) {        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user), attributes);    }}

7.添加 CasAuthenticationSuccessHandler.java

  • 在framework模块下添加:
package com.ruoyi.framework.security.handle;import com.ruoyi.common.constant.CacheConstants;import com.ruoyi.common.constant.Constants;import com.ruoyi.common.core.domain.model.LoginUser;import com.ruoyi.common.core.redis.RedisCache;import com.ruoyi.framework.config.properties.CasProperties;import com.ruoyi.framework.web.service.TokenService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;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;import java.util.concurrent.TimeUnit;/** * @author Wen先森 * @description CAS认证中心 * @date 2024/7/26 17:05 */@Servicepublic class CasAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {    private static final Logger log = LoggerFactory.getLogger(CasAuthenticationSuccessHandler.class);    private static final RequestCache requestCache = new HttpSessionRequestCache();    private final RedisCache redisCache;    private final TokenService tokenService;    private final CasProperties casProperties;    // 令牌有效期(默认30分钟)    @Value("${token.expireTime}")    private int expireTime;    public CasAuthenticationSuccessHandler(RedisCache redisCache, TokenService tokenService, CasProperties casProperties) {        this.redisCache = redisCache;        this.tokenService = tokenService;        this.casProperties = casProperties;    }    @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);        // 打印日志        log.debug("CAS认证中心的ticket:"+authentication.getCredentials().toString());        // 往Redis中设置token        redisCache.setCacheObject(CacheConstants.LOGIN_TICKET_KEY+authentication.getCredentials().toString(), token, expireTime, TimeUnit.MINUTES);        // 往Cookie中设置token        Cookie casCookie = new Cookie(Constants.WEB_TOKEN_KEY, token);        casCookie.setMaxAge(expireTime * 60);        // TODO: 设置 cookie path 为 根目录(解决前端 cookie 丢失问题), 不确定 是否合理        casCookie.setPath("/");        response.addCookie(casCookie);        // 设置后端认证成功标识        HttpSession httpSession = request.getSession();        httpSession.setAttribute(Constants.CAS_TOKEN, token);        httpSession.setMaxInactiveInterval(expireTime * 60);        // 登录成功后跳转到前端登录页面        getRedirectStrategy().sendRedirect(request, response, casProperties.getWebUrl());    }}

8.修改 SecurityConfig

  • 添加cas的处理逻辑:
package com.ruoyi.framework.config;import com.ruoyi.framework.config.properties.CasProperties;import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;import com.ruoyi.framework.security.filter.SingleSignOutTokenFilter;import com.ruoyi.framework.security.handle.CasAuthenticationSuccessHandler;import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;import com.ruoyi.framework.web.service.impl.UserDetailsServiceCasImpl;import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.Ordered;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.http.SessionCreationPolicy;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.web.authentication.logout.LogoutFilter;import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;import org.springframework.web.filter.CorsFilter;/** * spring security配置 * * @author ruoyi */@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {    private final CasProperties casProperties;    private final UserDetailsServiceCasImpl userDetailsServiceCasImpl;    private final CasAuthenticationSuccessHandler casAuthenticationSuccessHandler;    // 跨域过滤器    private final CorsFilter corsFilter;    // 自定义用户认证逻辑    private final UserDetailsService userDetailsService;    // 自定义用户退出处理类    private final LogoutSuccessHandlerImpl logoutSuccessHandler;    // 自定义用户token认证过滤器    private final JwtAuthenticationTokenFilter authenticationTokenFilter;    public SecurityConfig(CasProperties casProperties, UserDetailsServiceCasImpl userDetailsServiceCasImpl, CasAuthenticationSuccessHandler casAuthenticationSuccessHandler, CorsFilter corsFilter, UserDetailsService userDetailsService, LogoutSuccessHandlerImpl logoutSuccessHandler, JwtAuthenticationTokenFilter authenticationTokenFilter) {        this.casProperties = casProperties;        this.userDetailsServiceCasImpl = userDetailsServiceCasImpl;        this.casAuthenticationSuccessHandler = casAuthenticationSuccessHandler;        this.corsFilter = corsFilter;        this.userDetailsService = userDetailsService;        this.logoutSuccessHandler = logoutSuccessHandler;        this.authenticationTokenFilter = authenticationTokenFilter;    }    /**     * 解决无法直接注入AuthenticationManager     *     * @return AuthenticationManager     */    @Bean    @Override    public AuthenticationManager authenticationManagerBean() throws Exception {        return super.authenticationManagerBean();    }    /**     * anyRequest          |   匹配所有请求路径     * access              |   SpringEl表达式结果为true时可以访问     * anonymous           |   匿名可以访问     * denyAll             |   用户不能访问     * fullyAuthenticated  |   用户完全认证可以访问(非RememberMe下自动登录)     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问     * permitAll           |   用户可以任意访问     * rememberMe          |   允许通过RememberMe登录的用户访问     * authenticated       |   用户登录后可访问     */    @Override    protected void configure(HttpSecurity httpSecurity) throws Exception {        httpSecurity                // CSRF禁用,因为不使用session                .csrf().disable()                // 基于token,所以不需要session                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()                // 过滤请求                .authorizeRequests()                // 对于登录login,注册register,验证码captchaImage可以任意访问                // .antMatchers("/login", "/register", "/captchaImage").permitAll()                // 静态资源可以任意访问                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()                .antMatchers("/swagger-ui.html").permitAll()                .antMatchers("/swagger-resources/**").permitAll()                .antMatchers("/webjars/**").permitAll()                .antMatchers("/*/api-docs").permitAll()                .antMatchers("/druid/**").permitAll()                // 除上面外的所有请求全部需要鉴权认证                .anyRequest().authenticated()                .and()                .headers().frameOptions().disable();        // 单点登录登出        httpSecurity.logout().permitAll().logoutSuccessHandler(logoutSuccessHandler);        // 添加CAS filter        httpSecurity.addFilter(casAuthenticationFilter())                // 请求单点退出过滤器                // .addFilterBefore(casLogoutFilter(), LogoutFilter.class)                // token认证过滤器                .addFilterBefore(authenticationTokenFilter, CasAuthenticationFilter.class)                // 单点登出过滤器                .addFilterBefore(singleSignOutTokenFilter(), CasAuthenticationFilter.class).exceptionHandling()                // 认证失败                .authenticationEntryPoint(casAuthenticationEntryPoint());        // 添加CORS filter        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);        // 禁用缓存        httpSecurity.headers().cacheControl();    }    /**     * 强散列哈希加密实现注册     *     * @return 强散列哈希加密实现     */    @Bean    public BCryptPasswordEncoder bCryptPasswordEncoder() {        return new BCryptPasswordEncoder();    }    /**     * 身份认证接口     *     * @param auth 身份认证     * @throws Exception 异常抛出     */    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        if (casProperties.isCasEnable()) {            super.configure(auth);            auth.authenticationProvider(casAuthenticationProvider());        } else {            auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());        }    }    /**     * CAS认证的入口注册     *     * @return CAS认证的入口     */    @Bean    public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {        CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();        casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());        casAuthenticationEntryPoint.setServiceProperties(serviceProperties());        return casAuthenticationEntryPoint;    }    /**     * 指定service相关信息注册     *     * @return 指定service相关信息     */    @Bean    public ServiceProperties serviceProperties() {        ServiceProperties serviceProperties = new ServiceProperties();        serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());        serviceProperties.setAuthenticateAllArtifacts(true);        return serviceProperties;    }    /**     * CAS认证过滤器注册     *     * @return CAS认证过滤器     * @throws Exception 异常抛出     */    @Bean    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();        casAuthenticationFilter.setAuthenticationManager(authenticationManager());        casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());        casAuthenticationFilter.setAuthenticationSuccessHandler(casAuthenticationSuccessHandler);        return casAuthenticationFilter;    }    /**     * CAS认证Provider注册     *     * @return CAS认证Provider     */    @Bean    public CasAuthenticationProvider casAuthenticationProvider() {        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();        casAuthenticationProvider.setAuthenticationUserDetailsService(userDetailsServiceCasImpl);        casAuthenticationProvider.setServiceProperties(serviceProperties());        casAuthenticationProvider.setTicketValidator(cas30ServiceTicketValidator());        casAuthenticationProvider.setKey("casAuthenticationProviderKey");        return casAuthenticationProvider;    }    /**     * CAS服务票据验证器注册     *     * @return CAS服务票据验证器     */    @Bean    public Cas30ServiceTicketValidator cas30ServiceTicketValidator() {        return new Cas30ServiceTicketValidator(casProperties.getCasServerTicketValidatorUrl());    }    /**     * 单点登出过滤器注册     *     * @return 单点登出过滤器     */    @Bean    public SingleSignOutTokenFilter singleSignOutTokenFilter() {        SingleSignOutTokenFilter singleSignOutTokenFilter = new SingleSignOutTokenFilter();        singleSignOutTokenFilter.setIgnoreInitConfiguration(true);        return singleSignOutTokenFilter;    }    /**     * 单点登出监听器注册     *     * @return 单点登出监听器     */    @Bean    public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListenerBean() {        ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> listenerRegistrationBean = new ServletListenerRegistrationBean<>();        listenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());        listenerRegistrationBean.setEnabled(true);        listenerRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);        return listenerRegistrationBean;    }    /**     * 请求单点退出过滤器注册     *     * @return 单点退出过滤器     */    @Bean    public LogoutFilter casLogoutFilter() {        LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());        logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());        return logoutFilter;    }}

9.添加 SingleSignOutTokenFilter

  • 单点退出过滤器
package com.ruoyi.framework.security.filter;import com.ruoyi.framework.security.handle.SingleSignOutHandlerImpl;import org.jasig.cas.client.configuration.ConfigurationKeys;import org.jasig.cas.client.session.SessionMappingStorage;import org.jasig.cas.client.util.AbstractConfigurationFilter;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.concurrent.atomic.AtomicBoolean;/** * @author Wen先森 * @description 单点退出过滤器 * @date 2024/8/1 20:59 */public final class SingleSignOutTokenFilter extends AbstractConfigurationFilter {    private static final SingleSignOutHandlerImpl HANDLER = new SingleSignOutHandlerImpl();    private final AtomicBoolean handlerInitialized = new AtomicBoolean(false);    @Override    public void init(final FilterConfig filterConfig) throws ServletException {        super.init(filterConfig);        if (!isIgnoreInitConfiguration()) {            setArtifactParameterName(getString(ConfigurationKeys.ARTIFACT_PARAMETER_NAME));            setLogoutParameterName(getString(ConfigurationKeys.LOGOUT_PARAMETER_NAME));            setRelayStateParameterName(getString(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME));            setLogoutCallbackPath(getString(ConfigurationKeys.LOGOUT_CALLBACK_PATH));            HANDLER.setArtifactParameterOverPost(getBoolean(ConfigurationKeys.ARTIFACT_PARAMETER_OVER_POST));            HANDLER.setEagerlyCreateSessions(getBoolean(ConfigurationKeys.EAGERLY_CREATE_SESSIONS));        }        HANDLER.init();        handlerInitialized.set(true);    }    public void setArtifactParameterName(final String name) {        HANDLER.setArtifactParameterName(name);    }    public void setLogoutParameterName(final String name) {        HANDLER.setLogoutParameterName(name);    }    public void setRelayStateParameterName(final String name) {        HANDLER.setRelayStateParameterName(name);    }    public void setLogoutCallbackPath(final String logoutCallbackPath) {        HANDLER.setLogoutCallbackPath(logoutCallbackPath);    }    public void setSessionMappingStorage(final SessionMappingStorage storage) {        HANDLER.setSessionMappingStorage(storage);    }    @Override    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {        final HttpServletRequest request = (HttpServletRequest) servletRequest;        final HttpServletResponse response = (HttpServletResponse) servletResponse;        if (!this.handlerInitialized.getAndSet(true)) {            HANDLER.init();        }        if (HANDLER.process(request, response)) {            filterChain.doFilter(servletRequest, servletResponse);        }    }    @Override    public void destroy() {    }    private static SingleSignOutHandlerImpl getSingleSignOutHandler() {        return HANDLER;    }}

10.添加 SingleSignOutHandlerImpl

  • 单点退出过滤器实现类
package com.ruoyi.framework.security.handle;import com.alibaba.fastjson2.JSON;import com.ruoyi.common.constant.CacheConstants;import com.ruoyi.common.constant.Constants;import com.ruoyi.common.constant.HttpStatus;import com.ruoyi.common.core.domain.AjaxResult;import com.ruoyi.common.core.domain.model.LoginUser;import com.ruoyi.common.core.redis.RedisCache;import com.ruoyi.common.utils.ServletUtils;import com.ruoyi.common.utils.StringUtils;import com.ruoyi.common.utils.spring.SpringUtils;import com.ruoyi.framework.manager.AsyncManager;import com.ruoyi.framework.manager.factory.AsyncFactory;import com.ruoyi.framework.web.service.TokenService;import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import org.jasig.cas.client.Protocol;import org.jasig.cas.client.configuration.ConfigurationKeys;import org.jasig.cas.client.session.HashMapBackedSessionMappingStorage;import org.jasig.cas.client.session.SessionMappingStorage;import org.jasig.cas.client.util.CommonUtils;import org.jasig.cas.client.util.XmlUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.config.YamlMapFactoryBean;import org.springframework.core.io.ClassPathResource;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import javax.xml.bind.DatatypeConverter;import java.nio.charset.StandardCharsets;import java.util.*;import java.util.zip.Inflater;/** * @author Wen先森 * @description 单点退出过滤器实现类 * @date 2024/8/1 21:00 */public final class SingleSignOutHandlerImpl {    private final static int DECOMPRESSION_FACTOR = 10;    private final Logger logger = LoggerFactory.getLogger(getClass());    private SessionMappingStorage sessionMappingStorage = new HashMapBackedSessionMappingStorage();    private String artifactParameterName = Protocol.CAS2.getArtifactParameterName();    private String logoutParameterName = ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue();    private String relayStateParameterName = ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue();    private String logoutCallbackPath;    private boolean artifactParameterOverPost = false;    private boolean eagerlyCreateSessions = true;    private List<String> safeParameters;    private final LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy() : new Servlet25LogoutStrategy();    public void setSessionMappingStorage(final SessionMappingStorage storage) {        this.sessionMappingStorage = storage;    }    public void setArtifactParameterOverPost(final boolean artifactParameterOverPost) {        this.artifactParameterOverPost = artifactParameterOverPost;    }    public SessionMappingStorage getSessionMappingStorage() {        return this.sessionMappingStorage;    }    public void setArtifactParameterName(final String name) {        this.artifactParameterName = name;    }    public void setLogoutParameterName(final String name) {        this.logoutParameterName = name;    }    public void setLogoutCallbackPath(final String logoutCallbackPath) {        this.logoutCallbackPath = logoutCallbackPath;    }    public void setRelayStateParameterName(final String name) {        this.relayStateParameterName = name;    }    public void setEagerlyCreateSessions(final boolean eagerlyCreateSessions) {        this.eagerlyCreateSessions = eagerlyCreateSessions;    }    public synchronized void init() {        if (this.safeParameters == null) {            CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null.");            CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null.");            CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannot be null.");            CommonUtils.assertNotNull(this.relayStateParameterName, "relayStateParameterName cannot be null.");            if (this.artifactParameterOverPost) {                this.safeParameters = Arrays.asList(this.logoutParameterName, this.artifactParameterName);            } else {                this.safeParameters = Collections.singletonList(this.logoutParameterName);            }        }    }    private boolean isTokenRequest(final HttpServletRequest request) {        return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters));    }    private boolean isLogoutRequest(final HttpServletRequest request) {        if ("POST".equalsIgnoreCase(request.getMethod())) {            return !isMultipartRequest(request)                    && pathEligibleForLogout(request)                    && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,                    this.safeParameters));        }        if ("GET".equalsIgnoreCase(request.getMethod())) {            return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters));        }        return false;    }    private boolean pathEligibleForLogout(final HttpServletRequest request) {        return logoutCallbackPath == null || logoutCallbackPath.equals(getPath(request));    }    private String getPath(final HttpServletRequest request) {        return request.getServletPath() + CommonUtils.nullToEmpty(request.getPathInfo());    }    public boolean process(final HttpServletRequest request, final HttpServletResponse response) {        if (isTokenRequest(request)) {            logger.trace("Received a token request");            recordSession(request);            return true;        }        if (isLogoutRequest(request)) {            logger.trace("Received a logout request");            destroySession(request, response);            return false;        }        logger.trace("Ignoring URI for logout: {}", request.getRequestURI());        return true;    }    private void recordSession(final HttpServletRequest request) {        final HttpSession session = request.getSession(this.eagerlyCreateSessions);        if (session == null) {            logger.debug("No session currently exists (and none created).  Cannot record session information for single sign out.");            return;        }        final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);        logger.debug("用户登录认证的ticket:"+token);        logger.debug("Recording session for token {}", token);        try {            this.sessionMappingStorage.removeBySessionById(session.getId());        } catch (final Exception ignored) {        }        sessionMappingStorage.addSessionById(token, session);    }    private String uncompressLogoutMessage(final String originalMessage) {        final byte[] binaryMessage = DatatypeConverter.parseBase64Binary(originalMessage);        Inflater decompresser = null;        try {            decompresser = new Inflater();            decompresser.setInput(binaryMessage);            final byte[] result = new byte[binaryMessage.length * DECOMPRESSION_FACTOR];            final int resultLength = decompresser.inflate(result);            return new String(result, 0, resultLength, StandardCharsets.UTF_8);        } catch (final Exception e) {            logger.error("Unable to decompress logout message", e);            throw new RuntimeException(e);        } finally {            if (decompresser != null) {                decompresser.end();            }        }    }    @SuppressWarnings("unchecked")    private void destroySession(final HttpServletRequest request, final HttpServletResponse response) {        String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);        if (CommonUtils.isBlank(logoutMessage)) {            logger.error("Could not locate logout message of the request from {}", this.logoutParameterName);            return;        }        if (!logoutMessage.contains("SessionIndex")) {            logoutMessage = uncompressLogoutMessage(logoutMessage);        }        logger.trace("Logout request: {}", logoutMessage);        final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");        logger.debug("用户退出系统的ticket:"+token);        // 字符串非空判断        if (CommonUtils.isNotBlank(token)) {            // 获取Spring的Bean实例            RedisCache redisCache = SpringUtils.getBean("redisCache");            TokenService tokenService = SpringUtils.getBean("tokenService");            // 获取Redis中jwt生成的token            String loginToken = redisCache.getCacheObject(CacheConstants.LOGIN_TICKET_KEY+token);            // 字符串非空判断            if (StringUtils.isNotEmpty(loginToken)) {                // 删除Redis中jwt生成的token                redisCache.deleteObject(CacheConstants.LOGIN_TICKET_KEY+token);                // 新建实例                YamlMapFactoryBean yamlMapFb = new YamlMapFactoryBean();                // 读取文件                yamlMapFb.setResources(new ClassPathResource("application.yml"));                // 获取配置                String secret = (String) ((Map<String, Object>) Objects.requireNonNull(yamlMapFb.getObject()).get("token")).get("secret");                try {                    // 解密jwt生成的token                    Claims claims = Jwts.parser()                            .setSigningKey(secret)                            .parseClaimsJws(loginToken)                            .getBody();                    // 解析对应的权限以及用户信息                    String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);                    // 获取Redis的key                    String userKey = CacheConstants.LOGIN_TOKEN_KEY + uuid;                    // 获取Redis中登录用户的信息                    LoginUser loginUser = redisCache.getCacheObject(userKey);                    // 对象非空判断                    if (StringUtils.isNotNull(loginUser)) {                        // 用户账号                        String userName = loginUser.getUsername();                        // 删除用户缓存记录                        tokenService.delLoginUser(loginUser.getToken());                        // 记录用户退出日志                        AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));                    }                    // 将字符串渲染到客户端                    ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));                } catch (Exception e) {                    e.printStackTrace();                }            }            final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);            if (session != null) {                final String sessionID = session.getId();                logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);                try {                    session.invalidate();                } catch (final IllegalStateException e) {                    logger.debug("Error invalidating session.", e);                }                this.logoutStrategy.logout(request);            }        }    }    private boolean isMultipartRequest(final HttpServletRequest request) {        return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart");    }    private static boolean isServlet30() {        try {            return HttpServletRequest.class.getMethod("logout") != null;        } catch (final NoSuchMethodException e) {            return false;        }    }    private interface LogoutStrategy {        void logout(HttpServletRequest request);    }    private static class Servlet25LogoutStrategy implements LogoutStrategy {        @Override        public void logout(final HttpServletRequest request) {        }    }    private class Servlet30LogoutStrategy implements LogoutStrategy {        @Override        public void logout(final HttpServletRequest request) {            try {                request.logout();            } catch (final ServletException e) {                logger.debug("Error performing request.logout.");            }        }    }}

前端

1.修改 settings.js

  /**   * 开启cas   */  casEnable: true,  /**   * 单点url   */  casUrl: 'http://host:port/sso/login',  /**   * 后台登录url   */  apploginUrl: process.env.VUE_APP_FRONT_END_HOST_AND_PORT + process.env.VUE_APP_BASE_API + '/cas/index',  /**   * 单点登录url   */  casloginUrl: 'http://host:port/sso/login?service='+ process.env.VUE_APP_FRONT_END_HOST_AND_PORT + process.env.VUE_APP_PUBLIC_PATH + '/index',  /**   * 单点登出url   */  caslogoutUrl: 'http://host:port/sso/logout?service=http://host:port/sso/login?service='+ process.env.VUE_APP_FRONT_END_HOST_AND_PORT + process.env.VUE_APP_PUBLIC_PATH + '/index',

2.修改 permission.js

  • 判断没有token时访问cas登录页面:
import router from './router'import store from './store'import { Message } from 'element-ui'import NProgress from 'nprogress'import 'nprogress/nprogress.css'import { getToken } from '@/utils/auth'import { isRelogin } from '@/utils/request'import defaultSettings from '@/settings'NProgress.configure({ showSpinner: false })const whiteList = ['/login', '/auth-redirect', '/bind', '/register']router.beforeEach((to, from, next) => {  NProgress.start()  debugger  alert('beforeEach getToken')  if (getToken()) {    debugger    alert('getToken in')    to.meta.title && store.dispatch('settings/setTitle', to.meta.title)    /* has token*/    if (to.path === '/login') {      next({ path: '/' })      NProgress.done()    } else {      if (store.getters.roles.length === 0) {        isRelogin.show = true        // 判断当前用户是否已拉取完user_info信息        store.dispatch('GetInfo').then(() => {          isRelogin.show = false          store.dispatch('GenerateRoutes').then(accessRoutes => {            // 根据roles权限生成可访问的路由表            router.addRoutes(accessRoutes) // 动态添加可访问路由表            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成          })        }).catch(err => {            store.dispatch('LogOut').then(() => {              Message.error(err)              next({ path: '/' })            })          })      } else {        next()      }    }  } else {    // 没有token    if (whiteList.indexOf(to.path) !== -1) {      // 在免登录白名单,直接进入      next()    } else {      if (!defaultSettings.casEnable) {        next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页      }      //开启cas      if (defaultSettings.casEnable) {        alert('defaultSettings.apploginUrl:' + defaultSettings.apploginUrl);        window.location.href = defaultSettings.apploginUrl // 否则全部重定向到登录页      }      NProgress.done()    }  }})router.afterEach(() => {  NProgress.done()})

3.修改 request.js、Navbar.vue

  • request.js
import axios from 'axios'import { Notification, MessageBox, Message, Loading } from 'element-ui'import store from '@/store'import { getToken, removeAllCookie } from '@/utils/auth'import errorCode from '@/utils/errorCode'import { tansParams, blobValidate } from "@/utils/ruoyi";import cache from '@/plugins/cache'import { saveAs } from 'file-saver'import defaultSettings from '@/settings'let downloadLoadingInstance;// 是否显示重新登录export let isRelogin = { show: false };axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'// 创建axios实例const service = axios.create({  // axios中请求配置有baseURL选项,表示请求URL公共部分  baseURL: process.env.VUE_APP_BASE_API,  // 超时  timeout: 10000})// request拦截器service.interceptors.request.use(config => {  // 是否需要设置 token  const isToken = (config.headers || {}).isToken === false  // 是否需要防止数据重复提交  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false  if (getToken() && !isToken) {    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改  }  // get请求映射params参数  if (config.method === 'get' && config.params) {    let url = config.url + '?' + tansParams(config.params);    url = url.slice(0, -1);    config.params = {};    config.url = url;  }  if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {    const requestObj = {      url: config.url,      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,      time: new Date().getTime()    }    const sessionObj = cache.session.getJSON('sessionObj')    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {      cache.session.setJSON('sessionObj', requestObj)    } else {      const s_url = sessionObj.url;                  // 请求地址      const s_data = sessionObj.data;                // 请求数据      const s_time = sessionObj.time;                // 请求时间      const interval = 1000;                         // 间隔时间(ms),小于此时间视为重复提交      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {        const message = '数据正在处理,请勿重复提交';        console.warn(`[${s_url}]: ` + message)        return Promise.reject(new Error(message))      } else {        cache.session.setJSON('sessionObj', requestObj)      }    }  }  return config}, error => {    console.log(error)    Promise.reject(error)})// 响应拦截器service.interceptors.response.use(res => {    // 单点重定向判断    if(res.status === 200 && res.request.responseURL.indexOf(defaultSettings.casUrl) > -1){      removeAllCookie()      alert('defaultSettings.casloginUrl:' + defaultSettings.casloginUrl);      window.location.href = defaultSettings.casloginUrl    }    // 未设置状态码则默认成功状态    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;        MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {          confirmButtonText: '重新登录',          cancelButtonText: '取消',          type: 'warning'        }      ).then(() => {        isRelogin.show = false;        store.dispatch('LogOut').then(() => {          location.href = '/index';        })      }).catch(() => {        isRelogin.show = false;      });    }      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')    } else if (code === 500) {      Message({        message: msg,        type: 'error'      })      return Promise.reject(new Error(msg))    } else if (code === 302) {      removeAllCookie()      alert('defaultSettings.casloginUrl:' + defaultSettings.casloginUrl);      window.location.href = defaultSettings.casloginUrl    }  else if (code !== 200) {      Notification.error({        title: msg      })      return Promise.reject('error')    } else {      return 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) + "异常";    }    Message({      message: message,      type: 'error',      duration: 5 * 1000    })    return Promise.reject(error)  })// 通用下载方法export function download(url, params, filename) {  downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })  return service.post(url, params, {    transformRequest: [(params) => { return tansParams(params) }],    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },    responseType: 'blob'  }).then(async (data) => {    const isLogin = await blobValidate(data);    if (isLogin) {      const blob = new Blob([data])      saveAs(blob, filename)    } else {      const resText = await data.text();      const rspObj = JSON.parse(resText);      const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']      Message.error(errMsg);    }    downloadLoadingInstance.close();  }).catch((r) => {    console.error(r)    Message.error('下载文件出现错误,请联系管理员!')    downloadLoadingInstance.close();  })}export default service
  • Navbar.vue
<template>  <div class="navbar">    <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>    <top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>    <div class="right-menu">      <template v-if="device!=='mobile'">        <search id="header-search" class="right-menu-item" />        <el-tooltip content="源码地址" effect="dark" placement="bottom">          <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />        </el-tooltip>        <el-tooltip content="文档地址" effect="dark" placement="bottom">          <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />        </el-tooltip>        <screenfull id="screenfull" class="right-menu-item hover-effect" />        <el-tooltip content="布局大小" effect="dark" placement="bottom">          <size-select id="size-select" class="right-menu-item hover-effect" />        </el-tooltip>      </template>      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">        <div class="avatar-wrapper">          <img :src="avatar" class="user-avatar">          <i class="el-icon-caret-bottom" />        </div>        <el-dropdown-menu slot="dropdown">          <router-link to="/user/profile">            <el-dropdown-item>个人中心</el-dropdown-item>          </router-link>          <el-dropdown-item @click.native="setting = true">            <span>布局设置</span>          </el-dropdown-item>          <el-dropdown-item divided @click.native="logout">            <span>退出登录</span>          </el-dropdown-item>        </el-dropdown-menu>      </el-dropdown>    </div>  </div></template><script>import { mapGetters } from 'vuex'import Breadcrumb from '@/components/Breadcrumb'import TopNav from '@/components/TopNav'import Hamburger from '@/components/Hamburger'import Screenfull from '@/components/Screenfull'import SizeSelect from '@/components/SizeSelect'import Search from '@/components/HeaderSearch'import RuoYiGit from '@/components/RuoYi/Git'import RuoYiDoc from '@/components/RuoYi/Doc'export default {  components: {    Breadcrumb,    TopNav,    Hamburger,    Screenfull,    SizeSelect,    Search,    RuoYiGit,    RuoYiDoc  },  computed: {    ...mapGetters([      'sidebar',      'avatar',      'device'    ]),    setting: {      get() {        return this.$store.state.settings.showSettings      },      set(val) {        this.$store.dispatch('settings/changeSetting', {          key: 'showSettings',          value: val        })      }    },    topNav: {      get() {        return this.$store.state.settings.topNav      }    }  },  methods: {    toggleSideBar() {      this.$store.dispatch('app/toggleSideBar')    },    async logout() {      this.$confirm('确定注销并退出系统吗?', '提示', {        confirmButtonText: '确定',        cancelButtonText: '取消',        type: 'warning'      }).then(() => {        this.$store.dispatch('LogOut').then(() => {          // location.href = '/index';        })      }).catch(() => {});    }  }}</script><style lang="scss" scoped>.navbar {  height: 50px;  overflow: hidden;  position: relative;  background: #fff;  box-shadow: 0 1px 4px rgba(0,21,41,.08);  .hamburger-container {    line-height: 46px;    height: 100%;    float: left;    cursor: pointer;    transition: background .3s;    -webkit-tap-highlight-color:transparent;    &:hover {      background: rgba(0, 0, 0, .025)    }  }  .breadcrumb-container {    float: left;  }  .topmenu-container {    position: absolute;    left: 50px;  }  .errLog-container {    display: inline-block;    vertical-align: top;  }  .right-menu {    float: right;    height: 100%;    line-height: 50px;    &:focus {      outline: none;    }    .right-menu-item {      display: inline-block;      padding: 0 8px;      height: 100%;      font-size: 18px;      color: #5a5e66;      vertical-align: text-bottom;      &.hover-effect {        cursor: pointer;        transition: background .3s;        &:hover {          background: rgba(0, 0, 0, .025)        }      }    }    .avatar-container {      margin-right: 30px;      .avatar-wrapper {        margin-top: 5px;        position: relative;        .user-avatar {          cursor: pointer;          width: 40px;          height: 40px;          border-radius: 10px;        }        .el-icon-caret-bottom {          cursor: pointer;          position: absolute;          right: -20px;          top: 25px;          font-size: 12px;        }      }    }  }}</style>

4.修改 user.js

  • 登出后跳转到cas登出页面:
import defaultSettings from '@/settings'import { login, logout, getInfo } from '@/api/login'import { getToken, setToken, removeToken } from '@/utils/auth'const user = {  state: {    token: getToken(),    name: '',    avatar: '',    roles: [],    permissions: []  },  mutations: {    SET_TOKEN: (state, token) => {      state.token = token    },    SET_NAME: (state, name) => {      state.name = name    },    SET_AVATAR: (state, avatar) => {      state.avatar = avatar    },    SET_ROLES: (state, roles) => {      state.roles = roles    },    SET_PERMISSIONS: (state, permissions) => {      state.permissions = permissions    }  },  actions: {    // 登录    Login({ commit }, userInfo) {      alert('Login');      const username = userInfo.username.trim()      const password = userInfo.password      const code = userInfo.code      const uuid = userInfo.uuid      return new Promise((resolve, reject) => {        login(username, password, code, uuid).then(res => {          setToken(res.token)          commit('SET_TOKEN', res.token)          resolve()        }).catch(error => {          reject(error)        })      })    },    // 获取用户信息    GetInfo({ commit, state }) {      return new Promise((resolve, reject) => {        getInfo().then(res => {          const user = res.user          const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;          if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组            commit('SET_ROLES', res.roles)            commit('SET_PERMISSIONS', res.permissions)          } else {            commit('SET_ROLES', ['ROLE_DEFAULT'])          }          commit('SET_NAME', user.userName)          commit('SET_AVATAR', avatar)          resolve(res)        }).catch(error => {          reject(error)        })      })    },    // 退出系统    LogOut({ commit, state }) {      return new Promise((resolve, reject) => {        logout(state.token).then(() => {          commit('SET_TOKEN', '')          commit('SET_ROLES', [])          commit('SET_PERMISSIONS', [])          removeToken()          resolve()          location.href = defaultSettings.caslogoutUrl        }).catch(error => {          reject(error)        })      })    },    // 前端 登出    FedLogOut({ commit }) {      return new Promise(resolve => {        commit('SET_TOKEN', '')        removeToken()        resolve()      })    }  }}export default user

5.修改 auth.js

import Cookies from 'js-cookie'const TokenKey = 'Admin-Token'const JsessionId = 'JSESSIONID'export function getToken() {  return Cookies.get(TokenKey)}export function setToken(token) {  return Cookies.set(TokenKey, token)}export function removeToken() {  debugger  alert('removeToken')  return Cookies.remove(TokenKey)}export function removeJsessionId() {  return Cookies.remove(JsessionId)}export function removeAllCookie() {  removeToken()  removeJsessionId()}

6.添加环境变量

# 智能招聘管理系统 - 写在 开发环境中 为了 不报错VUE_APP_PUBLIC_PATH = '/xxx_vue'#前端域名+端口VUE_APP_FRONT_END_HOST_AND_PORT = 'http://host:port'#后端域名+端口VUE_APP_BACK_END_HOST_AND_PORT = 'http://host:port'

也许您对下面的内容还感兴趣: