若依前后端分离版整合CAS Client实现一对多绑定

前言

针对当前系统已经存在用户的情况下,实现CAS Server用户与CAS Client用户一对多绑定,该项目使用权限框架是spring security。

本例针对局域网开发,部署需额外配置。

后端

1. 添加配置文件

ruoyi-admin/src/main/resources目录下添加application.properties文件

#CAS服务地址
cas.server.host.url=https://CAS服务的域名/cas
#CAS服务登录地址
cas.server.host.login_url=${cas.server.host.url}/login
#CAS服务登出地址
cas.server.host.logout_url=${cas.server.host.url}/logout?service=${app.server.host.url}
#应用访问地址
app.server.host.url=https://该应用的域名:8443
#应用登录地址
app.login.url=/login
#应用登出地址
app.logout.url=/logout

2. 配置HTTPS

  1. 修改src\main\java\com\ruoyi\common\core\domain\model目录下的pom文件,添加依赖
<!-- 添加spring security cas支持 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
</dependency>
  1. ruoyi-admin/src/main/resources目录下添加SSL证书,此处使用的是.p12格式的证书,证书是阿里云签发的
    证书转换命令:keytool -importkeystore -destkeystore 新证书.p12 -deststoretype pkcs12 -srckeystore 原证书.pfx
    SSL证书
  2. 修改application.yml支持HTTPS
# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: 8443
  servlet:
    # 应用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # tomcat最大线程数,默认为200
    max-threads: 800
    # Tomcat启动初始化的线程数,默认值25
    min-spare-threads: 30
  ssl:
    key-store: classpath:证书名称.p12
    key-store-type: pkcs12
    key-store-password: 证书密码

3. 修改Spring Security配置

  1. ruoyi-framework/src/main/java/com/ruoyi/framework/config目录新建properties包,新建加载配置文件类
package com.ruoyi.framework.config.properties;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
 
/**
 * CAS的配置参数
 * @author xu
 */
@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;

	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;
	}
}
  1. ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle目录新建
package com.ruoyi.framework.security.handle;

import com.alibaba.fastjson.JSON;
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.exception.CustomException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.config.properties.CasProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;

/**
 * 认证失败处理类 返回未授权
 * 
 * @author xu
 */
@Component
public class CASAuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
    private static final long serialVersionUID = -8970718410437077606L;

    @Autowired
    private CasProperties casProperties;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException
    {
        StringBuffer requestURL = request.getRequestURL();
        System.out.println("requestURL=>"+requestURL);
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        AjaxResult error = AjaxResult.error(code, msg);
        error.put("loginUrl",casProperties.getCasServerLoginUrl()+"?service="+casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
        ServletUtils.renderString(response, JSON.toJSONString(error));
    }
}
  1. ruoyi-framework/src/main/java/com/ruoyi/framework/security/web/service新建类
package com.ruoyi.framework.web.service;

import java.util.HashSet;
import java.util.Set;

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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

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

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

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    /**
     * 加载登录用户的信息
     *
     * @param token
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
        System.out.println("当前的用户名是:" + token.getName());
        String username = token.getName();
        SysUser user = userService.selectUserByCasUserName(username);
        // TODO 此处无效,待寻找解决方案
        /*
        if (StringUtils.isNull(user)) {
            log.info("登录用户:{} 不存在.请绑定CAS用户", 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);
    }

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

}
  1. ruoyi-framework/src/main/java/com/ruoyi/framework/config下新建security配置类
package com.ruoyi.framework.config;

import com.ruoyi.framework.config.properties.CasProperties;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.CASAuthenticationEntryPointImpl;
import com.ruoyi.framework.web.service.CustomUserDetailsService;
import com.ruoyi.framework.web.service.UserDetailsServiceImpl;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
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.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

@Configuration
@EnableWebSecurity //启用web权限
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法验证
public class CasSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Autowired
    private CasProperties casProperties;

    /**
     * 认证失败处理类
     */
    @Autowired
    private CASAuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 解决 无法直接注入 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());
    }

    /**
     * 定义安全策略
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // 认证失败处理类
                .authorizeRequests()//配置安全策略
                .antMatchers("/login", "/captchaImage").permitAll()
                .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()

                // activiti modeler 放行
                .antMatchers("/modeler/**").anonymous()
                .antMatchers("/activiti/definition/upload").anonymous()
                .antMatchers("/activiti/definition/readResource").anonymous()
                .antMatchers("/activiti/process/read-resource").anonymous()

                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated().and()

                //.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint()).and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .addFilter(casAuthenticationFilter())
                .addFilterBefore(casLogoutFilter(), LogoutFilter.class)
                .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
    }

    /**
     * 认证的入口
     */
    @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());
        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;
    }

    /**
     * 用户自定义的AuthenticationUserDetailsService
     */
    @Bean
    public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService() {
        return new CustomUserDetailsService();
    }

    /**
     * 配置ticket校验器
     *
     * @return
     */
    @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;
    }

}

