SpringSecurity系列之SpringBoot+CAS單點(diǎn)登錄
1.準(zhǔn)備工作
準(zhǔn)備工作主要做兩件事。
1.1 服務(wù)記錄
某一個(gè) Client 需要接入 CAS Server 進(jìn)行驗(yàn)證,則該 Client 必須提前在 CAS Server 上配置其信息。
這個(gè)信息既可以動(dòng)態(tài)添加,也可以通過 JSON 來配置,后面松哥會教搭建如何動(dòng)態(tài)添加,這里方便起見,我們還是通過 JSON 來進(jìn)行配置。
具體配置方式如下,在 CAS Server 中創(chuàng)建如下目錄:
- src/main/resources/services
在該目錄下創(chuàng)建一個(gè)名為 client1-99.json 的文件,client1 表示要接入的 client 的名字,99 表示要接入的 client 的 id,json 文件內(nèi)容如下(這個(gè)配置可以參考官方給出的模版:overlays/org.apereo.cas.cas-server-webapp-tomcat-5.3.14/WEB-INF/classes/services/Apereo-10000002.json):
- {
- "@class": "org.apereo.cas.services.RegexRegisteredService",
- "serviceId": "^(https|http)://.*",
- "name": "client1",
- "id": 99,
- "description": "應(yīng)用1 的定義信息",
- "evaluationOrder": 1
- }
這段 JSON 配置含義如下:
- @calss 指定注冊服務(wù)類,這個(gè)是固定的org.apereo.cas.services.RegexRegisteredService。
- serviceId 則通過正則表達(dá)式用來匹配具體的請求。
- name 是接入的 client 的名稱。
- id 是接入的 client 的 id。
- description 是接入的 client 的描述信息。
- evaluationOrder 則指定了執(zhí)行的優(yōu)先級。
接下來再在 src/main/resources/application.properties 文件中配置剛剛 json 的信息,如下:
- cas.serviceRegistry.json.location=classpath:/services
- cas.serviceRegistry.initFromJson=true
這里有兩行配置:
- 指定配置 JSON 文件的位置。
- 開啟 JSON 識別。
OK,配置完成后,重啟 CAS Server。
CAS Server 啟動(dòng)成功后,我們在控制臺看到如下日志,表示 JSON 配置已經(jīng)加載成功了:
1.2 JDK 證書
第二個(gè)要提前準(zhǔn)備的東西就是 JDK 證書。
在實(shí)際開發(fā)中,這一步可以忽略,但是因?yàn)槲覀儸F(xiàn)在用的自己生成的 SSL 證書,所以我們要將自己生成的證書導(dǎo)入到 JDK 中,否則在使用 Spring Security 接入 CAS 單點(diǎn)登錄時(shí),會拋出如下錯(cuò)誤:
將 SSL 證書導(dǎo)入 JDK 中的命令其實(shí)也很簡單,兩個(gè)步驟,第一個(gè)導(dǎo)出 .cer 文件,第二步,導(dǎo)入 JDK,命令如下:
- keytool -export -trustcacerts -alias casserver -file ./cas.cer -keystore ./keystore
- sudo keytool -import -trustcacerts -alias casserver -file ./cas.cer -keystore /Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home/lib/security/cacerts
注意,在執(zhí)行 export 導(dǎo)出命令時(shí),需要輸入密鑰口令,這個(gè)口令就是自己一開始創(chuàng)建 SSL 證書時(shí)設(shè)置的。在執(zhí)行 import 導(dǎo)入命令時(shí),也需要輸入口令,這個(gè)口令是 changeit,注意,不是自己一開始設(shè)置的。
密鑰庫的位置在 JDK 目錄下的 /lib/security/cacerts,小伙伴們根據(jù)自己實(shí)際情況來修改(在 JDK9 之前,位置在 jre/lib/security/cacerts)。
我們在本地測試一定要導(dǎo)入證書到 JDK 證書庫中,否則后面的測試會出現(xiàn)上圖中的錯(cuò)誤,證書導(dǎo)入 JDK 證書庫之后,要確保之后的開發(fā)中,使用的是本地的 JDK。
注意,JDK 證書導(dǎo)入之后,CASServer 需要重啟一下。
1.3 修改 hosts
另外,我們還需要修改電腦 hosts 文件,因?yàn)榍懊骊P(guān)于 CAS Server,關(guān)于 SSL 證書的配置都涉及到域名,所以后面的訪問我們將通過域名的形式訪問,hosts 文件中添加如下兩條記錄:
第一個(gè)是 CAS Server 的請求域名,第二個(gè)是 CAS Client 的請求域名。
2.開發(fā) Client
在使用 Spring Security 開發(fā) CAS Client 之前,有一個(gè)基本問題需要先和小伙伴們捋清楚:用戶登錄是在 CAS Server 上登錄,所以 Spring Security 中雖然依舊存在用戶的概念,但是對于用戶的處理邏輯會和前面的有所不同。
好了,接下來我們來看下具體步驟。
首先我們來創(chuàng)建一個(gè)普通的 Spring Boot 項(xiàng)目,加入 Web 依賴 和 Spring Security 依賴,如下:
項(xiàng)目創(chuàng)建成功后,我們再來手動(dòng)加入 cas 依賴:
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-cas</artifactId>
- </dependency>
接下來,在 application.properties 中配置 CAS Server 和 CAS Client 的請求地址信息:
- cas.server.prefix=https://cas.javaboy.org:8443/cas
- cas.server.login=${cas.server.prefix}/login
- cas.server.logout=${cas.server.prefix}/logout
- cas.client.prefix=http://client1.cas.javaboy.org:8080
- cas.client.login=${cas.client.prefix}/login/cas
- cas.client.logoutRelative=/logout/cas
- cas.client.logout=${cas.client.prefix}${cas.client.logoutRelative}
這些配置都是自定義配置,所以配置的 key 可以自己隨意定義。至于配置的含義都好理解,分別配置了 CAS Server 和 CAS Client 的登錄和注銷地址。
配置好之后,我們需要將這些配置注入到實(shí)體類中使用,這里就用到了類型安全的屬性綁定。
這里我創(chuàng)建兩個(gè)類分別用來接收 CAS Server 和 CAS Client 的配置文件:
- @ConfigurationProperties(prefix = "cas.server")
- public class CASServerProperties {
- private String prefix;
- private String login;
- private String logout;
- //省略 getter/setter
- }
- @ConfigurationProperties(prefix = "cas.client")
- public class CASClientProperties {
- private String prefix;
- private String login;
- private String logoutRelative;
- private String logout;
- //省略 getter/setter
- }
另外記得在啟動(dòng)類上面添加 @ConfigurationPropertiesScan 注解來掃描這兩個(gè)配置類:
- @SpringBootApplication
- @ConfigurationPropertiesScan
- public class Client1Application {
- public static void main(String[] args) {
- SpringApplication.run(Client1Application.class, args);
- }
- }
這里配置完成后,我們一會將在配置文件中來使用。
接下來創(chuàng)建 CAS 的配置文件,略長:
- @Configuration
- public class CasSecurityConfig {
- @Autowired
- CASClientProperties casClientProperties;
- @Autowired
- CASServerProperties casServerProperties;
- @Autowired
- UserDetailsService userDetailService;
- @Bean
- ServiceProperties serviceProperties() {
- ServiceProperties serviceProperties = new ServiceProperties();
- serviceProperties.setService(casClientProperties.getLogin());
- return serviceProperties;
- }
- @Bean
- @Primary
- AuthenticationEntryPoint authenticationEntryPoint() {
- CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
- entryPoint.setLoginUrl(casServerProperties.getLogin());
- entryPoint.setServiceProperties(serviceProperties());
- return entryPoint;
- }
- @Bean
- TicketValidator ticketValidator() {
- return new Cas20ProxyTicketValidator(casServerProperties.getPrefix());
- }
- @Bean
- CasAuthenticationProvider casAuthenticationProvider() {
- CasAuthenticationProvider provider = new CasAuthenticationProvider();
- provider.setServiceProperties(serviceProperties());
- provider.setTicketValidator(ticketValidator());
- provider.setUserDetailsService(userDetailService);
- provider.setKey("javaboy");
- return provider;
- }
- @Bean
- CasAuthenticationFilter casAuthenticationFilter(AuthenticationProvider authenticationProvider) {
- CasAuthenticationFilter filter = new CasAuthenticationFilter();
- filter.setServiceProperties(serviceProperties());
- filter.setAuthenticationManager(new ProviderManager(authenticationProvider));
- return filter;
- }
- @Bean
- SingleSignOutFilter singleSignOutFilter() {
- SingleSignOutFilter sign = new SingleSignOutFilter();
- sign.setIgnoreInitConfiguration(true);
- return sign;
- }
- @Bean
- LogoutFilter logoutFilter() {
- LogoutFilter filter = new LogoutFilter(casServerProperties.getLogout(), new SecurityContextLogoutHandler());
- filter.setFilterProcessesUrl(casClientProperties.getLogoutRelative());
- return filter;
- }
- }
這個(gè)配置文件略長,但是并不難,我來和大家挨個(gè)解釋:
- 首先一進(jìn)來注入三個(gè)對象,這三個(gè)中,有兩個(gè)是我們前面寫的配置類的實(shí)例,另外一個(gè)則是 UserDetailsService,關(guān)于 UserDetailsService,我想我也不必多做解釋,大家參考本系列前面的文章就知道 UserDetailsService 的作用,一會我會給出 UserDetailsService 的實(shí)現(xiàn)。
- 接下來配置 ServiceProperties,ServiceProperties 中主要配置一下 Client 的登錄地址即可,這個(gè)地址就是在 CAS Server 上登錄成功后,重定向的地址。
- CasAuthenticationEntryPoint 則是 CAS 驗(yàn)證的入口,這里首先設(shè)置 CAS Server 的登錄地址,同時(shí)將前面的 ServiceProperties 設(shè)置進(jìn)去,這樣當(dāng)它登錄成功后,就知道往哪里跳轉(zhuǎn)了。
- TicketValidator 這是配置 ticket 校驗(yàn)地址,CAS Client 拿到 ticket 要去 CAS Server 上校驗(yàn),默認(rèn)校驗(yàn)地址是:https://cas.javaboy.org:8443/cas/proxyValidate?ticket=xxx
- CasAuthenticationProvider 主要用來處理 CAS 驗(yàn)證邏輯,關(guān)于 AuthenticationProvider 松哥在前面的文章中和大家分享過(SpringSecurity 自定義認(rèn)證邏輯的兩種方式(高級玩法)),當(dāng)時(shí)就說,想要自定義認(rèn)證邏輯,如短信登錄等,都可以通過擴(kuò)展 AuthenticationProvider 來實(shí)現(xiàn),這里的 CAS 登錄當(dāng)然也不例外,這里雖然設(shè)置了一個(gè) userDetailService,但是目的不是為了從數(shù)據(jù)庫中查詢數(shù)據(jù)做校驗(yàn),因?yàn)榈卿浭窃?CAS Server 中進(jìn)行的,這個(gè)的作用,我在后面會做介紹。
- CasAuthenticationFilter 則是 CAS 認(rèn)證的過濾器,過濾器將請求攔截下來之后,交由 CasAuthenticationProvider 來做具體處理。
- SingleSignOutFilter 表示接受 CAS Server 發(fā)出的注銷請求,所有的注銷請求都將從 CAS Client 轉(zhuǎn)發(fā)到 CAS Server,CAS Server 處理完后,會通知所有的 CAS Client 注銷登錄。
- LogoutFilter 則是配置將注銷請求轉(zhuǎn)發(fā)到 CAS Server。
接下來我再來給大家看下我定義的 UserDetailsService:
- @Component
- @Primary
- public class UserDetailsServiceImpl implements UserDetailsService{
- @Override
- public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
- return new User(s, "123", true, true, true, true,
- AuthorityUtils.createAuthorityList("ROLE_user"));
- }
- }
既然是單點(diǎn)登錄,也就是用戶是在 CAS Server 上登錄的,這里的 UserDetailsService 意義在哪里呢?
用戶雖然在 CAS Server 上登錄,但是,登錄成功之后,CAS Client 還是要獲取用戶的基本信息、角色等,以便做進(jìn)一步的權(quán)限控制,所以,這里的 loadUserByUsername 方法中的參數(shù),實(shí)際上就是你從 CAS Server 上登錄成功后獲取到的用戶名,拿著這個(gè)用戶名,去數(shù)據(jù)庫中查詢用戶的相關(guān)信心并返回,方便 CAS Client 在后續(xù)的鑒權(quán)中做進(jìn)一步的使用,這里我為了方便,就沒有去數(shù)據(jù)庫中查詢了,而是直接創(chuàng)建了一個(gè) User 對象返回。
接下來,我們再來看看 Spring Security 的配置:
- @Configuration
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- @Autowired
- AuthenticationEntryPoint authenticationEntryPoint;
- @Autowired
- AuthenticationProvider authenticationProvider;
- @Autowired
- SingleSignOutFilter singleSignOutFilter;
- @Autowired
- LogoutFilter logoutFilter;
- @Autowired
- CasAuthenticationFilter casAuthenticationFilter;
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- auth.authenticationProvider(authenticationProvider);
- }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.authorizeRequests().antMatchers("/user/**")
- .hasRole("user")
- .antMatchers("/login/cas").permitAll()
- .anyRequest().authenticated()
- .and()
- .exceptionHandling()
- .authenticationEntryPoint(authenticationEntryPoint)
- .and()
- .addFilter(casAuthenticationFilter)
- .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
- .addFilterBefore(logoutFilter, LogoutFilter.class);
- }
- }
這里的配置就簡單很多了:
- 首先配置 authenticationProvider,這個(gè) authenticationProvider 實(shí)際上就是一開始配置的 CasAuthenticationProvider。
- 接下來配置 /user/** 格式的路徑需要有 user 角色才能訪問,登錄路徑 /login/cas 可以直接訪問,剩余接口都是登錄成功之后才能訪問。
- 最后把 authenticationEntryPoint 配置進(jìn)來,再把自定義的過濾器加進(jìn)來,這些都比較容易我就不多說了。
最后,再提供兩個(gè)測試接口:
- @RestController
- public class HelloController {
- @GetMapping("/hello")
- public String hello() {
- return "hello";
- }
- @GetMapping("/user/hello")
- public String user() {
- return "user";
- }
- }
OK ,如此之后,我們的 CAS Client 現(xiàn)在就開發(fā)完成了,接下來啟動(dòng) CAS Client,啟動(dòng)成功后,瀏覽器輸入 http://client1.cas.javaboy.org:8080/user/hello 訪問 hello 接口,此時(shí)會自動(dòng)跳轉(zhuǎn)到 CAS Server 上登錄,登錄成功之后,經(jīng)過兩個(gè)重定向,會重新回到 hello 接口。
3.小結(jié)
OK,這就是松哥和大家介紹的 Spring Security + CAS 單點(diǎn)登錄,當(dāng)然,這個(gè)案例中還有很多需要完善的地方,松哥會在后面的文章中繼續(xù)和大家分享完善的方案。
好了 ,本文就說到這里,本文相關(guān)案例我已經(jīng)上傳到 GitHub ,大家可以自行下載:https://github.com/lenve/spring-security-samples
本文轉(zhuǎn)載自微信公眾號「江南一點(diǎn)雨」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系江南一點(diǎn)雨公眾號。