参考
[Spring Boot 2] 如何移除 jsessionid - TimmyBeef's Blog
后端 开发
1. 添加cas依赖
- 在common模块pom添加spring-security-cas依赖:
<!-- spring security cas-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
2、修改配置文件
- 在admin模块下的application.yml配置文件中添加:
# CAS 相关配置 start
# CAS服务器配置
cas:
server:
host:
#CAS服务地址
url: http://host:port/sso
#CAS ticket 验证 服务地址
ticket_validator_url: http://host:port/sso
#CAS服务登录地址
login_url: ${cas.server.host.url}/login
#CAS服务登出地址
logout_url: ${cas.server.host.url}/logout?service=${cas.server.host.url}/login?service=${app.server.host.url}
#应用访问地址
app:
#项目名称
name: Xxx
#是否开启CAS
casEnable: true
server:
host:
#项目地址
url: http://host:${server.port}
#应用登录地址
login_url: /
#应用登出地址
logout_url: /logout
#前端回调地址
callback_url: /cas/index
#前端登录地址
web_url: http://host:port/xxx_vue
# CAS 相关配置 end
3、修改LoginUser.java
- 由于CAS认证需要authorities属性,此属性不能为空,此处为了方便直接new HashSet():
package com.ruoyi.common.core.domain.model;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.alibaba.fastjson2.annotation.JSONField;
import com.ruoyi.common.core.domain.entity.SysUser;
/**
* 登录用户身份权限
*
* @author ruoyi
*/
@Data
public class LoginUser implements UserDetails
{
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
// CAS用户信息
private Map<String, Object> attributes;
public LoginUser()
{
}
public LoginUser(SysUser user, Set<String> permissions)
{
this.user = user;
this.permissions = permissions;
}
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
{
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
}
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions, Map<String, Object> attributes) {
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
this.attributes = attributes;
}
@JSONField(serialize = false)
@Override
public String getPassword()
{
return user.getPassword();
}
@Override
public String getUsername()
{
return user.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired()
{
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked()
{
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled()
{
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new HashSet<>();
}
}
4、修改 Constants.java
- 添加CAS认证成功标识:
// CAS登录成功后的后台标识
public static final String CAS_TOKEN = "cas_token";
// CAS登录成功后的前台Cookie的Key
public static final String WEB_TOKEN_KEY = "Admin-Token";
5、添加 CasProperties.java
- 读取cas配置信息:
package com.ruoyi.framework.config.properties;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author MikuHoney
* @description CAS的配置参数
* @date 2022/7/26 16:58
*/
@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
- 在framework模块下添加:
package com.ruoyi.framework.web.service.impl;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* @author MikuHoney
* @description 用于加载用户信息,实现UserDetailsService接口,或者实现AuthenticationUserDetailsService接口。
* @date 2022/7/26 17:02
*/
@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
- 在framework模块下添加:
package com.ruoyi.framework.security.handle;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.framework.config.properties.CasProperties;
import com.ruoyi.framework.web.service.TokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* @author MikuHoney
* @description CAS认证中心
* @date 2022/7/26 17:05
*/
@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;
// 令牌有效期(默认30分钟)
@Value("${token.expireTime}")
private int expireTime;
public CasAuthenticationSuccessHandler(RedisCache redisCache, TokenService tokenService, CasProperties casProperties) {
this.redisCache = redisCache;
this.tokenService = tokenService;
this.casProperties = casProperties;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
LoginUser userDetails = (LoginUser) authentication.getPrincipal();
String token = tokenService.createToken(userDetails);
// 打印日志
log.debug("CAS认证中心的ticket:"+authentication.getCredentials().toString());
// 往Redis中设置token
redisCache.setCacheObject(CacheConstants.LOGIN_TICKET_KEY+authentication.getCredentials().toString(), token, expireTime, TimeUnit.MINUTES);
// 往Cookie中设置token
Cookie casCookie = new Cookie(Constants.WEB_TOKEN_KEY, token);
casCookie.setMaxAge(expireTime * 60);
// TODO: 设置 cookie path 为 根目录(解决前端 cookie 丢失问题), 不确定 是否合理
casCookie.setPath("/");
response.addCookie(casCookie);
// 设置后端认证成功标识
HttpSession httpSession = request.getSession();
httpSession.setAttribute(Constants.CAS_TOKEN, token);
httpSession.setMaxInactiveInterval(expireTime * 60);
// 登录成功后跳转到前端登录页面
getRedirectStrategy().sendRedirect(request, response, casProperties.getWebUrl());
}
}
8、修改 SecurityConfig
- 添加cas的处理逻辑:
package com.ruoyi.framework.config;
import com.ruoyi.framework.config.properties.CasProperties;
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ruoyi.framework.security.filter.SingleSignOutTokenFilter;
import com.ruoyi.framework.security.handle.CasAuthenticationSuccessHandler;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
import com.ruoyi.framework.web.service.impl.UserDetailsServiceCasImpl;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.filter.CorsFilter;
/**
* spring security配置
*
* @author ruoyi
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CasProperties casProperties;
private final UserDetailsServiceCasImpl userDetailsServiceCasImpl;
private final CasAuthenticationSuccessHandler casAuthenticationSuccessHandler;
// 跨域过滤器
private final CorsFilter corsFilter;
// 自定义用户认证逻辑
private final UserDetailsService userDetailsService;
// 自定义用户退出处理类
private final LogoutSuccessHandlerImpl logoutSuccessHandler;
// 自定义用户token认证过滤器
private final JwtAuthenticationTokenFilter authenticationTokenFilter;
public SecurityConfig(CasProperties casProperties, UserDetailsServiceCasImpl userDetailsServiceCasImpl, CasAuthenticationSuccessHandler casAuthenticationSuccessHandler, CorsFilter corsFilter, UserDetailsService userDetailsService, LogoutSuccessHandlerImpl logoutSuccessHandler, JwtAuthenticationTokenFilter authenticationTokenFilter) {
this.casProperties = casProperties;
this.userDetailsServiceCasImpl = userDetailsServiceCasImpl;
this.casAuthenticationSuccessHandler = casAuthenticationSuccessHandler;
this.corsFilter = corsFilter;
this.userDetailsService = userDetailsService;
this.logoutSuccessHandler = logoutSuccessHandler;
this.authenticationTokenFilter = authenticationTokenFilter;
}
/**
* 解决无法直接注入AuthenticationManager
*
* @return AuthenticationManager
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非RememberMe下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过RememberMe登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login,注册register,验证码captchaImage可以任意访问
// .antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源可以任意访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
.antMatchers("/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 单点登录登出
httpSecurity.logout().permitAll().logoutSuccessHandler(logoutSuccessHandler);
// 添加CAS filter
httpSecurity.addFilter(casAuthenticationFilter())
// 请求单点退出过滤器
// .addFilterBefore(casLogoutFilter(), LogoutFilter.class)
// token认证过滤器
.addFilterBefore(authenticationTokenFilter, CasAuthenticationFilter.class)
// 单点登出过滤器
.addFilterBefore(singleSignOutTokenFilter(), CasAuthenticationFilter.class).exceptionHandling()
// 认证失败
.authenticationEntryPoint(casAuthenticationEntryPoint());
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
// 禁用缓存
httpSecurity.headers().cacheControl();
}
/**
* 强散列哈希加密实现注册
*
* @return 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*
* @param auth 身份认证
* @throws Exception 异常抛出
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
if (casProperties.isCasEnable()) {
super.configure(auth);
auth.authenticationProvider(casAuthenticationProvider());
} else {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
/**
* CAS认证的入口注册
*
* @return CAS认证的入口
*/
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
/**
* 指定service相关信息注册
*
* @return 指定service相关信息
*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/**
* CAS认证过滤器注册
*
* @return CAS认证过滤器
* @throws Exception 异常抛出
*/
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
casAuthenticationFilter.setAuthenticationSuccessHandler(casAuthenticationSuccessHandler);
return casAuthenticationFilter;
}
/**
* CAS认证Provider注册
*
* @return CAS认证Provider
*/
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(userDetailsServiceCasImpl);
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas30ServiceTicketValidator());
casAuthenticationProvider.setKey("casAuthenticationProviderKey");
return casAuthenticationProvider;
}
/**
* CAS服务票据验证器注册
*
* @return CAS服务票据验证器
*/
@Bean
public Cas30ServiceTicketValidator cas30ServiceTicketValidator() {
return new Cas30ServiceTicketValidator(casProperties.getCasServerTicketValidatorUrl());
}
/**
* 单点登出过滤器注册
*
* @return 单点登出过滤器
*/
@Bean
public SingleSignOutTokenFilter singleSignOutTokenFilter() {
SingleSignOutTokenFilter singleSignOutTokenFilter = new SingleSignOutTokenFilter();
singleSignOutTokenFilter.setIgnoreInitConfiguration(true);
return singleSignOutTokenFilter;
}
/**
* 单点登出监听器注册
*
* @return 单点登出监听器
*/
@Bean
public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListenerBean() {
ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> listenerRegistrationBean = new ServletListenerRegistrationBean<>();
listenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
listenerRegistrationBean.setEnabled(true);
listenerRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return listenerRegistrationBean;
}
/**
* 请求单点退出过滤器注册
*
* @return 单点退出过滤器
*/
@Bean
public LogoutFilter casLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
return logoutFilter;
}
}
9. 添加 SingleSignOutTokenFilter
- 单点退出过滤器
package com.ruoyi.framework.security.filter;
import com.ruoyi.framework.security.handle.SingleSignOutHandlerImpl;
import org.jasig.cas.client.configuration.ConfigurationKeys;
import org.jasig.cas.client.session.SessionMappingStorage;
import org.jasig.cas.client.util.AbstractConfigurationFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author MikuHoney
* @description 单点退出过滤器
* @date 2022/8/1 20:59
*/
public final class SingleSignOutTokenFilter extends AbstractConfigurationFilter {
private static final SingleSignOutHandlerImpl HANDLER = new SingleSignOutHandlerImpl();
private final AtomicBoolean handlerInitialized = new AtomicBoolean(false);
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
if (!isIgnoreInitConfiguration()) {
setArtifactParameterName(getString(ConfigurationKeys.ARTIFACT_PARAMETER_NAME));
setLogoutParameterName(getString(ConfigurationKeys.LOGOUT_PARAMETER_NAME));
setRelayStateParameterName(getString(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME));
setLogoutCallbackPath(getString(ConfigurationKeys.LOGOUT_CALLBACK_PATH));
HANDLER.setArtifactParameterOverPost(getBoolean(ConfigurationKeys.ARTIFACT_PARAMETER_OVER_POST));
HANDLER.setEagerlyCreateSessions(getBoolean(ConfigurationKeys.EAGERLY_CREATE_SESSIONS));
}
HANDLER.init();
handlerInitialized.set(true);
}
public void setArtifactParameterName(final String name) {
HANDLER.setArtifactParameterName(name);
}
public void setLogoutParameterName(final String name) {
HANDLER.setLogoutParameterName(name);
}
public void setRelayStateParameterName(final String name) {
HANDLER.setRelayStateParameterName(name);
}
public void setLogoutCallbackPath(final String logoutCallbackPath) {
HANDLER.setLogoutCallbackPath(logoutCallbackPath);
}
public void setSessionMappingStorage(final SessionMappingStorage storage) {
HANDLER.setSessionMappingStorage(storage);
}
@Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
if (!this.handlerInitialized.getAndSet(true)) {
HANDLER.init();
}
if (HANDLER.process(request, response)) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
}
private static SingleSignOutHandlerImpl getSingleSignOutHandler() {
return HANDLER;
}
}
10. 添加 SingleSignOutHandlerImpl
- 单点退出过滤器实现类
package com.ruoyi.framework.security.handle;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.web.service.TokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.jasig.cas.client.Protocol;
import org.jasig.cas.client.configuration.ConfigurationKeys;
import org.jasig.cas.client.session.HashMapBackedSessionMappingStorage;
import org.jasig.cas.client.session.SessionMappingStorage;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.XmlUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.YamlMapFactoryBean;
import org.springframework.core.io.ClassPathResource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.Inflater;
/**
* @author MikuHoney
* @description 单点退出过滤器实现类
* @date 2022/8/1 21:00
*/
public final class SingleSignOutHandlerImpl {
private final static int DECOMPRESSION_FACTOR = 10;
private final Logger logger = LoggerFactory.getLogger(getClass());
private SessionMappingStorage sessionMappingStorage = new HashMapBackedSessionMappingStorage();
private String artifactParameterName = Protocol.CAS2.getArtifactParameterName();
private String logoutParameterName = ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue();
private String relayStateParameterName = ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue();
private String logoutCallbackPath;
private boolean artifactParameterOverPost = false;
private boolean eagerlyCreateSessions = true;
private List<String> safeParameters;
private final LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy() : new Servlet25LogoutStrategy();
public void setSessionMappingStorage(final SessionMappingStorage storage) {
this.sessionMappingStorage = storage;
}
public void setArtifactParameterOverPost(final boolean artifactParameterOverPost) {
this.artifactParameterOverPost = artifactParameterOverPost;
}
public SessionMappingStorage getSessionMappingStorage() {
return this.sessionMappingStorage;
}
public void setArtifactParameterName(final String name) {
this.artifactParameterName = name;
}
public void setLogoutParameterName(final String name) {
this.logoutParameterName = name;
}
public void setLogoutCallbackPath(final String logoutCallbackPath) {
this.logoutCallbackPath = logoutCallbackPath;
}
public void setRelayStateParameterName(final String name) {
this.relayStateParameterName = name;
}
public void setEagerlyCreateSessions(final boolean eagerlyCreateSessions) {
this.eagerlyCreateSessions = eagerlyCreateSessions;
}
public synchronized void init() {
if (this.safeParameters == null) {
CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null.");
CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null.");
CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannot be null.");
CommonUtils.assertNotNull(this.relayStateParameterName, "relayStateParameterName cannot be null.");
if (this.artifactParameterOverPost) {
this.safeParameters = Arrays.asList(this.logoutParameterName, this.artifactParameterName);
} else {
this.safeParameters = Collections.singletonList(this.logoutParameterName);
}
}
}
private boolean isTokenRequest(final HttpServletRequest request) {
return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters));
}
private boolean isLogoutRequest(final HttpServletRequest request) {
if ("POST".equalsIgnoreCase(request.getMethod())) {
return !isMultipartRequest(request)
&& pathEligibleForLogout(request)
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
this.safeParameters));
}
if ("GET".equalsIgnoreCase(request.getMethod())) {
return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters));
}
return false;
}
private boolean pathEligibleForLogout(final HttpServletRequest request) {
return logoutCallbackPath == null || logoutCallbackPath.equals(getPath(request));
}
private String getPath(final HttpServletRequest request) {
return request.getServletPath() + CommonUtils.nullToEmpty(request.getPathInfo());
}
public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
if (isTokenRequest(request)) {
logger.trace("Received a token request");
recordSession(request);
return true;
}
if (isLogoutRequest(request)) {
logger.trace("Received a logout request");
destroySession(request, response);
return false;
}
logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
return true;
}
private void recordSession(final HttpServletRequest request) {
final HttpSession session = request.getSession(this.eagerlyCreateSessions);
if (session == null) {
logger.debug("No session currently exists (and none created). Cannot record session information for single sign out.");
return;
}
final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);
logger.debug("用户登录认证的ticket:"+token);
logger.debug("Recording session for token {}", token);
try {
this.sessionMappingStorage.removeBySessionById(session.getId());
} catch (final Exception ignored) {
}
sessionMappingStorage.addSessionById(token, session);
}
private String uncompressLogoutMessage(final String originalMessage) {
final byte[] binaryMessage = DatatypeConverter.parseBase64Binary(originalMessage);
Inflater decompresser = null;
try {
decompresser = new Inflater();
decompresser.setInput(binaryMessage);
final byte[] result = new byte[binaryMessage.length * DECOMPRESSION_FACTOR];
final int resultLength = decompresser.inflate(result);
return new String(result, 0, resultLength, StandardCharsets.UTF_8);
} catch (final Exception e) {
logger.error("Unable to decompress logout message", e);
throw new RuntimeException(e);
} finally {
if (decompresser != null) {
decompresser.end();
}
}
}
@SuppressWarnings("unchecked")
private void destroySession(final HttpServletRequest request, final HttpServletResponse response) {
String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
if (CommonUtils.isBlank(logoutMessage)) {
logger.error("Could not locate logout message of the request from {}", this.logoutParameterName);
return;
}
if (!logoutMessage.contains("SessionIndex")) {
logoutMessage = uncompressLogoutMessage(logoutMessage);
}
logger.trace("Logout request: {}", logoutMessage);
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
logger.debug("用户退出系统的ticket:"+token);
// 字符串非空判断
if (CommonUtils.isNotBlank(token)) {
// 获取Spring的Bean实例
RedisCache redisCache = SpringUtils.getBean("redisCache");
TokenService tokenService = SpringUtils.getBean("tokenService");
// 获取Redis中jwt生成的token
String loginToken = redisCache.getCacheObject(CacheConstants.LOGIN_TICKET_KEY+token);
// 字符串非空判断
if (StringUtils.isNotEmpty(loginToken)) {
// 删除Redis中jwt生成的token
redisCache.deleteObject(CacheConstants.LOGIN_TICKET_KEY+token);
// 新建实例
YamlMapFactoryBean yamlMapFb = new YamlMapFactoryBean();
// 读取文件
yamlMapFb.setResources(new ClassPathResource("application.yml"));
// 获取配置
String secret = (String) ((Map<String, Object>) Objects.requireNonNull(yamlMapFb.getObject()).get("token")).get("secret");
try {
// 解密jwt生成的token
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(loginToken)
.getBody();
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
// 获取Redis的key
String userKey = CacheConstants.LOGIN_TOKEN_KEY + uuid;
// 获取Redis中登录用户的信息
LoginUser loginUser = redisCache.getCacheObject(userKey);
// 对象非空判断
if (StringUtils.isNotNull(loginUser)) {
// 用户账号
String userName = loginUser.getUsername();
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
// 记录用户退出日志
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
}
// 将字符串渲染到客户端
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
} catch (Exception e) {
e.printStackTrace();
}
}
final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
if (session != null) {
final String sessionID = session.getId();
logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);
try {
session.invalidate();
} catch (final IllegalStateException e) {
logger.debug("Error invalidating session.", e);
}
this.logoutStrategy.logout(request);
}
}
}
private boolean isMultipartRequest(final HttpServletRequest request) {
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart");
}
private static boolean isServlet30() {
try {
return HttpServletRequest.class.getMethod("logout") != null;
} catch (final NoSuchMethodException e) {
return false;
}
}
private interface LogoutStrategy {
void logout(HttpServletRequest request);
}
private static class Servlet25LogoutStrategy implements LogoutStrategy {
@Override
public void logout(final HttpServletRequest request) {
}
}
private class Servlet30LogoutStrategy implements LogoutStrategy {
@Override
public void logout(final HttpServletRequest request) {
try {
request.logout();
} catch (final ServletException e) {
logger.debug("Error performing request.logout.");
}
}
}
}
11. 修改 TokenService
package com.ruoyi.framework.web.service;
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.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.AddressUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import eu.bitwalker.useragentutils.UserAgent;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* token验证处理
*
* @author ruoyi
*/
@Component("tokenService")
public class TokenService {
// 令牌自定义标识
@Value("${token.header}")
private String header;
// 令牌秘钥
@Value("${token.secret}")
private String secret;
// 令牌有效期(单位:分钟)
@Value("${token.expireTime}")
private int expireTime;
private static final long MILLIS_SECOND = 1000;
private static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
@Autowired
private RedisCache redisCache;
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
// 字符串非空判断
if (StringUtils.isNotEmpty(token)) {
try {
// 从令牌中获取数据声明
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
// 获取Redis中令牌的key值
String userKey = getTokenKey(uuid);
// 获取Redis中的用户信息
return redisCache.getCacheObject(userKey);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 设置用户身份信息
*/
public void setLoginUser(LoginUser loginUser) {
// 非空判断
if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
// 刷新令牌有效期
refreshToken(loginUser);
}
}
/**
* 删除用户身份信息
*/
public void delLoginUser(String token) {
// 字符串非空判断
if (StringUtils.isNotEmpty(token)) {
// 获取Redis中令牌的key值
String userKey = getTokenKey(token);
// 删除Redis中的用户信息
redisCache.deleteObject(userKey);
}
}
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser) {
// 获取随机UUID
String token = IdUtils.fastUUID();
// 设置用户唯一标识
loginUser.setToken(token);
// 设置用户代理信息
setUserAgent(loginUser);
// 刷新令牌有效期
refreshToken(loginUser);
// 新建集合对象
Map<String, Object> claims = new HashMap<>();
// 集合新增元素
claims.put(Constants.LOGIN_USER_KEY, token);
// 从数据声明中生成令牌
return createToken(claims);
}
/**
* 验证令牌有效期(相差不足20分钟时自动刷新缓存)
*
* @param loginUser 令牌
*/
public void verifyToken(LoginUser loginUser) {
// 获取过期时间
long expireTime = loginUser.getExpireTime();
// 获取当前时间
long currentTime = System.currentTimeMillis();
// 时间差值判断
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
// 刷新令牌有效期
refreshToken(loginUser);
}
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
// 设置登录时间
loginUser.setLoginTime(System.currentTimeMillis());
// 设置过期时间
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
// 将用户信息保存在Redis中
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
/**
* 设置用户代理信息
*
* @param loginUser 登录信息
*/
public void setUserAgent(LoginUser loginUser) {
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}
/**
* 从数据声明中生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)))
.compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)))
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 获取请求token
*
* @param request 请求
* @return token
*/
private String getToken(HttpServletRequest request) {
// 获取请求头信息
String token = request.getHeader(header);
// 条件判断
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
// 去除令牌前缀
token = token.replace(Constants.TOKEN_PREFIX, "");
}
// 返回令牌信息
return token;
}
/**
* 获取Redis中令牌的key值
* @param uuid 随机数
* @return Redis中令牌的key值
*/
private String getTokenKey(String uuid) {
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
}
}
12. secret 长度修改
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: 38E17E08D8E840CCAAC70B4824BB20056F3WOT8P1GFW6151037NOTLDPKQX762L3OJWW45GTL09K6ZI83E1KLTKDB
# 令牌有效期(默认30分钟)
expireTime: 30
单点登录 前端 开发
1、修改 settings.js
/**
* 开启cas
*/
casEnable: true,
/**
* 单点url
*/
casUrl: 'http://host:port/sso/login',
/**
* 后台登录url
*/
apploginUrl: process.env.VUE_APP_FRONT_END_HOST_AND_PORT + process.env.VUE_APP_BASE_API + '/cas/index',
/**
* 单点登录url
*/
casloginUrl: 'http://host:port/sso/login?service='+ process.env.VUE_APP_FRONT_END_HOST_AND_PORT + process.env.VUE_APP_PUBLIC_PATH + '/index',
/**
* 单点登出url
*/
caslogoutUrl: 'http://host:port/sso/logout?service=http://host:port/sso/login?service='+ process.env.VUE_APP_FRONT_END_HOST_AND_PORT + process.env.VUE_APP_PUBLIC_PATH + '/index',
2、修改 permission.js
- 判断没有token时访问cas登录页面:
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isRelogin } from '@/utils/request'
import defaultSettings from '@/settings'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/auth-redirect', '/bind', '/register']
router.beforeEach((to, from, next) => {
NProgress.start()
debugger
alert('beforeEach getToken')
if (getToken()) {
debugger
alert('getToken in')
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
if (!defaultSettings.casEnable) {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
}
//开启cas
if (defaultSettings.casEnable) {
alert('defaultSettings.apploginUrl:' + defaultSettings.apploginUrl);
window.location.href = defaultSettings.apploginUrl // 否则全部重定向到登录页
}
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
3、修改 request.js、Navbar.vue
-
登出后不做响应:
-
request.js
import axios from 'axios'
import { Notification, MessageBox, Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken, removeAllCookie } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from "@/utils/ruoyi";
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import defaultSettings from '@/settings'
let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API,
// 超时
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 单点重定向判断
if(res.status === 200 && res.request.responseURL.indexOf(defaultSettings.casUrl) > -1){
removeAllCookie()
alert('defaultSettings.casloginUrl:' + defaultSettings.casloginUrl);
window.location.href = defaultSettings.casloginUrl
}
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if(res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer'){
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = '/index';
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
Message({
message: msg,
type: 'error'
})
return Promise.reject(new Error(msg))
} else if (code === 302) {
removeAllCookie()
alert('defaultSettings.casloginUrl:' + defaultSettings.casloginUrl);
window.location.href = defaultSettings.casloginUrl
} else if (code !== 200) {
Notification.error({
title: msg
})
return Promise.reject('error')
} else {
return res.data
}
},
error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
}
else if (message.includes("timeout")) {
message = "系统接口请求超时";
}
else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({
message: message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
// 通用下载方法
export function download(url, params, filename) {
downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob'
}).then(async (data) => {
const isLogin = await blobValidate(data);
if (isLogin) {
const blob = new Blob([data])
saveAs(blob, filename)
} else {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg);
}
downloadLoadingInstance.close();
}).catch((r) => {
console.error(r)
Message.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close();
})
}
export default service
- Navbar.vue
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
<div class="right-menu">
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<el-tooltip content="源码地址" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip content="文档地址" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar" class="user-avatar">
<i class="el-icon-caret-bottom" />
</div>
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item @click.native="setting = true">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
export default {
components: {
Breadcrumb,
TopNav,
Hamburger,
Screenfull,
SizeSelect,
Search,
RuoYiGit,
RuoYiDoc
},
computed: {
...mapGetters([
'sidebar',
'avatar',
'device'
]),
setting: {
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
},
topNav: {
get() {
return this.$store.state.settings.topNav
}
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
// location.href = '/index';
})
}).catch(() => {});
}
}
}
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container {
float: left;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>
4、修改 user.js
- 登出后跳转到cas登出页面:
import defaultSettings from '@/settings'
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
roles: [],
permissions: []
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
alert('Login');
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_NAME', user.userName)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
// 退出系统
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
resolve()
location.href = defaultSettings.caslogoutUrl
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
5. 修改 auth.js
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
const JsessionId = 'JSESSIONID'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
debugger
alert('removeToken')
return Cookies.remove(TokenKey)
}
export function removeJsessionId() {
return Cookies.remove(JsessionId)
}
export function removeAllCookie() {
removeToken()
removeJsessionId()
}
6. 添加环境变量
# 智能招聘管理系统 - 写在 开发环境中 为了 不报错
VUE_APP_PUBLIC_PATH = '/xxx_vue'
#前端域名+端口
VUE_APP_FRONT_END_HOST_AND_PORT = 'http://host:port'
#后端域名+端口
VUE_APP_BACK_END_HOST_AND_PORT = 'http://host:port'
补充
- 正常情况下 使用 若依vue前后端分离单点登录和登出 工程代码就可以了,开发环境没有问题;但是正式环境是 nginx 代理,而且是第一次用,过程中遇到很多问题,在此总结一下。
1. 首先 修改 app.server.host.url 地址为 nginx 配置的代理地址
app:
server:
host:
url: http://host:port/xxx-api
2. 页面不停刷新
- 由
丢失cookie导致,设置默认的cookie path - CasAuthenticationSuccessHandler
- onAuthenticationSuccess
// 往Cookie中设置token
Cookie casCookie = new Cookie(Constants.WEB_TOKEN_KEY, token);
casCookie.setMaxAge(expireTime * 60);
// TODO: 设置 cookie path 为 根目录(解决前端 cookie 丢失问题), 不确定 是否合理
casCookie.setPath("/");
response.addCookie(casCookie);
- 解决了不停刷新的问题,先在不确定是否合理
3. 偶尔 404
-
由重定向地址拼接
jsessionid导致 -
修改
session.tracking-modes
server:
servlet:
session:
tracking-modes: cookie