注意:同目录下SecurityConfig类记得注释掉@EnableGlobalMethodSecurity注解
  1. 修改ruoyi-common模块下src\main\java\com\ruoyi\common\core\domain\model\LoginUser的getAuthorities()方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
    return new HashSet();
}
  1. 修改ruoyi-system模块java\com\ruoyi\system的service及mapper,添加通过cas用户查询当前系统用户的方法
    1. ISysUserService
    /**
     * 通过CAS用户名查询用户
     *
     * @param casUserName 用户名
     * @return 用户对象信息
     */
    public SysUser selectUserByCasUserName(String casUserName);
    
    1. SysUserServiceImpl
     @Override
     public SysUser selectUserByCasUserName(String casUserName) {
         return userMapper.selectUserByCasUserName(casUserName);
     }
    
    1. SysUserMapper
    SysUser selectUserByCasUserName(@Param("casUserName") String casUserName);
    
    1. SysUserMapper.xml
    <select id="selectUserByCasUserName" parameterType="String" resultMap="SysUserResult">
    	<include refid="selectUserVo"/>
    	where u.cas_user_name = #{casUserName}
    </select>
    
注意:数据库用户表添加cas_user_name字段
  1. 修改RuoYiApplication启动类,添加接口用于跳转前端页面
package com.ruoyi;

import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 启动程序
 *
 * @author ruoyi
 */
@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class,
        org.activiti.spring.boot.SecurityAutoConfiguration.class,
})
@RestController
public class RuoYiApplication
{
    public static void main(String[] args)
    {
        // System.setProperty("spring.devtools.restart.enabled", "false");
        SpringApplication.run(RuoYiApplication.class, args);
        System.out.println("(♥◠‿◠)ノ゙  若依启动成功   ლ(´ڡ`ლ)゙  \n" +
                " .-------.       ____     __        \n" +
                " |  _ _   \\      \\   \\   /  /    \n" +
                " | ( ' )  |       \\  _. /  '       \n" +
                " |(_ o _) /        _( )_ .'         \n" +
                " | (_,_).' __  ___(_ o _)'          \n" +
                " |  |\\ \\  |  ||   |(_,_)'         \n" +
                " |  | \\ `'   /|   `-'  /           \n" +
                " |  |  \\    /  \\      /           \n" +
                " ''-'   `'-'    `-..-'              ");
    }

    @RequestMapping("/")
    public void index(HttpServletResponse response) throws IOException {
        response.sendRedirect("https://你的前端域名");
    }
}
  1. 全局替换tokenService.getLoginUser方法修改为SecurityUtils.getLoginUser()方法

前端

1. 修改vue.config.js文件代理支持https

// webpack-dev-server 相关配置
devServer: {
   host: '0.0.0.0',
   port: port,
   open: true,
   https: true,
   proxy: {
     // detail: https://cli.vuejs.org/config/#devserver-proxy
     [process.env.VUE_APP_BASE_API]: {
       target: `https://localhost:8443`,
       changeOrigin: true,
       pathRewrite: {
         ['^' + process.env.VUE_APP_BASE_API]: ''
       }
     }
   },
   disableHostCheck: true
 },

2. 修改.env.development端口号为443

#端口
port = 443

3. 修改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'

NProgress.configure({ showSpinner: false })

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

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (store.getters.roles.length !== 0) {
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
        next()
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      store.dispatch('GetInfo').then(res => {
        // 拉取user_info
        const roles = res.roles
        store.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {
          // 根据roles权限生成可访问的路由表
          router.addRoutes(accessRoutes) // 动态添加可访问路由表
          next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          NProgress.done()
        })
      }).catch(err => {})
    }
  }
})

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

4. 修改vuex中user.js退出登录业务逻辑

// 退出系统
LogOut({ commit, state }) {
   return new Promise((resolve, reject) => {
     commit('SET_TOKEN', '')
     commit('SET_ROLES', [])
     commit('SET_PERMISSIONS', [])
     window.location.replace("https://你的后端域名:8443/logout")
   })
 },

5. 修改request.js

import axios from 'axios'
import { Notification, MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'

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
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    let url = config.url + '?';
    for (const propName of Object.keys(config.params)) {
      const value = config.params[propName];
      var part = encodeURIComponent(propName) + "=";
      if (value && typeof(value) !== "undefined") {
        if (typeof value === 'object') {
          for (const key of Object.keys(value)) {
            let params = propName + '[' + key + ']';
            var subPart = encodeURIComponent(params) + "=";
            url += subPart + encodeURIComponent(value[key]) + "&";
          }
        } else {
          url += part + encodeURIComponent(value) + "&";
        }
      }
    }
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    if (code === 401) {
      MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).then(() => {
        // 单点登录时使用
        window.location.replace(res.data.loginUrl)
      })
    } else if (code === 500) {
      Message({
        message: msg,
        type: 'error'
      })
      return Promise.reject(new Error(msg))
    } 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 default service

其他

1. 修改客户端及服务端hosts文件,添加相应的域名映射

1. 客户端hosts
	> CAS服务端局域网I CAS服务端域名 
	> 	127.0.0.1 CAS客户端域名	
	
2. 服务端hosts(每添加一个客户端需添加一条)
	>你的局域网IP CAS客户端域名

2. 如果访问页面被HSTS拦截

chrome浏览器:chrome://net-internals/#hsts
在这里插入图片描述之后点击delete

Logo

快速构建 Web 应用程序

更多推荐