若依前后端分离版本集成CAS

发布时间 2023-06-05 22:23:31作者: sumling

若依前后端分离版本集成CAS

转自 若依前后端分离版本集成CAS Server5.3_qq_27230853的博客-CSDN博客

根据大佬的方式实现了集成cas,但是遇到一个问题,就是当我把后端项目加上后缀后就前后端一直来回重定向,最后发现是Cookie中的token没有作为请求值,而是作为响应值,捣鼓了一天,最后加上casCookie.setPath("/")能正常,原理也没搞清楚,先记录下吧。

后端配置

一、后端集成

在common项目中添加对cas的支持,RuoYi-Vue版本使用的是Security权限框架,所以添加securyti-cas依赖即可:

<!-- 添加spring security cas支持 -->
<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-cas</artifactId>
</dependency>

二、修改LoginUser.java

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

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
  return new HashSet();
}

三、修改Constants.java

添加CAS认证成功标识:

/**
* CAS登录成功后的后台标识
*/
public static final String CAS_TOKEN = "cas_token";
/**
* CAS登录成功后的前台Cookie的Key
*/
public static final String WEB_TOKEN_KEY = "Admin-Token";

四、添加CasPropertys.java

此类为读取cas配置信息:

package com.ruoyi.framework.config.properties;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* CAS的配置参数
*/
@Component
public class CasProperties {
   @Value("${cas.server.host.url}")
   private String casServerUrl;

   @Value("${cas.server.host.login_url}")
   private String casServerLoginUrl;

   @Value("${cas.server.host.logout_url}")
   private String casServerLogoutUrl;

   @Value("${app.server.host.url}")
   private String appServerUrl;

   @Value("${app.login_url}")
   private String appLoginUrl;

   @Value("${app.logout_url}")
   private String appLogoutUrl;

   @Value("${app.web_url}")
   private String webUrl;

   public String getWebUrl() {
       return webUrl;
  }

   public String getCasServerUrl() {
       return casServerUrl;
  }

   public void setCasServerUrl(String casServerUrl) {
       this.casServerUrl = casServerUrl;
  }

   public String getCasServerLoginUrl() {
       return casServerLoginUrl;
  }

   public void setCasServerLoginUrl(String casServerLoginUrl) {
       this.casServerLoginUrl = casServerLoginUrl;
  }

   public String getCasServerLogoutUrl() {
       return casServerLogoutUrl;
  }

   public void setCasServerLogoutUrl(String casServerLogoutUrl) {
       this.casServerLogoutUrl = casServerLogoutUrl;
  }

   public String getAppServerUrl() {
       return appServerUrl;
  }

   public void setAppServerUrl(String appServerUrl) {
       this.appServerUrl = appServerUrl;
  }

   public String getAppLoginUrl() {
       return appLoginUrl;
  }

   public void setAppLoginUrl(String appLoginUrl) {
       this.appLoginUrl = appLoginUrl;
  }

   public String getAppLogoutUrl() {
       return appLogoutUrl;
  }

   public void setAppLogoutUrl(String appLogoutUrl) {
       this.appLogoutUrl = appLogoutUrl;
  }

}

五、添加CasUserDetailsService.java

在framework项目下添加:

package com.ruoyi.framework.web.service;

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.BaseException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
* 用于加载用户信息 实现UserDetailsService接口,或者实现AuthenticationUserDetailsService接口
*
* @author HuXiao
*/
@Service
public class CasUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {

   private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

   @Autowired
   private ISysUserService userService;

   @Autowired
   private SysPermissionService permissionService;

   public UserDetails createLoginUser(SysUser user) {
       return new LoginUser(user, permissionService.getMenuPermission(user));
  }

   @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 UsernameNotFoundException("登录用户:" + username + " 不存在");
      } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
           log.info("登录用户:{} 已被删除.", username);
           throw new BaseException("对不起,您的账号:" + username + " 已被删除");
      } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
           log.info("登录用户:{} 已被停用.", username);
           throw new BaseException("对不起,您的账号:" + username + " 已停用");
      }
       return createLoginUser(user);
  }
}

六、添加CasAuthenticationSuccessHandler.java (就是在这里设置casCookie.setPath("/"))

也是在framework项目下:

package com.ruoyi.framework.security.handle;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.framework.config.properties.CasProperties;
import com.ruoyi.framework.web.service.TokenService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
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;

