后端
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;
@Data
public 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
@Component
public 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;
@Service
public 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;
@Service
public 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'