最近接了一個任務,公司之前為客戶做了很多的系統,後面做成了通用的業務系統準備向外銷售,是以需要做一個示範系統将所有業務系統都放到示範系統中,使用者在示範系統登入後可以通路其中的任意業務系統,這一聽就是一個單點登入的需求啊,是以就去了解了下,發現了CAS,
CAS是中央認證服務
Central Authentication Service
的簡稱。最初由耶魯大學的Shawn Bayern 開發,後由Jasig社群維護,經過十多年發展,目前已成為影響最大、廣泛使用的、基于Java實作的、開源SSO解決方案。
這裡首先需要說明一下oss的含義和基本流程
含義:有無數業務系統,它們有自己的使用者、角色、權限等,現在要做oss即要将所有業務系統的使用者統一到使用者中心,去掉業務系統的登入,所有的登入都走使用者中心,使用者中心登入成功後其他業務系統不需要再次登入
流程:我在網上找了一個流程解釋的很清晰,可以看下https://www.cnblogs.com/Eleven-Liu/p/10336181.html
是以要想做單點登入,必須要有統一的使用者中心,那麼問題來了,我們肯定有很多老系統,因曆史原因沒法統一使用者,或許是查詢資料耦合了使用者,或許壓根就不是用我們熟悉的技術開發的、等等,我現在面對的就是這種情況,改造老系統需要耗時又耗力,決定使用者中心的搭建與業務系統的改造同步進行,在使用者中心建好之前先在每個系統裡面建立一個統一的使用者,統一登入的時候先在使用者中心登入之後傳回到前端頁面根據使用者中心傳回的ticket去業務系統中擷取一個token然後根據token通路各個業務系統,最後在逐漸整合各個業務系統的使用者到使用者中心,因為我們的業務系統之前大多數都是用的spring security 來做的權限驗證,是以這次做oss也是在 spring security 的基礎上來做,這樣可以相容之前系統中的相關接口權限。廢話不多說直接上代碼了
這裡首先需要搭建一個cas服務端,相關代碼可以去cas官網下載下傳直接放在tomcat下啟動,但是這種方式不推薦,還有另外一種overlay的方式,就是将 cas.war 以overlay的方式放到你的自定義cas-server項目中,然後可以在你的 cas-server中實作多種自定義的配置,這裡網上的文章有很多就不贅述了
首先興建一個 cas-spring-security-boot-starter的spring boot 項目具體目錄如下
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwczX0xiRGZkRGZ0Xy9GbvNGL2EzXlpXazxSP9E0T0QzVZFDaykFcGdUYxgnMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLwkTNxUjMwATM0IDOwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
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啊哈哈
請大家關注下部落格謝謝 請大家關注下部落格謝謝 請大家關注下部落格謝謝 重要的事情說三遍