使用CAS 5.1搭建(先用的其他版本5.3,6.2都有导包的问题,最后用的5.1才可以),SpringBoot2.3.0,Spring Security5.3
江南一点雨系列(SpringBoot + Spring Security 整合CAS):
- 23. 松哥手把手教你入门 Spring Boot + CAS 单点登录
- 24. Spring Boot 实现单点登录的第三种方案!
- 25. Spring Boot+CAS 单点登录,如何对接数据库?
- 26. Spring Boot+CAS 默认登录页面太丑了,怎么办?
前后端分离项目解决跨域: 28. Spring Boot 中三种跨域场景总结
史上最全的Cas学习整理-yellowcong
spring security集成casCAS实现单点登录SSO执行原理探究(终于明白了)
CAS(一)搭建CAS - server服务器
CAS单点登录-自定义登录页、修改编辑登录页
不使用域名,直接使用ip地址生成 https 证书:IP地址开启https
##
# CAS Server Context Configuration
#
server.context-path=/cas
server.port=8443
cas.serviceRegistry.json.location=classpath:/services
cas.serviceRegistry.initFromJson=true
#as.tgc.secure=false
server.ssl.key-store=classpath:keystore
server.ssl.key-store-password=123456
server.ssl.key-password=123456
# server.ssl.ciphers=
# server.ssl.client-auth=
# server.ssl.enabled=
# server.ssl.key-alias=
# server.ssl.key-store-provider=
# server.ssl.key-store-type=
# server.ssl.protocol=
# server.ssl.trust-store=
# server.ssl.trust-store-password=
# server.ssl.trust-store-provider=
# server.ssl.trust-store-type=
server.max-http-header-size=2097152
server.use-forward-headers=true
server.connection-timeout=20000
server.error.include-stacktrace=ALWAYS
server.tomcat.max-http-post-size=2097152
server.tomcat.basedir=build/tomcat
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.pattern=%t %a "%r" %s (%D ms)
server.tomcat.accesslog.suffix=.log
server.tomcat.max-threads=10
server.tomcat.port-header=X-Forwarded-Port
server.tomcat.protocol-header=X-Forwarded-Proto
server.tomcat.protocol-header-https-value=https
server.tomcat.remote-ip-header=X-FORWARDED-FOR
server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
##
# CAS Cloud Bus Configuration
#
spring.cloud.bus.enabled=false
# spring.cloud.bus.refresh.enabled=true
# spring.cloud.bus.env.enabled=true
# spring.cloud.bus.destination=CasCloudBus
# spring.cloud.bus.ack.enabled=true
endpoints.enabled=false
endpoints.sensitive=true
endpoints.restart.enabled=false
endpoints.shutdown.enabled=false
management.security.enabled=true
management.security.roles=ACTUATOR,ADMIN
management.security.sessions=if_required
management.context-path=/status
management.add-application-context-header=false
security.basic.authorize-mode=role
security.basic.enabled=false
security.basic.path=/cas/status/**
##
# CAS Web Application Session Configuration
#
server.session.timeout=300
server.session.cookie.http-only=true
server.session.tracking-modes=COOKIE
##
# CAS Thymeleaf View Configuration
#
spring.thymeleaf.encoding=UTF-8
#spring.thymeleaf.cache=true
spring.thymeleaf.cache=false
spring.thymeleaf.mode=HTML
##
# CAS Log4j Configuration
#
# logging.config=file:/etc/cas/log4j2.xml
server.context-parameters.isLog4jAutoInitializationDisabled=true
##
# CAS AspectJ Configuration
#
spring.aop.auto=true
spring.aop.proxy-target-class=true
##
# CAS Authentication Credentials
#
#cas.authn.accept.users=admin::1234
# 数据库连接
cas.authn.jdbc.query[0].url=jdbc:mysql://数据库IP:数据库端口/数据库名?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
# 数据库用户名
cas.authn.jdbc.query[0].user=数据库用户名
# 数据库用户密码
cas.authn.jdbc.query[0].password=数据库用户密码
# 查询账号密码SQL,必须包含密码字段
cas.authn.jdbc.query[0].sql=select * from g_user where username=?
cas.authn.jdbc.query[0].fieldPassword=password
# 数据库驱动
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
# 数据库dialect配置
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
# Rest配置
#cas.authn.rest.uri=http://127.0.01:8088/user/login
# cas.authn.rest.name=
# cas.authn.rest.passwordEncoder.type=NONE|DEFAULT|STANDARD|BCRYPT|SCRYPT|PBKDF2|com.example.CustomPasswordEncoder
# cas.authn.rest.passwordEncoder.characterEncoding=UTF-8
# cas.authn.rest.passwordEncoder.encodingAlgorithm=
# cas.authn.rest.passwordEncoder.secret=
# cas.authn.rest.passwordEncoder.strength=16
#cas.theme.defaultThemeName=mylogin
#配置允许登出后跳转到指定页面(使用场景:退出CAS并跳转到首页: https://localhost:8443/cas/logout?service=http://localhost:8081/index)
cas.logout.followServiceRedirects=true
#跳转到指定页面需要的参数名为 service
cas.logout.redirectParameter=service
#登出后需要跳转到的地址,如果配置该参数,service将无效。
#cas.logout.redirectUrl=http://localhost:8081/index
#在退出时是否需要 确认退出提示 true弹出确认提示框 false直接退出
#cas.logout.confirmLogout=true
#是否移除子系统的票据
#cas.logout.removeDescendantTickets=true
#禁用单点登出,默认是false不禁止
#cas.slo.disabled=true
#默认异步通知客户端,清除session
#cas.slo.asynchronous=true
#默认配置(负数为永不过期)
#TGT的最大生存时间,默认28800秒,也就是八小时;
cas.ticket.tgt.maxTimeToLiveInSeconds=-1
#在用户没有对系统进行任何操作的情况下,默认7200秒之后,也就是两个小时之后TGT会过期
cas.ticket.tgt.timeToKillInSeconds=-1
#Remember Me配置
# cas.ticket.tgt.rememberMe.enabled=true
# cas.ticket.tgt.rememberMe.timeToKillInSeconds=28800
#Timeout(应用于TGTs的过期策略提供了最近使用的过期策略,类似于Web服务器会话超时)
# cas.ticket.tgt.timeout.maxTimeToLiveInSeconds=28800
#Throttled Timeout(节流超时策略通过节流的概念扩展了超时策略,其中最多每N秒使用一个票据。)
# cas.ticket.tgt.throttledTimeout.timeToKillInSeconds=28800
# cas.ticket.tgt.throttledTimeout.timeInBetweenUsesInSeconds=5
#Hard Timeout(硬超时策略提供了从创建时开始计算的有限票据生命周期。)
# cas.ticket.tgt.hardTimeout.timeToKillInSeconds=28800
#当你访问一个应用系统时,cas server签发了一张票据,你需要在十秒钟之内拿着这种ST去server进行校验,过了20秒钟就过期了,系统也就访问不了;
cas.ticket.st.timeToKillInSeconds=20
#ST可以用几次才过期,默认是用过一次就过期。
cas.ticket.st.numberOfUses=1
<!--Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- security 对CAS支持 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
# CAS服务地址
cas.server.prefix=https://localhost:8443/cas
# CAS Server 登录地址
cas.server.login=${cas.server.prefix}/login
# CAS Server 登出地址
cas.server.logout=${cas.server.prefix}/logout
#应用访问地址
cas.client.prefix=http://localhost:8081
# CAS Client 登录地址
cas.client.login=${cas.client.prefix}/login/cas
# CAS Client 登出地址
cas.client.logoutRelative=/logout/cas
cas.client.logout=${cas.client.prefix}${cas.client.logoutRelative}
#SpringSecurity session超时时间设置(28800秒=8小时)
server.servlet.session.timeout=28800
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "cas.client")
public class CASClientProperties {
private String prefix;
private String login;
private String logoutRelative;
private String logout;
/** 此处省略getter、setter */
}
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "cas.server")
public class CASServerProperties {
private String prefix;
private String login;
private String logout;
/** 此处省略getter、setter */
}
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ProxyTicketValidator;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
@Configuration
public class CasSecurityConfig {
@Autowired
CASClientProperties casClientProperties;
@Autowired
CASServerProperties casServerProperties;
@Autowired
UserDetailsService userDetailService;
/**
* 1.处理 CAS 验证逻辑
* @return
*/
@Bean
CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidator());
provider.setUserDetailsService(userDetailService);
provider.setKey("aaaaa"); //不知道具体作用,目前看来使用上没有影响
return provider;
}
/**
* 2.配置一下 Client 的登录地址;
* 在 CAS Server 上登录成功后,重定向的地址
* @return
*/
@Bean
ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casClientProperties.getLogin()); //配置 Client 的登录地址
return serviceProperties;
}
/**
* 3.配置 ticket 校验地址,CAS Client 拿到 ticket 要去 CAS Server 上校验,
* 默认校验地址是:${cas.server.prefix}/proxyValidate?ticket=xxx
* @return
*/
@Bean
TicketValidator ticketValidator() {
return new Cas20ProxyTicketValidator(casServerProperties.getPrefix());
}
/**
* 4.CAS 认证的过滤器,过滤器将请求拦截下来之后,交由 CasAuthenticationProvider 来做具体处理
* @param authenticationProvider
* @return
*/
@Bean
CasAuthenticationFilter casAuthenticationFilter(AuthenticationProvider authenticationProvider) {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(serviceProperties());
filter.setAuthenticationManager(new ProviderManager(authenticationProvider));
return filter;
}
/**
* 5.表示接受 CAS Server 发出的注销请求,所有的注销请求都将从 CAS Client 转发到 CAS Server,CAS Server 处理完后,会通知所有的 CAS Client 注销登录。
* @return
*/
@Bean
SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter sign = new SingleSignOutFilter();
sign.setIgnoreInitConfiguration(true);
return sign;
}
/**
* 6.配置将注销请求转发到 CAS Server。
* @return
*/
@Bean
LogoutFilter logoutFilter() {
LogoutFilter filter = new LogoutFilter(casServerProperties.getLogout(), new SecurityContextLogoutHandler());
filter.setFilterProcessesUrl(casClientProperties.getLogoutRelative());
return filter;
}
/**
* 7.CAS 验证的入口
* @return
*/
@Bean
@Primary
AuthenticationEntryPoint authenticationEntryPoint() {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(casServerProperties.getLogin()); //设置 CAS Server 的登录地址
entryPoint.setServiceProperties(serviceProperties());
return entryPoint;
}
}
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.time.Duration;
import java.util.Arrays;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
AuthenticationProvider authenticationProvider;
@Autowired
SingleSignOutFilter singleSignOutFilter;
@Autowired
LogoutFilter logoutFilter;
@Autowired
CasAuthenticationFilter casAuthenticationFilter;
/**
* 8.
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
/**
* 9.配置安全策略: /user/** 格式的路径需要有 "user" 角色才能访问;
*
* requestMatchers() 配置一个request Mather数组,参数为RequestMatcher 对象,其match 规则自定义,需要的时候放在最前面,对需要匹配的的规则进行自定义与过滤
* authorizeRequests() URL权限配置
* antMatchers() 配置一个request Mather 的 string数组,参数为 ant 路径格式, 直接匹配url
* anyRequest 匹配任意url,无参 ,最好放在最后面
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable(); //解决浏览器报错:项目中用到iframe嵌入网页,然后用到springsecurity就被拦截了 浏览器报错 x-frame-options deny;原因是因为springSecurty使用X-Frame-Options防止网页被Frame
http
.authorizeRequests()
.antMatchers("/user/**").hasRole("user")
.antMatchers("/admin/**").hasRole("admin") //hasRole(String role): 限制单个角色访问,角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较. 另一个方法是hasAuthority(String authority)
.antMatchers("/admin/**").hasAnyRole("admin","user") //hasAnyRole(String... roles): 限制多个角色访问,admin和user角色都可以访问
.antMatchers("/hello").permitAll() //permitAll(): 指定URL无需保护
.antMatchers("/login/cas").permitAll()
.anyRequest() //匹配任意url
.authenticated() //表示剩下的任何请求需要验证之后才可以访问
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
//.addFilter(casAuthenticationFilter)
.addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
.addFilterBefore(logoutFilter, LogoutFilter.class)
.logout()
.logoutUrl("/signout") // 退出登录的url
.logoutSuccessUrl("/cas_logout") // 退出登录成功跳转的url
.deleteCookies("JSESSIONID") // 删除名为"JSESSIONID"的cookie
.and()
.cors()
//.configurationSource(corsConfigurationSource())
.and()
.csrf()
.disable();
// //只允许一个用户登录,如果同一个账户两次登录,那么第一个账户将被踢下线,跳转到登录页面
// http.sessionManagement().maximumSessions(1).expiredUrl("/login");
}
/**
* 解决security跨域
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setMaxAge(Duration.ofHours(1));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",configuration);
return source;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@Primary
public class UserDetailsServiceImpl implements UserDetailsService{
@Autowired
private UserDao userDao;
/**
*
* @param s 用户虽然在 CAS Server 上登录,但是,登录成功之后,CAS Client 还是要获取用户的基本信息、角色等,
* 以便做进一步的权限控制,所以,这里的 loadUserByUsername 方法中的参数,实际上就是你从 CAS Server 上登录成功后获取到的用户名,
* 拿着这个用户名,去数据库中查询用户的相关信息并返回,方便 CAS Client 在后续的鉴权中做进一步的使用。
*
* 当用户在 CAS Server 上登录成功之后,拿着用户名回到 CAS Client,然后我们再去数据库中根据用户名获取用户的详细信息,包括用户的角色等,进而在后面的鉴权中用上角色;
* @return
* @throws UsernameNotFoundException
*/
//@Override
//public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// return new User(s, "1234", true, true, true, true,
// AuthorityUtils.createAuthorityList("ROLE_user"));
//}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByUsername(username);
List<Role> roles = new ArrayList<>();
roles.add(userDao.getRole(user.getId()));
user.setRoles(roles);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}
public class Role {
private Long id; //角色 id
private String name; //角色名称(英文)
private String nameZh; //角色名称(中文)
/** 此处省略getter、setter */
}
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private boolean accountNonExpired; //账户是否没有过期
private boolean accountNonLocked; //账户是否没有被锁定
private boolean credentialsNonExpired; //密码是否没有过期
private boolean enabled; //账户是否可用
private List<Role> roles; //用户的角色
//用户的角色信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
/** 此处省略getter、setter */
}
@Repository
public interface UserDao {
@Select("SELECT u.id, u.account_non_expired as accountnonexpired, u.account_non_locked as accountnonlocked, u.credentials_non_expired as credentialsnonexpired, u.enabled, u.password, u.username, r.name as roles, r.name_zh FROM g_user u \n" +
"LEFT JOIN g_user_roles ur on ur.t_user_id = u.id \n" +
"LEFT JOIN g_role r on ur.roles_id = r.id \n" +
"where u.username= #{username};")
User findByUsername(String username);
@Select("SELECT r.* FROM g_user u \n" +
"LEFT JOIN g_user_roles ur on ur.t_user_id = u.id \n" +
"LEFT JOIN g_role r on ur.roles_id = r.id \n" +
"where u.id= #{id};")
Role getRole(Long id);
@Select("SELECT id, account_non_expired as accountnonexpired, account_non_locked as accountnonlocked, credentials_non_expired as credentialsnonexpired, enabled, password, username FROM g_user where username = #{username} and password = #{password};")
User findByUsernameAndPassword(String username, String password);
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
private static final Logger logger = LoggerFactory.getLogger(IndexController.class);
@CrossOrigin
@GetMapping("/hello")
public String hello() {
return "hello";
}
@CrossOrigin
@GetMapping("/admin/hello")
public String admin_hello() {
return "admin_hello";
}
@GetMapping("/user/hello")
public String user_hello() {
return "user_hello";
}
}