最近接了一个任务,公司之前为客户做了很多的系统,后面做成了通用的业务系统准备向外销售,因此需要做一个演示系统将所有业务系统都放到演示系统中,用户在演示系统登录后可以访问其中的任意业务系统,这一听就是一个单点登录的需求啊,因此就去了解了下,发现了CAS,
CAS是中央认证服务Central Authentication Service
的简称。最初由耶鲁大学的Shawn Bayern 开发,后由Jasig社区维护,经过十多年发展,目前已成为影响最大、广泛使用的、基于Java实现的、开源SSO解决方案。
这里首先需要说明一下SSO的含义和基本流程
含义:有无数业务系统,它们有自己的用户、角色、权限等,现在要做SSO即要将所有业务系统的用户统一到用户中心,去掉业务系统的登录,所有的登录都走用户中心,用户中心登录成功后其他业务系统不需要再次登录
流程:
因此要想做单点登录,必须要有统一的用户中心,那么问题来了,我们肯定有很多老系统,因历史原因没法统一用户,或许是查询数据耦合了用户,或许压根就不是用我们熟悉的技术开发的、等等,我现在面对的就是这种情况,改造老系统需要耗时又耗力,决定用户中心的搭建与业务系统的改造同步进行,在用户中心建好之前先在每个系统里面建立一个统一的用户,统一登录的时候先在用户中心登录之后返回到前端页面根据用户中心返回的ticket去业务系统中获取一个token然后根据token访问各个业务系统,最后在逐步整合各个业务系统的用户到用户中心,因为我们的业务系统之前大多数都是用的spring security 来做的权限验证,因此这次做SSO也是在 spring security 的基础上来做,这样可以兼容之前系统中的相关接口权限。废话不多说直接上代码了
这里首先需要搭建一个cas服务端,相关代码可以去cas官网下载直接放在tomcat下启动,但是这种方式不推荐,还有另外一种overlay的方式,就是将 cas.war 以overlay的方式放到你的自定义cas-server项目中,然后可以在你的 cas-server中实现多种自定义的配置,这里网上的帖子有很多就不赘述了
首先兴建一个 cas-spring-security-boot-starter的spring boot 项目具体目录如下
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.rxsk.cas</groupId>
<artifactId>cas-spring-security-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- security starter Poms -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- security 对CAS支持 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<!--Hutool Java工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.2</version>
</dependency>
</dependencies>
<distributionManagement>
<repository>
<id>你的私服id1</id>
<name>你的私服名称1</name>
<url>http://你的私服地址/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>你的私服id2</id>
<name>你的私服名称2</name>
<url>http://你的私服地址/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>
使用了 spring-security-cas 来集成cas,使用了 jjwt 来生成用户token
CasSecurityConfig security与cas的配置类
package com.rxsk.cas.config;
import com.rxsk.cas.filter.JwtTokenFilter;
import com.rxsk.cas.properties.CasProperties;
import com.rxsk.cas.service.UserDetailsPlusService;
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.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import javax.annotation.Resource;
import java.util.Arrays;
@EnableWebSecurity
@Configuration
@EnableConfigurationProperties(CasProperties.class)
public class CasSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private CasProperties casProperties;
@Resource
private UserDetailsPlusService userDetailsPlusService;
@Resource
private JwtTokenFilter jwtTokenFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider()).userDetailsService(userDetailsPlusService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
//.antMatchers("/**").anonymous()
.antMatchers(casProperties.getIgnoredUrl()).anonymous()
.anyRequest().authenticated()
.and()
.exceptionHandling()
/*.accessDeniedHandler(casAccessDeniedHandler)*/
.authenticationEntryPoint(authenticationEntryPoint())
.and()
.addFilter(casAuthenticationFilter())
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
.addFilterBefore(logoutFilter(), LogoutFilter.class);
}
/**
* 配置CAS Client的属性
*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
// 与CasAuthenticationFilter监视的URL一致
serviceProperties.setService(casProperties.getClientLoginUrl());
//serviceProperties.setServiceParameter(casProperties.getFilterUrlPattern());
// 是否关闭单点登录,默认为false,所以也可以不设置。
serviceProperties.setSendRenew(false);
return serviceProperties;
}
/**
* CAS认证入口,提供用户浏览器重定向的地址
*/
@Bean
@Primary
public AuthenticationEntryPoint authenticationEntryPoint() {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
// CAS Server认证的登录地址
entryPoint.setLoginUrl(casProperties.getServerLoginUrl());
entryPoint.setServiceProperties(serviceProperties());
return entryPoint;
}
/**
* ticket校验,需要提供CAS Server校验ticket的地址
*/
@Bean
public TicketValidator ticketValidator() {
// 默认情况下使用Cas20ProxyTicketValidator,验证入口是${casServerPrefix}/proxyValidate
return new Cas20ProxyTicketValidator(casProperties.getServerUrlPrefix());
}
/**
* cas认证处理逻辑
*/
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidator());
provider.setUserDetailsService(userDetailsPlusService);
provider.setKey("blurooo");
return provider;
}
/**
* 提供CAS认证专用过滤器,过滤器的认证逻辑由CasAuthenticationProvider提供
*/
@Bean
public CasAuthenticationFilter casAuthenticationFilter() {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(serviceProperties());
filter.setAuthenticationManager(new ProviderManager(Arrays.asList(casAuthenticationProvider())));
return filter;
}
/**
* 接受cas服务端发出的注销请求
*/
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(casProperties.getServerUrlPrefix());
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
/**
* 将注销请求转发到cas server
*/
@Bean
public LogoutFilter logoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(casProperties.getServerLogoutUrl(), new SecurityContextLogoutHandler());
// 设置客户端注销请求的路径
logoutFilter.setFilterProcessesUrl(casProperties.getServerLogoutUrl());
return logoutFilter;
}
}
CasProperties cas的配置文件:
package com.rxsk.cas.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "cas")
public class CasProperties {
private String serverUrlPrefix;
private String serverLoginUrl;
private String serverLogoutUrl;
private String filterUrlPattern;
private String clientUrlPrefix;
private String clientLoginUrl;
private String ossLoginUserAccount;
private String ossLoginUserPassword;
private String[] ignoredUrl;
private String jwtTokenHead;
private String jwtSecretKey;
private Long jwtExpired;
private String jwtTokenPrefix;
public String getServerUrlPrefix() {
return serverUrlPrefix;
}
public void setServerUrlPrefix(String serverUrlPrefix) {
this.serverUrlPrefix = serverUrlPrefix;
}
public String getServerLoginUrl() {
return serverLoginUrl;
}
public void setServerLoginUrl(String serverLoginUrl) {
this.serverLoginUrl = serverLoginUrl;
}
public String getServerLogoutUrl() {
return serverLogoutUrl;
}
public void setServerLogoutUrl(String serverLogoutUrl) {
this.serverLogoutUrl = serverLogoutUrl;
}
public String getFilterUrlPattern() {
return filterUrlPattern;
}
public void setFilterUrlPattern(String filterUrlPattern) {
this.filterUrlPattern = filterUrlPattern;
}
public String getClientUrlPrefix() {
return clientUrlPrefix;
}
public void setClientUrlPrefix(String clientUrlPrefix) {
this.clientUrlPrefix = clientUrlPrefix;
}
public String getClientLoginUrl() {
return clientLoginUrl;
}
public void setClientLoginUrl(String clientLoginUrl) {
this.clientLoginUrl = clientLoginUrl;
}
public String getOssLoginUserAccount() {
return ossLoginUserAccount;
}
public void setOssLoginUserAccount(String ossLoginUserAccount) {
this.ossLoginUserAccount = ossLoginUserAccount;
}
public String getOssLoginUserPassword() {
return ossLoginUserPassword;
}
public void setOssLoginUserPassword(String ossLoginUserPassword) {
this.ossLoginUserPassword = ossLoginUserPassword;
}
public String getJwtSecretKey() {
return jwtSecretKey;
}
public void setJwtSecretKey(String jwtSecretKey) {
this.jwtSecretKey = jwtSecretKey;
}
public Long getJwtExpired() {
return jwtExpired;
}
public void setJwtExpired(Long jwtExpired) {
this.jwtExpired = jwtExpired;
}
public String getJwtTokenPrefix() {
return jwtTokenPrefix;
}
public void setJwtTokenPrefix(String jwtTokenPrefix) {
this.jwtTokenPrefix = jwtTokenPrefix;
}
public String getJwtTokenHead() {
return jwtTokenHead;
}
public void setJwtTokenHead(String jwtTokenHead) {
this.jwtTokenHead = jwtTokenHead;
}
public String[] getIgnoredUrl() {
return ignoredUrl;
}
public void setIgnoredUrl(String[] ignoredUrl) {
this.ignoredUrl = ignoredUrl;
}
}
最终将此项目打包成jar并推送到你的私服中,在需要cas集成的业务系统pom.xml 中加入相关的jar
<dependency>
<groupId>com.rxsk.cas</groupId>
<artifactId>cas-spring-security-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在你的配置文件中中加入cas相关配置
# oss单点登录相关配置
cas:
server-url-prefix: http://10.100.3.8:8080/cas
server-login-url: http://10.100.3.8:8080/cas/login
server-logout-url: http://10.100.3.8:8080/cas/logout
filter-url-pattern:
client-url-prefix: http://10.100.12.44:8085/passport
ignored-url: /login/ticket-login
client-login-url: ${cas.client-url-prefix}
oss-login-user-account: admin
oss-login-user-password: 123456
jwt-secret-key: 12345678
jwt-expired: 2592000
jwt-token-prefix: Bearer
jwt-token-head: Authorization
此时你的系统就已经初步具备了单点登录的能力,第一次访问所有需要授权认证的接口都将被重定向到server-login-url: http://10.100.3.8:8080/cas/login 这个地址去做统一认证登录,登录成功后会回调到 client-url-prefix: http://10.100.12.44:8085/passport 这个地址,具体如下http://10.100.12.44:8085/passport?ticket=ST-28-JO1sEEDlNDDcr7fyAxdyeoLMips-DESKTOP-H2TVDRJ
这里会获取到一个 ticket,然后前端在这个页面获取到这个ticket并调用业务系统的 ignored-url: /login/ticket-login 这个接口去验证ticket是否正确,调这个接口需要传递service这个参数,这个参数必须跟 client-url-prefix: http://10.100.12.44:8085/passport 这个参数保持一致,否则验证不通过
最终验证通过后在业务系统中生成一个token并返回给前端,前端拿着这个token就可以访问所有经过改造后统一了token生成规则的所有业务系统了
这里要补充的是有些接口是需要相关权限才能访问的,因此业务系统需要实现
UserDetailsPlusService 这个接口的 loadUserByUserId(Long userId) 这个接口并通过查数据库获取用户权限
@Override
public UserDetailPlus loadUserByUserId(Long userId) throws UsernameNotFoundException {
if (userId == null) {
throw new BusinessException("userId不能空");
}
SysUserDO sysUserDO = sysUserMapper.selectByUserIdAndStatus(userId, null);
if (Objects.isNull(sysUserDO)) {
throw new BusinessException("用户不存在");
}
if(StringUtils.isBlank(sysUserDO.getAccount())){
sysUserDO.setAccount("none");
}
Long parkId = 0L;
if(sysUserDO.getParkId() != null){
parkId = sysUserDO.getParkId();
}else if(sysUserDO.getDefaultParkId() != null){
parkId = sysUserDO.getDefaultParkId();
}
String[] userResource = getUserResource(sysUserDO.getUserId());
return new UserDetailPlus(sysUserDO.getUserId(), parkId, sysUserDO.getUsername(), sysUserDO.getAccount(),
sysUserDO.getPassword(), true, true, true, true,
AuthorityUtils.createAuthorityList(userResource));
}
这里的userId可以通过解析之前返回的token获取到,因此在调 /login/ticket-login 这个接口生成token的时候需要把用户id设置到token中,我们用的是jwt因此可以方便的把用户id保存到jwt的claims中,解析的时候可以拿到用户id
业务系统需要定义 /login/ticket-login 这个接口来验证ticket与返回token
@RequestMapping("/ticket-login") public Response<TicketLoginRespVO> ticketLogin(@RequestBody TicketLoginReqVO req){ TicketLoginRespVO ticketLoginRespVO = new TicketLoginRespVO(); TicketValidator ticketValidator = new Cas20ProxyTicketValidator(casProperties.getServerUrlPrefix()); Assertion casAssertion = null; try { casAssertion = ticketValidator.validate(req.getTicket(), req.getService()); } catch (TicketValidationException e) { e.printStackTrace(); log.error("票据校验异常", e); } AttributePrincipal casPrincipal = casAssertion.getPrincipal(); SysUserDO sysUserDO = loginService.loginByAccountPassword(casProperties.getOssLoginUserAccount(), casProperties.getOssLoginUserPassword()); //生成token,记录到redis LoginRespVO loginRespVO = super.createTokenVO(sysUserDO, true); ticketLoginRespVO.setToken(loginRespVO.getToken()); return Response.builder(ticketLoginRespVO); }
cas-spring-security-boot-starter 已提交到码云,有需要的小伙伴可以找我要或者在之前的文章中有相关的链接地址
有问题可加微信
补充一句,加微信别老是您您您的,都是打工人,不必这么客气,我也才18啊哈哈
请大家关注下博客谢谢 请大家关注下博客谢谢 请大家关注下博客谢谢 重要的事情说三遍