@Service
public class CasAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

   protected final Log logger = LogFactory.getLog(this.getClass());

   private RequestCache requestCache = new HttpSessionRequestCache();

   @Autowired
   private TokenService tokenService;

   @Autowired
   private CasProperties casProperties;

   // 令牌有效期(默认30分钟)
   @Value("${token.expireTime}")
   private int expireTime;

   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                       Authentication authentication) throws ServletException, IOException {
       String targetUrlParameter = getTargetUrlParameter();
       if (isAlwaysUseDefaultTargetUrl()
               || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
           requestCache.removeRequest(request, response);
           super.onAuthenticationSuccess(request, response, authentication);
           return;
      }
       clearAuthenticationAttributes(request);
       LoginUser userDetails = (LoginUser) authentication.getPrincipal();
       String token = tokenService.createToken(userDetails);
       //往Cookie中设置token
       Cookie casCookie = new Cookie(Constants.WEB_TOKEN_KEY, token);
       casCookie.setMaxAge(expireTime * 60);
       //添加casCookie.setPath("/")后正常将token传至Cookie
       casCookie.setPath("/");
       response.addCookie(casCookie);
       //设置后端认证成功标识
       HttpSession httpSession = request.getSession();
       httpSession.setAttribute(Constants.CAS_TOKEN, token);
       //登录成功后跳转到前端登录页面
       getRedirectStrategy().sendRedirect(request, response, casProperties.getWebUrl());
  }
}

七、修改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.handle.CasAuthenticationSuccessHandler;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
import com.ruoyi.framework.web.service.CasUserDetailsService;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   private CasProperties casProperties;

   @Autowired
   private CasUserDetailsService customUserDetailsService;

   @Autowired
   private CasAuthenticationSuccessHandler casAuthenticationSuccessHandler;

   @Autowired
   private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

   /**
    * 退出处理类
    */
   @Autowired
   private LogoutSuccessHandlerImpl logoutSuccessHandler;

   /**
    * 跨域过滤器
    */
   @Autowired
   private CorsFilter corsFilter;

   @Override
   protected void configure(HttpSecurity httpSecurity) throws Exception {
       httpSecurity
               // CSRF禁用,因为不使用session
              .csrf().disable()
               // 基于token,所以不需要session
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
               // 过滤请求
              .authorizeRequests()
               // 对于登录login 验证码captchaImage 允许匿名访问
               //.antMatchers("/login", "/captchaImage").anonymous()
              .antMatchers(
                       HttpMethod.GET,
                       "/*.html",
                       "/**/*.html",
                       "/**/*.css",
                       "/**/*.js"
              ).permitAll()
              .antMatchers("/profile/**").anonymous()
              .antMatchers("/common/download**").anonymous()
              .antMatchers("/common/download/resource**").anonymous()
              .antMatchers("/swagger-ui.html").anonymous()
              .antMatchers("/swagger-resources/**").anonymous()
              .antMatchers("/webjars/**").anonymous()
              .antMatchers("/*/api-docs").anonymous()
              .antMatchers("/druid/**").anonymous()
               // 除上面外的所有请求全部需要鉴权认证
              .anyRequest().authenticated()
              .and()
              .headers().frameOptions().disable();
       //单点登录登出
       httpSecurity.logout().permitAll().logoutSuccessHandler(logoutSuccessHandler);
       // Custom JWT based security filter
       httpSecurity.addFilter(casAuthenticationFilter())
              .addFilterBefore(jwtAuthenticationTokenFilter, CasAuthenticationFilter.class)
               //.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
              .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class).exceptionHandling()
              .authenticationEntryPoint(casAuthenticationEntryPoint());

       // 添加CORS filter
       httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
       httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
       // disable page caching
       httpSecurity.headers().cacheControl();
  }

   /**
    * 解决 无法直接注入 AuthenticationManager
    *
    * @return
    * @throws Exception
    */
   @Bean
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception {
       return super.authenticationManagerBean();
  }

   /**
    * 定义认证用户信息获取来源,密码校验规则等
    */
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       super.configure(auth);
       auth.authenticationProvider(casAuthenticationProvider());
  }

   /**
    * 认证的入口
    */
   @Bean
   public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
       CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
       casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
       casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
       return casAuthenticationEntryPoint;
  }

   /**
    * 指定service相关信息
    */
   @Bean
   public ServiceProperties serviceProperties() {
       ServiceProperties serviceProperties = new ServiceProperties();
       serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
       serviceProperties.setAuthenticateAllArtifacts(true);
       return serviceProperties;
  }

   /**
    * CAS认证过滤器
    */
   @Bean
   public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
       CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
       casAuthenticationFilter.setAuthenticationManager(authenticationManager());
       casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
       casAuthenticationFilter.setAuthenticationSuccessHandler(casAuthenticationSuccessHandler);
       return casAuthenticationFilter;
  }

   /**
    * cas 认证 Provider
    */
   @Bean
   public CasAuthenticationProvider casAuthenticationProvider() {
       CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
       casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService);
       casAuthenticationProvider.setServiceProperties(serviceProperties());
       casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
       casAuthenticationProvider.setKey("casAuthenticationProviderKey");
       return casAuthenticationProvider;
  }

   @Bean
   public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
       return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl());
  }

   /**
    * 单点登出过滤器
    */
   @Bean
   public SingleSignOutFilter singleSignOutFilter() {
       SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
       singleSignOutFilter.setCasServerUrlPrefix(casProperties.getCasServerUrl());
       singleSignOutFilter.setIgnoreInitConfiguration(true);
       return singleSignOutFilter;
  }

   /**
    * 请求单点退出过滤器
    */
   @Bean
   public LogoutFilter casLogoutFilter() {
       LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(),
               new SecurityContextLogoutHandler());
       logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
       return logoutFilter;
  }
}

