1.cas單點(diǎn)登錄介紹
單點(diǎn)登錄( Single Sign-On , 簡(jiǎn)稱 SSO )是目前比較流行的服務(wù)于企業(yè)業(yè)務(wù)整合的解決方案之一, SSO 使得在多個(gè)應(yīng)用系統(tǒng)中磁椒,用戶只需要 登錄一次 就可以訪問所有相互信任的應(yīng)用系統(tǒng)。
從結(jié)構(gòu)體系看长豁, CAS 包括兩部分: CAS Server 和 CAS Client 算撮。
CAS Server
CAS Server 負(fù)責(zé)完成對(duì)用戶的認(rèn)證工作 , 需要獨(dú)立部署 , CAS Server 會(huì)處理用戶名 / 密碼等憑證(Credentials) 。
CAS Client
負(fù)責(zé)處理對(duì)客戶端受保護(hù)資源的訪問請(qǐng)求萍鲸,需要對(duì)請(qǐng)求方進(jìn)行身份認(rèn)證時(shí),重定向到 CAS Server 進(jìn)行認(rèn)證擦俐。(原則上脊阴,客戶端應(yīng)用不再接受任何的用戶名密碼等 Credentials )。
CAS Client 與受保護(hù)的客戶端應(yīng)用部署在一起蚯瞧,以 Filter 方式保護(hù)受保護(hù)的資源嘿期。
2.cas下載編譯
jasig cas下載官網(wǎng)地址 https://www.apereo.org/projects/cas
github項(xiàng)目下載地址 https://github.com/apereo/cas/releases
我使用下載的是cas-4.1.10
是maven構(gòu)建的項(xiàng)目,如果你要下cas-4.2以上的版本可能就是gradle構(gòu)建的項(xiàng)目埋合,如果你常用的是gradle备徐,那就下載cas-4.2以上的版本,這里比較坑的就是cas的編譯甚颂。
將下載的項(xiàng)目解壓后蜜猾,進(jìn)入到項(xiàng)目目錄里使用mvn clean install
進(jìn)行編譯,這里會(huì)等待很時(shí)間振诬,如果你沒用翻墻可能有些jar包下載不下來瓣铣,如果編譯不了就在網(wǎng)上找別人編譯后的項(xiàng)目,我在編譯的時(shí)候就一直編譯不了贷揽,下載太慢,坑了我很長時(shí)間梦碗。
編譯后將cas-server-webapp項(xiàng)目下的targer的war包放到tomcat的里禽绪,啟動(dòng)tomcat蓖救。這里的詳細(xì)步驟可以參考
Jasig cas 單點(diǎn)登錄系統(tǒng)Server&Java Client配置
這里的搭建cas server一端就不詳細(xì)說了,下面主要說說與spring boot的結(jié)合配置
3.spring boot的配置
這里需要你有一個(gè)spring boot的項(xiàng)目印屁,如何搭建可以參考網(wǎng)上的教程循捺。
引入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
有過spring security的經(jīng)驗(yàn)的應(yīng)該知道關(guān)于權(quán)限登陸的用法 ,實(shí)現(xiàn)AuthenticationUserDetailsService
接口或UserDetailsService
接口雄人,用作登陸驗(yàn)證从橘。
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.ArrayList;
import java.util.List;
/**
* Authenticate a user from the database.
*/
public class CustomUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
String login = token.getPrincipal().toString();
String username = login.toLowerCase();
List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return new AppUserDetails(username, grantedAuthorities);
}
}
然后就用到了userDetails
接口,我們這里實(shí)現(xiàn)這個(gè)接口础钠,并定義一些關(guān)于用戶的信息和權(quán)限恰力。如果你在clien要使用這些信息,比如用戶名旗吁,角色踩萎,權(quán)限等信息,你可以在這里處理一下很钓。
package com.shang.spray.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class AppUserDetails implements UserDetails {
/** */
private static final long serialVersionUID = -4777124807325532850L;
private String username;
private String password;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
private Collection<? extends GrantedAuthority> authorities;
private List<String> roles;
public AppUserDetails() {
super();
}
public AppUserDetails(String username, Collection<? extends GrantedAuthority> authorities) {
super();
this.username = username;
this.password = "";
this.accountNonExpired = true;
this.accountNonLocked = true;
this.credentialsNonExpired = true;
this.enabled = true;
this.authorities = authorities;
this.roles = new ArrayList<>();
this.roles.addAll(authorities.stream().map((Function<GrantedAuthority, String>) GrantedAuthority::getAuthority).collect(Collectors.toList()));
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
/*
* List<GrantedAuthority> l = new ArrayList<GrantedAuthority>(); l.add(new
* GrantedAuthority() { private static final long serialVersionUID = 1L;
*
* @Override public String getAuthority() { return "ROLE_AUTHENTICATED"; } }); return l;
*/
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
接下來就是最重要的一個(gè)步驟香府,繼承實(shí)現(xiàn)WebSecurityConfigurerAdapter
配置類,并配置關(guān)于單點(diǎn)登錄服務(wù)器的一些信息和攔截頁面码倦。這里先放上這個(gè)實(shí)現(xiàn)類
package com.shang.spray.security;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
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.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.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.inject.Inject;
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String CAS_URL_LOGIN = "cas.service.login";
private static final String CAS_URL_LOGOUT = "cas.service.logout";
private static final String CAS_URL_PREFIX = "cas.url.prefix";
private static final String CAS_SERVICE_URL = "app.service.security";
private static final String APP_SERVICE_HOME = "app.service.home";
@Inject
private Environment env;
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties sp = new ServiceProperties();
sp.setService(env.getRequiredProperty(CAS_SERVICE_URL));
sp.setSendRenew(false);
return sp;
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService());
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey("an_id_for_this_auth_provider_only");
return casAuthenticationProvider;
}
@Bean
public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(env.getRequiredProperty(CAS_URL_PREFIX));
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl("/j_spring_cas_security_check");
return casAuthenticationFilter;
}
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(env.getRequiredProperty(CAS_URL_LOGIN));
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(env.getRequiredProperty(CAS_URL_PREFIX));
return singleSignOutFilter;
}
@Bean
public LogoutFilter requestCasGlobalLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(env.getRequiredProperty(CAS_URL_LOGOUT) + "?service="
+ env.getRequiredProperty(APP_SERVICE_HOME), new SecurityContextLogoutHandler());
logoutFilter.setLogoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST"));
return logoutFilter;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(casAuthenticationFilter());
http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint());
http.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
.addFilterBefore(requestCasGlobalLogoutFilter(), LogoutFilter.class);
http.csrf().disable();
http.headers().frameOptions().disable();
http.authorizeRequests().antMatchers("/assets/**").permitAll()
.anyRequest().authenticated();
http.logout().logoutUrl("/logout").logoutSuccessUrl("/").invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
}
}
配置信息
#cas
app.service.security=http://localhost:8200/j_spring_cas_security_check
app.service.home=http://localhost:8200
cas.service.login=http://localhost:8080/cas/login
cas.service.logout=http://localhost:8080/cas/logout
cas.url.prefix=http://localhost:8080/cas
關(guān)于上面的一些說明:
- serviceProperties單點(diǎn)登錄服務(wù)器地址
- casAuthenticationProvider 權(quán)限登錄配置
- 客戶端登錄地址和退出地址的配置
- 客戶端攔截的路徑配置
- 配置后需要啟用配置
@Configuration
和@EnableWebSecurity
新更新內(nèi)容
上面的版本有些問題企孩,我這里做了下新修改。
項(xiàng)目地址為:https://github.com/shangjing105/spray-module
cas項(xiàng)目為里面的spray-manage-cas
與cas服務(wù)端spray-cas
j_spring_cas_security_check
是每個(gè)項(xiàng)目客戶端spring secrity驗(yàn)證ticket的地址袁稽,spring secrity
版本4.0以下的是用j_spring_cas_security_check這個(gè)地址勿璃,4.0以上的用的是/login/cas
地址,配置的時(shí)候注意項(xiàng)目版本运提。
//因?yàn)槲矣玫腸as版本是4.0以上蝗柔,所以修改為/login/cas
app.service.security=http://localhost:8400/login/cas
app.service.home=http://localhost:8400
cas.service.login=http://localhost:8080/login
cas.service.logout=http://localhost:8080/logout
cas.url.prefix=http://localhost:8080
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
//這里給上面的配置去掉,因?yàn)槟J(rèn)就是/login/cas民泵,可以不用配置癣丧。如果是4.0以前就是j_spring_cas_security_check
return casAuthenticationFilter;
}
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
//這里增加一個(gè)條件,不然啟動(dòng)時(shí)會(huì)出錯(cuò):casserverurlprefix cannot be null
singleSignOutFilter.setIgnoreInitConfiguration(true);
singleSignOutFilter.setCasServerUrlPrefix(env.getRequiredProperty(CAS_URL_PREFIX));
return singleSignOutFilter;
}
4.結(jié)束
以上單點(diǎn)登錄cas與spring boot的配置使用栈妆,對(duì)于有多個(gè)后臺(tái)的系統(tǒng)胁编,寫一個(gè)單獨(dú)的單點(diǎn)登錄系統(tǒng)來對(duì)所有的平臺(tái)來管理感覺非常有必要。 沒有使用過的可以去嘗試一下鳞尔,簡(jiǎn)單好用你值的一學(xué)嬉橙。
有什么問題歡迎給我來信或留言!