后端
1.添加CAS依赖
- 在common模块pom添加spring-security-cas依赖:
<dependency><groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId></dependency>
2.修改配置文件
- 在admin模块下的application.yml配置文件中添加:
cas: server: host: url: http://host:port/sso ticket_validator_url: http://host:port/sso login_url: ${cas.server.host.url}/login logout_url: ${cas.server.host.url}/logout?service=${cas.server.host.url}/login?service=${app.server.host.url}app: name: Xxx 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
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;@Datapublic class LoginUser implements UserDetails{ private static final long serialVersionUID = 1L; private Long userId; private Long deptId; private String token; private Long loginTime; private Long expireTime; private String ipaddr; private String loginLocation; private String browser; private String os; private Set<String> permissions; private SysUser user; 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; } @JSONField(serialize = false) @Override public boolean isAccountNonLocked() { return true; } @JSONField(serialize = false) @Override public boolean isCredentialsNonExpired() { return true; } @JSONField(serialize = false) @Override public boolean isEnabled() { return true; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return new HashSet<>(); }}
4.修改 Constants.java
public static final String CAS_TOKEN = "cas_token"; 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;@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
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;@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
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;@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; @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()); redisCache.setCacheObject(CacheConstants.LOGIN_TICKET_KEY+authentication.getCredentials().toString(), token, expireTime, TimeUnit.MINUTES); 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()); }}
8.修改 SecurityConfig
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;@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; 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; } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .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); httpSecurity.addFilter(casAuthenticationFilter()) .addFilterBefore(authenticationTokenFilter, CasAuthenticationFilter.class) .addFilterBefore(singleSignOutTokenFilter(), CasAuthenticationFilter.class).exceptionHandling() .authenticationEntryPoint(casAuthenticationEntryPoint()); httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); httpSecurity.headers().cacheControl(); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { if (casProperties.isCasEnable()) { super.configure(auth); auth.authenticationProvider(casAuthenticationProvider()); } else { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } } @Bean public CasAuthenticationEntryPoint casAuthenticationEntryPoint() { CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint(); casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl()); casAuthenticationEntryPoint.setServiceProperties(serviceProperties()); return casAuthenticationEntryPoint; } @Bean public ServiceProperties serviceProperties() { ServiceProperties serviceProperties = new ServiceProperties(); serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl()); serviceProperties.setAuthenticateAllArtifacts(true); return serviceProperties; } @Bean public CasAuthenticationFilter casAuthenticationFilter() throws Exception { CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter(); casAuthenticationFilter.setAuthenticationManager(authenticationManager()); casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl()); casAuthenticationFilter.setAuthenticationSuccessHandler(casAuthenticationSuccessHandler); return casAuthenticationFilter; } @Bean public CasAuthenticationProvider casAuthenticationProvider() { CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider(); casAuthenticationProvider.setAuthenticationUserDetailsService(userDetailsServiceCasImpl); casAuthenticationProvider.setServiceProperties(serviceProperties()); casAuthenticationProvider.setTicketValidator(cas30ServiceTicketValidator()); casAuthenticationProvider.setKey("casAuthenticationProviderKey"); return casAuthenticationProvider; } @Bean public Cas30ServiceTicketValidator cas30ServiceTicketValidator() { return new Cas30ServiceTicketValidator(casProperties.getCasServerTicketValidatorUrl()); } @Bean public SingleSignOutTokenFilter singleSignOutTokenFilter() { SingleSignOutTokenFilter singleSignOutTokenFilter = new SingleSignOutTokenFilter(); singleSignOutTokenFilter.setIgnoreInitConfiguration(true); return singleSignOutTokenFilter; } @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; }}
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;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;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)) { RedisCache redisCache = SpringUtils.getBean("redisCache"); TokenService tokenService = SpringUtils.getBean("tokenService"); String loginToken = redisCache.getCacheObject(CacheConstants.LOGIN_TICKET_KEY+token); if (StringUtils.isNotEmpty(loginToken)) { 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 { Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(loginToken) .getBody(); String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = CacheConstants.LOGIN_TOKEN_KEY + uuid; 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
casEnable: true, casUrl: 'http://host:port/sso/login', apploginUrl: process.env.VUE_APP_FRONT_END_HOST_AND_PORT + process.env.VUE_APP_BASE_API + '/cas/index', casloginUrl: 'http://host:port/sso/login?service='+ process.env.VUE_APP_FRONT_END_HOST_AND_PORT + process.env.VUE_APP_PUBLIC_PATH + '/index', 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
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) if (to.path === '/login') { next({ path: '/' }) NProgress.done() } else { if (store.getters.roles.length === 0) { isRelogin.show = true store.dispatch('GetInfo').then(() => { isRelogin.show = false store.dispatch('GenerateRoutes').then(accessRoutes => { router.addRoutes(accessRoutes) next({ ...to, replace: true }) }) }).catch(err => { store.dispatch('LogOut').then(() => { Message.error(err) next({ path: '/' }) }) }) } else { next() } } } else { if (whiteList.indexOf(to.path) !== -1) { next() } else { if (!defaultSettings.casEnable) { next(`/login?redirect=${to.fullPath}`) } if (defaultSettings.casEnable) { alert('defaultSettings.apploginUrl:' + defaultSettings.apploginUrl); window.location.href = defaultSettings.apploginUrl } NProgress.done() } }})router.afterEach(() => { NProgress.done()})
3.修改 request.js、Navbar.vue
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'const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000})service.interceptors.request.use(config => { const isToken = (config.headers || {}).isToken === false const isRepeatSubmit = (config.headers || {}).repeatSubmit === false if (getToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getToken() } 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; 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
<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(() => { }) }).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
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) { 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'