【Cas客户端接入】Springboot+Spring security接入cas,实现单点登录SSO,单点登出SLO,亲测可用

前言

cas服务端用的是cas overlay5.3.16,ruoyi用的是ruoyi-vue4.6.0,亲测可用,单点登录单点登出都可以用,如果不达预期,可能是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-xxx.yml配置文件中添加:

#CAS  
cas:  
    server:  
        host:  
            #CAS服务地址  
            url: https://cas.yyg.cn:8443/cas  
            #CAS服务登录地址  
            login_url: ${cas.server.host.url}/login  
            #CAS服务登出地址  
            logout_url: ${cas.server.host.url}/logout?service=${app.server.host.url}  
# 应用访问地址  
app:  
    #开启cas  
    casEnable: false  
    server:  
        host:  
            url: http://192.168.235.37:8097/dev-api  
    #应用登录地址  
    login_url: /  
    #应用登出地址  
    logout_url: /logout  
    #前端登录地址  
    web_url: http://192.168.235.37:8097  
    err_url: http://192.168.235.37:8096/portal

3、修改LoginUser.java

由于CAS认证需要authorities属性,此属性不能为空,此处为了方便直接new HashSet():

    @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、修改CacheConstants.java

添加CAS认证成功标识:

	/**
     * 存入redis里的键
     */
    public static final String LOGIN_TICKET_KEY = "login_ticket:";

6、添加 CasProperties.java

读取cas配置信息:

package com.ruoyi.framework.config.properties;  
import lombok.Data;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.stereotype.Component;  
  
/**  
 * CAS的配置参数  
 */  
@Component  
@Data  
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;  
  
    @Value("${app.web_ip}")  
    private String webIp;  
  
}

7、添加CasUserDetailsService.java

在framework模块下添加:

  
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.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接口  
 *  
 * @author HuXiao */@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)) {  
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");  
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {  
            log.info("登录用户:{} 已被删除.", username);  
            throw new UsernameNotFoundException("对不起,您的账号:" + username + " 已被删除");  
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {  
            log.info("登录用户:{} 已被停用.", username);  
            throw new UsernameNotFoundException("对不起,您的账号:" + username + " 已停用");  
        }  
  
        return createLoginUser(user);  
    }  
  
    public UserDetails createLoginUser(SysUser user) {  
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));  
    }  
}

8、添加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.apache.commons.logging.Log;  
import org.apache.commons.logging.LogFactory;  
import org.springframework.beans.factory.annotation.Autowired;  
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 www78 */@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;  
  
    @Autowired  
    private RedisCache redisCache;  
  
    /**  
     * 令牌有效期(默认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);  
        // 往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);  
        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());  
    }  
  
}

9、添加CustomAuthenticationFailureHandler.java

在framework模块下添加认证失败处理类:

package com.ruoyi.framework.security.handle;  
  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.security.core.AuthenticationException;  
import org.springframework.security.web.authentication.AuthenticationFailureHandler;  
import org.springframework.stereotype.Component;  
  
import javax.servlet.ServletException;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
  
@Component  
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {  
  
    @Value("${app.err_url}")  
    private String errUrl;  
    @Override  
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {  
        if (exception instanceof org.springframework.security.core.userdetails.UsernameNotFoundException) {  
            response.sendRedirect(errUrl);  
        }  
    }  
}

10、添加SingleSignOutTokenFilter.java

添加单点登出过滤器:

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;  
  
/**  
 * @description 单点退出过滤器  
 */  
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;  
    }  
}

11、添加SingleSignOutHandlerImpl.java

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;  
  
/**  
 * @description 单点退出过滤器实现类  
 */  
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.");  
            }  
        }  
    }  
}

12、修改SecurityConfig

添加cas的处理逻辑:

package com.ruoyi.framework.config;  
  
import com.ruoyi.framework.config.properties.CasProperties;  
import com.ruoyi.framework.config.properties.PermitAllUrlProperties;  
import com.ruoyi.framework.security.filter.SingleSignOutTokenFilter;  
import com.ruoyi.framework.security.handle.CasAuthenticationSuccessHandler;  
import com.ruoyi.framework.security.handle.CustomAuthenticationFailureHandler;  
import com.ruoyi.framework.security.storage.CustomSessionMappingStorage;  
import com.ruoyi.framework.web.service.CasUserDetailsService;  
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.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.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.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 CasProperties casProperties;  
  
    @Autowired  
    private CasUserDetailsService customUserDetailsService;  
  
    @Autowired  
    private CasAuthenticationSuccessHandler casAuthenticationSuccessHandler;  
  
    @Autowired  
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;  
  
    /**  
     * 自定义用户认证逻辑  
     */  
    @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;  
  
    @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", "/publicKey").anonymous()  
                    // 对于 铺底接口 单条应用查询 单挑用户查询 绑定/解绑接口 允许匿名访问  
                    .antMatchers(  
                            HttpMethod.GET,  
                            "/",  
                            "/*.html",  
                            "/**/*.html",  
                            "/**/*.css",  
                            "/**/*.js",  
                            "/profile/**"  
                    ).permitAll()  
                    .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().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()  
                    // 对于登录login 注册register 验证码captchaImage 允许匿名访问  
                    .antMatchers("/login", "/register", "/captchaImage", "/publicKey").anonymous()  
                    // 对于 铺底接口 单条应用查询 单挑用户查询 绑定/解绑接口 允许匿名访问   
                    .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(), LogoutFilter.class)  
                    .addFilterBefore(singleSignOutTokenFilter(), CasAuthenticationFilter.class).exceptionHandling()  
                    //认证失败  
                    .authenticationEntryPoint(casAuthenticationEntryPoint());  
            // 添加CORS filter  
            httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);  
            httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);  
  
        }  
    }  
  
    /**  
     * 强散列哈希加密实现  
     */  
    @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 CasAuthenticationEntryPoint();  
        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);  
        casAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);  
        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());  
    }  
  
    /**  
     * 单点登出过滤器  
     */  
//    @Bean  
//    public SingleSignOutFilter singleSignOutFilter() {  
//        SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();  
//        singleSignOutFilter.setLogoutCallbackPath("/login");  
//        singleSignOutFilter.setSessionMappingStorage(customSessionMappingStorage);  
//        singleSignOutFilter.setIgnoreInitConfiguration(true);  
//        return singleSignOutFilter;  
//    }  
    @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;  
    }  
  
    /**  
     * 请求单点退出过滤器  
     */  
    @Bean  
    public LogoutFilter casLogoutFilter() {  
        LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(),  
                new SecurityContextLogoutHandler());  
        logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());  
        return logoutFilter;  
    }  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值