八、修改配置文件

在admin项目下的application.yml配置文件中添加:

#CAS
cas:
server:
  host:
    #CAS服务地址
    url: http://localhost:9090/cas
    #CAS服务登录地址
    login_url: ${cas.server.host.url}/login
    #CAS服务登出地址
    logout_url: ${cas.server.host.url}/logout?service=${app.server.host.url}
# 应用访问地址
app:
server:
  host:
    url: http://localhost:${server.port}/appSupport
login_url: / #应用登录地址
logout_url: /logout #应用登出地址
web_url: http://localhost:1024/index #前端登录地址

至此,后端配置已完毕,不出意外启动项目后访问项目路径即可跳转到cas登录界面。

前段配置

一、修改settings.js

添加cas登录和登出地址方便访问

 /**
  * 单点登录url
  */
casloginUrl: 'http://localhost:9090/cas/login?service=http://localhost:8080/appSupport/',

/**
  * 单点登出url
  */
caslogoutUrl: 'http://localhost:9090/cas/logout?service=http://localhost:8080/appSupport/'

二、修改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'

NProgress.configure({showSpinner: false})

const whiteList = ['/auth-redirect', '/bind', '/register']

const defaultSettings = require('./settings.js')

router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
  if (store.getters.roles.length === 0) {
    // 判断当前用户是否已拉取完user_info信息
    store.dispatch('GetInfo').then(() => {
      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 {
    //next('index') // 否则全部重定向到登录页
    window.location.href = defaultSettings.casloginUrl
    NProgress.done()
  }
}
})

router.afterEach(() => {
NProgress.done()
})

三、修改request.js,Navbar.vue

登出后不做响应:

if (code === 401) {
    MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
        confirmButtonText: '重新登录',
        cancelButtonText: '取消',
        type: 'warning'
      }
    ).then(() => {
      store.dispatch('LogOut').then(() => {
        //location.href = '/index';
      })
    })
  }
methods: {
  toggleSideBar() {
    this.$store.dispatch('app/toggleSideBar')
  },
  async logout() {
    this.$confirm('确定注销并退出系统吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }).then(() => {
      this.$store.dispatch('LogOut').then(() => {
        //location.href = '/index';
      })
    })
  }
}

 

四、修改user.js

登出后跳转到cas登出页面:

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)
      })
    })
  }

至此,前端代码也已经修改完毕,启动前端项目不出意外在刚才的登录地址中输入用户名和密码就可以登录了,点击退出也会退出到cas的登录页面,若是不能退出到cas的登录页面请修改casServer的相关配置使其支持即可。注意,需要在上方引用settings。

总结

以上就是此次CAS集成的相关步骤,总体来说和前后端不分离类似但是稍有不同,此种集成需要CAS登录成功后直接请求后端路径,然后再通过后端路径转发到前端,token之类的都需要在后端通过response去设置,退出登录时之要后台清空后端登录信息,然后返回前端通过调用cas登出的方法去做cas的logout,不然会有跨域的问题,当然若是能解决axios跨域的问题也可以用后台做cas的logout,欢迎去大佬那留言~